All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.9 KiB
C#
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; }
|
|
}
|