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 { 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 { 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 { 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"); } }