All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
- 新增 GET /order/pay-preview:返回会员折/券互斥后的应付拆解与本单可选券 - POST /order/pay 支持 couponInstanceId(null 沿用已绑定券,0 不用券,>0 指定券),按试算应付扣款并核销券、释放被替换券的锁定 - 被当前订单锁定的券视为可用,修复入库/编辑重算营销金额时误报券被占用 Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
5.3 KiB
C#
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);
|
|
}
|
|
}
|