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.Application;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Oms.Application.Dtos.Marketing;
|
||
using Oms.Application.Dtos.Orders;
|
||
using Oms.Application.Shared;
|
||
using Oms.Domain.OrdersAggregate.Entities;
|
||
|
||
namespace Oms.Application.Services.Orders;
|
||
|
||
public partial class OrderService
|
||
{
|
||
private const string CouponNotBetterThanLevelReason = "优惠不高于会员折扣";
|
||
|
||
/// <summary>
|
||
/// 支付预览:按当前应收计算会员折/券互斥后的应付,并返回本单可选券。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 Id;couponInstanceId 为 null 自动推荐,0 不用券,大于 0 试算指定券。</param>
|
||
/// <returns>应付拆解、实际生效券与可选券列表;指定券不可用(未解锁/过期/线路不符等)时抛业务异常。</returns>
|
||
[HttpGet("/order/pay-preview")]
|
||
public async Task<OrderPayPreviewOutput> GetPayPreviewAsync([FromQuery] OrderPayPreviewInput input)
|
||
{
|
||
var order = await GetOrderOrThrowAsync(input.Id);
|
||
var fees = await OrderRepository.GetFeesByOrderIdsAsync([order.Id]);
|
||
var receivableSubtotal = MarketingFeeHelper.SumReceivable(fees);
|
||
var baseFreightAmount = MarketingFeeHelper.SumReceivableBaseFreight(fees);
|
||
var marketingHelper = LazyServiceProvider.GetRequiredService<MarketingOrderHelper>();
|
||
|
||
var memberOnly = await marketingHelper.BuildBreakdownAsync(
|
||
order.CustomerId,
|
||
receivableSubtotal,
|
||
baseFreightAmount,
|
||
null,
|
||
order.ConsigneeCountry,
|
||
order.Id);
|
||
var coupons = await marketingHelper.BuildAvailableCouponsAsync(
|
||
order.CustomerId,
|
||
baseFreightAmount,
|
||
order.ConsigneeCountry,
|
||
order.Id);
|
||
foreach (var coupon in coupons.Where(x => x.CanUse
|
||
&& (x.EstimatedDiscount ?? 0M) <= memberOnly.LevelDiscountAmount))
|
||
{
|
||
coupon.CanUse = false;
|
||
coupon.UnavailableReason = CouponNotBetterThanLevelReason;
|
||
}
|
||
|
||
coupons = coupons
|
||
.OrderByDescending(x => x.CanUse)
|
||
.ThenByDescending(x => x.EstimatedDiscount ?? 0M)
|
||
.ThenBy(x => x.ValidEndTime)
|
||
.ToList();
|
||
|
||
var targetCouponId = input.CouponInstanceId switch
|
||
{
|
||
0 => null,
|
||
> 0 => input.CouponInstanceId,
|
||
_ => ResolveRecommendedPayCouponId(order, coupons)
|
||
};
|
||
|
||
var breakdown = targetCouponId.HasValue
|
||
? await marketingHelper.BuildBreakdownAsync(
|
||
order.CustomerId,
|
||
receivableSubtotal,
|
||
baseFreightAmount,
|
||
targetCouponId,
|
||
order.ConsigneeCountry,
|
||
order.Id)
|
||
: memberOnly;
|
||
|
||
return new OrderPayPreviewOutput
|
||
{
|
||
Id = order.Id,
|
||
ReceivableSubtotal = breakdown.ReceivableSubtotal,
|
||
BaseFreightAmount = breakdown.BaseFreightAmount,
|
||
LevelDiscountAmount = breakdown.LevelDiscountAmount,
|
||
CouponDiscountAmount = breakdown.CouponDiscountAmount,
|
||
PayableAmount = breakdown.PayableAmount,
|
||
MemberLevelName = breakdown.MemberLevelName,
|
||
AppliedCouponInstanceId = breakdown.CouponInstanceId,
|
||
AvailableCoupons = coupons
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算支付应付:couponInstanceId 为 null 沿用订单已绑定券(不可用则不用券),0 不用券,大于 0 使用指定券。
|
||
/// </summary>
|
||
private async Task<MarketingPriceBreakdown> BuildPayBreakdownAsync(
|
||
Order order,
|
||
IEnumerable<OrderFee> fees,
|
||
int? couponInstanceId)
|
||
{
|
||
var feeList = fees as IList<OrderFee> ?? fees.ToList();
|
||
var receivableSubtotal = MarketingFeeHelper.SumReceivable(feeList);
|
||
var baseFreightAmount = MarketingFeeHelper.SumReceivableBaseFreight(feeList);
|
||
var marketingHelper = LazyServiceProvider.GetRequiredService<MarketingOrderHelper>();
|
||
|
||
Task<MarketingPriceBreakdown> Build(int? couponId) =>
|
||
marketingHelper.BuildBreakdownAsync(
|
||
order.CustomerId,
|
||
receivableSubtotal,
|
||
baseFreightAmount,
|
||
couponId,
|
||
order.ConsigneeCountry,
|
||
order.Id);
|
||
|
||
if (couponInstanceId is > 0)
|
||
{
|
||
var breakdown = await Build(couponInstanceId);
|
||
if (breakdown.CouponInstanceId != couponInstanceId)
|
||
{
|
||
throw new BusinessException("所选优惠券未达使用门槛或优惠不高于会员折扣,请重新选择");
|
||
}
|
||
|
||
return breakdown;
|
||
}
|
||
|
||
if (couponInstanceId == 0 || order.CouponInstanceId is not > 0)
|
||
{
|
||
return await Build(null);
|
||
}
|
||
|
||
try
|
||
{
|
||
return await Build(order.CouponInstanceId);
|
||
}
|
||
catch (BusinessException)
|
||
{
|
||
return await Build(null);
|
||
}
|
||
}
|
||
|
||
private static int? ResolveRecommendedPayCouponId(Order order, List<CouponAvailableOutput> coupons)
|
||
{
|
||
if (order.CouponInstanceId is > 0
|
||
&& coupons.Any(x => x.CanUse && x.CouponInstanceId == order.CouponInstanceId.Value))
|
||
{
|
||
return order.CouponInstanceId;
|
||
}
|
||
|
||
return coupons.FirstOrDefault(x => x.CanUse)?.CouponInstanceId;
|
||
}
|
||
}
|