admin/src/Oms.Tests/MarketingMemberPointsTests.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

201 lines
6.3 KiB
C#

using FluentAssertions;
using Oms.Application.Dtos.Marketing;
using Oms.Application.Shared;
using Oms.Domain.MarketingAggregate.Entities;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Tests;
public class MarketingMemberPointsTests
{
[Fact]
public void MemberDiscount_ShouldApplyOnlyToBaseFreight()
{
var result = MarketingBillingCalculator.Calculate(
receivableSubtotal: 1200M,
baseFreightAmount: 1000M,
extraDiscountRate: 0.05M,
memberLevelId: 5,
memberLevelName: "皇冠会员",
priceTierKey: null,
coupon: null);
result.LevelDiscountAmount.Should().Be(50M);
result.PayableAmount.Should().Be(1150M);
result.PaidBaseFreightAmount.Should().Be(950M);
result.CouponInstanceId.Should().BeNull();
}
[Fact]
public void CouponAndMember_ShouldPickCheaperWithoutStacking()
{
var coupon = new CouponDiscountInput
{
CouponInstanceId = 9,
CouponType = MarketingCouponTypeCodes.FixedOff,
FaceValue = 80M,
MinOrderAmount = 0M
};
var result = MarketingBillingCalculator.Calculate(
1200M,
1000M,
0.05M,
5,
"皇冠会员",
null,
coupon);
result.LevelDiscountAmount.Should().Be(0M);
result.CouponDiscountAmount.Should().Be(80M);
result.PayableAmount.Should().Be(1120M);
result.CouponInstanceId.Should().Be(9);
result.PaidBaseFreightAmount.Should().Be(920M);
}
[Fact]
public void Coupon_ShouldCapAtBaseFreightAndIgnoreSurcharge()
{
var coupon = new CouponDiscountInput
{
CouponInstanceId = 1,
CouponType = MarketingCouponTypeCodes.FixedOff,
FaceValue = 200M,
MinOrderAmount = 0M
};
var result = MarketingBillingCalculator.Calculate(
150M, 100M, 0M, null, null, null, coupon);
result.CouponDiscountAmount.Should().Be(100M);
result.PayableAmount.Should().Be(50M);
result.PaidBaseFreightAmount.Should().Be(0M);
}
[Fact]
public void CouponThreshold_ShouldUseBaseFreightNotOrderTotal()
{
var coupon = new CouponDiscountInput
{
CouponInstanceId = 1,
CouponType = MarketingCouponTypeCodes.FixedOff,
FaceValue = 15M,
MinOrderAmount = 150M
};
var result = MarketingBillingCalculator.Calculate(
250M, 140M, 0M, null, null, null, coupon);
result.CouponDiscountAmount.Should().Be(0M);
result.CouponInstanceId.Should().BeNull();
result.PayableAmount.Should().Be(250M);
}
[Fact]
public void CouponScope_ShouldMatchCountryCodes()
{
MarketingCouponScope.Matches("""{"countryCodes":["MY","SG"]}""", "MY").Should().BeTrue();
MarketingCouponScope.Matches("""{"countryCodes":["MY","SG"]}""", "US").Should().BeFalse();
MarketingCouponScope.Matches(null, "MY").Should().BeTrue();
}
[Fact]
public void EqualOrWorseCoupon_ShouldKeepMemberDiscount()
{
var coupon = new CouponDiscountInput
{
CouponInstanceId = 9,
CouponType = MarketingCouponTypeCodes.FixedOff,
FaceValue = 50M
};
var result = MarketingBillingCalculator.Calculate(
1200M,
1000M,
0.05M,
5,
"皇冠会员",
null,
coupon);
result.LevelDiscountAmount.Should().Be(50M);
result.CouponDiscountAmount.Should().Be(0M);
result.CouponInstanceId.Should().BeNull();
result.PayableAmount.Should().Be(1150M);
}
[Fact]
public void DiscountRate_ShouldCapAtFivePercent()
{
var result = MarketingBillingCalculator.Calculate(
1000M,
1000M,
0.2M,
1,
"测试",
null,
null);
result.LevelDiscountAmount.Should().Be(50M);
}
[Fact]
public void FuelSurcharge_ShouldNotCountAsBaseFreight()
{
var fees = new[]
{
new OrderFee { Type = OrderFeeTypeCodes.Receivable, CostItem = "freight", Amount = 100 },
new OrderFee { Type = OrderFeeTypeCodes.Receivable, CostItem = "燃油附加费", Amount = 30 },
new OrderFee { Type = OrderFeeTypeCodes.Receivable, CostItem = "偏远附加费", Amount = 20 }
};
MarketingFeeHelper.SumReceivableBaseFreight(fees).Should().Be(100M);
MarketingFeeHelper.IsBaseFreightCostItem("燃油附加费").Should().BeFalse();
}
[Fact]
public void V5_ShouldStepDownOneLevelOnly()
{
var levels = new List<MemberLevel>
{
new() { Id = 1, LevelCode = "V0", MinPoints = 0, CanDowngrade = false },
new() { Id = 2, LevelCode = "V3", MinPoints = 120000, CanDowngrade = false },
new() { Id = 3, LevelCode = "V4", MinPoints = 240000, CanDowngrade = true },
new() { Id = 4, LevelCode = "V5", MinPoints = 600000, CanDowngrade = true }
};
var v5 = levels[3];
var v0Target = levels[0];
var next = MarketingPointsManager.ResolveNextLevel(levels, v5, v0Target, allowDowngrade: true);
next!.LevelCode.Should().Be("V4");
}
[Fact]
public void V3_ShouldNotDowngrade()
{
var levels = new List<MemberLevel>
{
new() { Id = 1, LevelCode = "V0", MinPoints = 0, CanDowngrade = false },
new() { Id = 2, LevelCode = "V3", MinPoints = 120000, CanDowngrade = false }
};
var next = MarketingPointsManager.ResolveNextLevel(levels, levels[1], levels[0], allowDowngrade: true);
next!.LevelCode.Should().Be("V3");
}
[Fact]
public void Upgrade_CanJumpToMatchingLevel()
{
var levels = new List<MemberLevel>
{
new() { Id = 1, LevelCode = "V0", MinPoints = 0, CanDowngrade = false },
new() { Id = 2, LevelCode = "V4", MinPoints = 240000, CanDowngrade = true },
new() { Id = 3, LevelCode = "V5", MinPoints = 600000, CanDowngrade = true }
};
var next = MarketingPointsManager.ResolveNextLevel(levels, levels[0], levels[2], allowDowngrade: false);
next!.LevelCode.Should().Be("V5");
}
}