admin/src/Oms.Application/Shared/MarketingBillingCalculator.cs
zzh f972a28d79
All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
feat: 落地滚动会员、券发放与邀请分成奖励
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 17:40:24 +08:00

190 lines
5.9 KiB
C#

using Oms.Application.Dtos.Marketing;
namespace Oms.Application.Shared;
/// <summary>
/// 会员折扣仅作用于基础运费;与优惠券不叠加,取更低应付(同价保留会员折,避免占券)。
/// </summary>
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);
}
/// <summary>
/// 仅按基础运费判断门槛并抵扣,封顶不超过基础运费(不找零、不冲附加费)。
/// </summary>
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; }
}