using Oms.Application.Dtos.Marketing; namespace Oms.Application.Shared; /// /// 会员折扣仅作用于基础运费;与优惠券不叠加,取更低应付(同价保留会员折,避免占券)。 /// public static class MarketingBillingCalculator { public const decimal MaxFreightOffRate = 0.05M; public static MarketingPriceBreakdown Calculate( decimal receivableSubtotal, decimal extraDiscountRate, int? memberLevelId, string? memberLevelName, string? priceTierKey, CouponDiscountInput? coupon) { return Calculate( receivableSubtotal, receivableSubtotal, extraDiscountRate, memberLevelId, memberLevelName, priceTierKey, coupon); } public static MarketingPriceBreakdown Calculate( decimal receivableSubtotal, decimal baseFreightAmount, decimal extraDiscountRate, int? memberLevelId, string? memberLevelName, string? priceTierKey, CouponDiscountInput? coupon) { receivableSubtotal = Math.Max(0M, receivableSubtotal); baseFreightAmount = Math.Max(0M, Math.Min(baseFreightAmount, receivableSubtotal)); extraDiscountRate = Math.Clamp(extraDiscountRate, 0M, MaxFreightOffRate); var levelDiscount = Math.Round(baseFreightAmount * extraDiscountRate, 2, MidpointRounding.AwayFromZero); var payableMember = Math.Max(0M, receivableSubtotal - levelDiscount); var couponDiscount = coupon == null ? 0M : CalculateCouponDiscount(coupon, baseFreightAmount); var payableCoupon = Math.Max(0M, receivableSubtotal - couponDiscount); var useCoupon = coupon != null && couponDiscount > 0M && payableCoupon < payableMember; if (useCoupon) { return Build( receivableSubtotal, baseFreightAmount, 0M, couponDiscount, payableCoupon, memberLevelId, memberLevelName, priceTierKey, coupon!.CouponInstanceId); } return Build( receivableSubtotal, baseFreightAmount, levelDiscount, 0M, payableMember, memberLevelId, memberLevelName, priceTierKey, null); } /// /// 仅按基础运费判断门槛并抵扣,封顶不超过基础运费(不找零、不冲附加费)。 /// public static decimal CalculateCouponDiscount(CouponDiscountInput coupon, decimal baseFreightAmount) { if (baseFreightAmount <= 0) { return 0M; } if (baseFreightAmount < coupon.MinOrderAmount) { return 0M; } decimal discount = coupon.CouponType switch { MarketingCouponTypeCodes.FixedOff => coupon.FaceValue ?? 0M, MarketingCouponTypeCodes.DirectOff => coupon.FaceValue ?? 0M, MarketingCouponTypeCodes.PercentOff => CalculatePercentOff(baseFreightAmount, coupon.DiscountRate, coupon.MaxDiscountAmount), MarketingCouponTypeCodes.ShippingOff => coupon.FaceValue ?? 0M, _ => 0M }; return Math.Min(Math.Max(0M, discount), baseFreightAmount); } private static decimal CalculatePercentOff(decimal baseAmount, decimal? discountRate, decimal? maxDiscount) { if (!discountRate.HasValue || discountRate.Value <= 0 || discountRate.Value >= 1) { return 0M; } var discount = Math.Round(baseAmount * (1M - discountRate.Value), 2, MidpointRounding.AwayFromZero); if (maxDiscount.HasValue && maxDiscount.Value > 0) { discount = Math.Min(discount, maxDiscount.Value); } return discount; } public static decimal CalculateCommission(decimal basePayableAmount, decimal baseAmount, decimal commissionAmount) { if (basePayableAmount <= 0 || baseAmount <= 0 || commissionAmount <= 0) { return 0M; } var segments = Math.Floor(basePayableAmount / baseAmount); return segments * commissionAmount; } private static MarketingPriceBreakdown Build( decimal receivableSubtotal, decimal baseFreightAmount, decimal levelDiscount, decimal couponDiscount, decimal payable, int? memberLevelId, string? memberLevelName, string? priceTierKey, int? couponInstanceId) { return new MarketingPriceBreakdown { ReceivableSubtotal = receivableSubtotal, BaseFreightAmount = baseFreightAmount, LevelDiscountAmount = levelDiscount, AmountAfterLevel = Math.Max(0M, receivableSubtotal - levelDiscount), CouponDiscountAmount = couponDiscount, PayableAmount = payable, PaidBaseFreightAmount = MarketingFeeHelper.ResolvePaidBaseFreight( baseFreightAmount, levelDiscount, couponDiscount), MemberLevelId = memberLevelId, MemberLevelName = memberLevelName, PriceTierKey = priceTierKey, CouponInstanceId = couponInstanceId }; } } public static class MarketingCouponTypeCodes { public const string FixedOff = "fixed_off"; public const string PercentOff = "percent_off"; public const string DirectOff = "direct_off"; public const string ShippingOff = "shipping_off"; } public class CouponDiscountInput { public int CouponInstanceId { get; set; } public string CouponType { get; set; } = string.Empty; public decimal MinOrderAmount { get; set; } public decimal? FaceValue { get; set; } public decimal? DiscountRate { get; set; } public decimal? MaxDiscountAmount { get; set; } public string? ApplyScopeJson { get; set; } }