admin/src/Oms.Application/Shared/MarketingOrderHelper.cs
zzh 223d32f62a
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
feat: 订单支付时选券,新增支付预览接口并按所选券扣款核销
- 新增 GET /order/pay-preview:返回会员折/券互斥后的应付拆解与本单可选券
- POST /order/pay 支持 couponInstanceId(null 沿用已绑定券,0 不用券,>0 指定券),按试算应付扣款并核销券、释放被替换券的锁定
- 被当前订单锁定的券视为可用,修复入库/编辑重算营销金额时误报券被占用

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 14:01:00 +08:00

143 lines
5.3 KiB
C#

using Fake.DependencyInjection;
using Oms.Application.Dtos.Marketing;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.MarketingAggregate.Entities;
using Oms.Domain.MarketingAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Shared;
/// <summary>
/// 订单营销快照写入与查价叠加。
/// </summary>
public class MarketingOrderHelper : IScopedDependency
{
private readonly ICustomerRepository _customerRepository;
private readonly IMemberLevelRepository _memberLevelRepository;
private readonly MarketingCouponManager _couponManager;
public MarketingOrderHelper(
ICustomerRepository customerRepository,
IMemberLevelRepository memberLevelRepository,
MarketingCouponManager couponManager)
{
_customerRepository = customerRepository;
_memberLevelRepository = memberLevelRepository;
_couponManager = couponManager;
}
public async Task<MarketingPriceBreakdown> BuildBreakdownAsync(
int? customerId,
decimal receivableSubtotal,
int? couponInstanceId,
CancellationToken cancellationToken = default)
{
return await BuildBreakdownAsync(
customerId,
receivableSubtotal,
receivableSubtotal,
couponInstanceId,
cancellationToken);
}
public async Task<MarketingPriceBreakdown> BuildBreakdownAsync(
int? customerId,
decimal receivableSubtotal,
decimal baseFreightAmount,
int? couponInstanceId,
CancellationToken cancellationToken = default)
{
return await BuildBreakdownAsync(
customerId,
receivableSubtotal,
baseFreightAmount,
couponInstanceId,
null,
cancellationToken: cancellationToken);
}
/// <summary>
/// 计算会员折/券互斥后的应付拆解;<paramref name="ownerOrderId"/> 为当前订单时允许使用被该订单锁定的券。
/// </summary>
public async Task<MarketingPriceBreakdown> BuildBreakdownAsync(
int? customerId,
decimal receivableSubtotal,
decimal baseFreightAmount,
int? couponInstanceId,
string? destinationCountry,
int? ownerOrderId = null,
CancellationToken cancellationToken = default)
{
if (!customerId.HasValue || customerId.Value <= 0)
{
return MarketingBillingCalculator.Calculate(
receivableSubtotal, baseFreightAmount, 0M, null, null, null, null);
}
var sessionCustomer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == customerId.Value, cancellationToken);
if (sessionCustomer == null)
{
return MarketingBillingCalculator.Calculate(
receivableSubtotal, baseFreightAmount, 0M, null, null, null, null);
}
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(_customerRepository, sessionCustomer, cancellationToken);
var mainCustomerId = mainCustomer.Id;
MemberLevel? level = null;
if (mainCustomer.MemberLevelId.HasValue)
{
level = await _memberLevelRepository.FirstOrDefaultAsync(x => x.Id == mainCustomer.MemberLevelId.Value, cancellationToken);
}
var priceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(mainCustomer.PriceTierKey);
var couponInput = await _couponManager.ResolveCouponDiscountInputAsync(
couponInstanceId, mainCustomerId, destinationCountry, ownerOrderId, cancellationToken);
return MarketingBillingCalculator.Calculate(
receivableSubtotal,
baseFreightAmount,
level?.ExtraDiscountRate ?? 0M,
level?.Id,
level?.LevelName,
priceTierKey,
couponInput);
}
public void ApplySnapshotToOrder(Order order, MarketingPriceBreakdown breakdown, int? couponInstanceId)
{
order.MemberLevelId = breakdown.MemberLevelId;
order.PriceTierKey = breakdown.PriceTierKey;
order.ReceivableSubtotal = breakdown.ReceivableSubtotal;
order.LevelDiscountAmount = breakdown.LevelDiscountAmount;
order.CouponInstanceId = couponInstanceId ?? breakdown.CouponInstanceId;
order.CouponDiscountAmount = breakdown.CouponDiscountAmount;
order.PayableAmount = breakdown.PayableAmount;
}
public async Task<List<CouponAvailableOutput>> BuildAvailableCouponsAsync(
int? customerId,
decimal? baseFreightAmount,
string? destinationCountry = null,
int? ownerOrderId = null,
CancellationToken cancellationToken = default)
{
if (!customerId.HasValue || customerId.Value <= 0)
{
return [];
}
var sessionCustomer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == customerId.Value, cancellationToken);
if (sessionCustomer == null)
{
return [];
}
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(
_customerRepository,
sessionCustomer,
cancellationToken);
return await _couponManager.BuildAvailableCouponsAsync(
mainCustomer.Id, baseFreightAmount, destinationCountry, ownerOrderId, cancellationToken);
}
}