using FluentAssertions; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; using Oms.Application.Shared; using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.OrdersAggregate.Consts; namespace Oms.Tests; /// /// 验证查价卖价快照匹配:unit 公式、重量左开右闭区间。 /// public class PriceCheckSnapshotVerificationTests { private static SalesProductPricePublishSnapshotDto BuildUserSnapshot() => new() { SalesProductId = 1, Prices = [ PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 1, Status = 2, CostItem = "freight", Details = [ Detail(24, 1, "total", "3", "0,0.5", 11), Detail(25, 1, "total", "3", "0.5,1", 12), Detail(26, 1, "total", "3", "1,1.5", 13), Detail(27, 1, "total", "3", "1.5,2", 14), ] }), PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 2, Status = 2, CostItem = "handling", Details = [Detail(28, 2, "total", "3", "0,10", 20)] }) ] }; private static ChannelPriceDetailOutput Detail( int id, int priceId, string formula, string zone, string weight, decimal normalPrice) => new() { Id = id, PriceId = priceId, CalculationFormula = formula, CalculatedValue = 0, FactorValuesJson = new Dictionary { ["分区"] = zone, ["重量"] = weight }, CustomerGroupValues = new Dictionary { ["普通客户"] = normalPrice } }; private static ChannelPriceDetailOutput DetailWithCargoAndWarehouse( int id, int priceId, string formula, string zone, string weight, string cargoType, string warehouseId, decimal normalPrice) => new() { Id = id, PriceId = priceId, CalculationFormula = formula, CalculatedValue = 0, FactorValuesJson = new Dictionary { ["分区"] = zone, ["重量"] = weight, ["货物类型"] = cargoType, ["仓库"] = warehouseId }, CustomerGroupValues = new Dictionary { ["普通客户"] = normalPrice } }; [Fact] public void EnglishCargoTypeFactor_ShouldMatchCargoAttributeForFreight() { var snapshot = new SalesProductPricePublishSnapshotDto { SalesProductId = 1, Prices = [ PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 1, Status = 2, CostItem = "freight", Details = [ DetailWithCargoAndWarehouse(61, 1, "total", "3", "(0-0.5]", "general", "2", 10), DetailWithCargoAndWarehouse(62, 1, "total", "3", "(0-0.5]", "electronics", "2", 11), ] }), PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 2, Status = 2, CostItem = "handling", Details = [Detail(44, 2, "total", "3", "(0-10]", 23)] }) ] }; var generalFees = ParcelBillingCalculator.CalculateQuoteReceivableFees( snapshot, "PRICE-TEST", DateTime.Now, 0.214M, ["3"], CargoAttributeCodes.General, "普通客户", warehouseId: 2); generalFees.Should().HaveCount(2); generalFees.Single(x => x.CostItem == "freight").Amount.Should().Be(10M); generalFees.Single(x => x.CostItem == "handling").Amount.Should().Be(23M); var summary = ParcelBillingCalculator.SummarizeQuoteFees( ParcelBillingCalculator.CalculateQuoteReceivableFeeLines( snapshot, "PRICE-TEST", DateTime.Now, 0.214M, ["3"], CargoAttributeCodes.General, "普通客户", warehouseId: 2)); summary.CostFormula.Should().Be("10+23"); summary.Freight.Should().Be(10M); summary.OtherFees.Should().Be(23M); summary.OtherFeeList.Should().ContainSingle(x => x.Name == "操作费" && x.Amount == 23M); var electronicsFees = ParcelBillingCalculator.CalculateQuoteReceivableFees( snapshot, "PRICE-TEST", DateTime.Now, 0.214M, ["3"], CargoAttributeCodes.Electronics, "普通客户", warehouseId: 2); electronicsFees.Single(x => x.CostItem == "freight").Amount.Should().Be(11M); } [Fact] public void TryParseWeightInterval_SingleValue_ShouldBeLeftOpenRightClosedFromZero() { ParcelBillingCalculator.TryParseWeightInterval("0.5", out var start, out var end).Should().BeTrue(); start.Should().Be(0); end.Should().Be(0.5M); ParcelBillingCalculator.WeightInLeftOpenRightClosedInterval(0.5M, start, end).Should().BeTrue(); ParcelBillingCalculator.WeightInLeftOpenRightClosedInterval(0M, start, end).Should().BeFalse(); } [Fact] public void TryParseWeightInterval_IntervalNotation_ShouldParseLeftOpenRightClosed() { ParcelBillingCalculator.TryParseWeightInterval("(0-0.5]", out var start, out var end).Should().BeTrue(); start.Should().Be(0); end.Should().Be(0.5M); ParcelBillingCalculator.WeightInLeftOpenRightClosedInterval(0.5M, start, end).Should().BeTrue(); } [Fact] public void UserSnapshot_IntervalWeightFormat_Zone3_Weight05_ShouldMatchFreight11AndHandling20() { var snapshot = new SalesProductPricePublishSnapshotDto { SalesProductId = 1, Prices = [ PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 1, Status = 2, CostItem = "freight", Details = [ Detail(34, 1, "total", "3", "(0-0.5]", 11), Detail(35, 1, "total", "3", "(0.5-1]", 12), ] }), PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 2, Status = 2, CostItem = "handling", Details = [Detail(38, 2, "total", "3", "(0-10]", 20)] }) ] }; var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees( snapshot, "PRICE-20260626102033", DateTime.Now, 0.5M, ["3"], CargoAttributeCodes.General, "普通客户"); fees.Should().HaveCount(2); fees.Sum(x => x.Amount).Should().Be(31M); } [Fact] public void UserSnapshot_Zone3_Weight05_ShouldMatchFreight11AndHandling20() { var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees( BuildUserSnapshot(), "PRICE-20260601-001", DateTime.Now, 0.5M, ["3"], CargoAttributeCodes.General, "普通客户"); fees.Should().HaveCount(2); fees.Single(x => x.CostItem == "freight").Amount.Should().Be(11M); fees.Single(x => x.CostItem == "handling").Amount.Should().Be(20M); fees.Sum(x => x.Amount).Should().Be(31M); } [Fact] public void TryResolveZoneKeysFromPriceSnapshot_UsZip06000_ShouldMatchZone3() { var snapshot = new SalesProductPricePublishSnapshotDto { PriceAreaId = 3, PriceAreaName = "美国分区", PriceAreaInfos = [ new KpiAreaSnapshotRuleDto { Id = 4, CountryCode = "US", PostcodeRange = "(50000,90000]" }, new KpiAreaSnapshotRuleDto { Id = 3, CountryCode = "US", PostcodeRange = "(00000,50000]" } ] }; var keys = ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot( snapshot, "US", null, null, "06000"); keys.Should().NotBeNull(); keys!.Should().Contain("3"); } [Fact] public void ExplainQuotePriceMiss_ZoneMismatch_ShouldNameEachPriceItem() { var reason = ParcelBillingCalculator.ExplainQuotePriceMiss( BuildUserSnapshot(), 0.5M, ["默认分区"], CargoAttributeCodes.General, "普通客户"); reason.Should().Contain("分区不匹配"); reason.Should().Contain("明细分区=[3]"); reason.Should().Contain("解析分区键=[默认分区]"); reason.Should().Contain("freight"); reason.Should().Contain("handling"); } [Fact] public void UserSnapshot_WrongZone_ShouldReturnNoFees() { var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees( BuildUserSnapshot(), "PRICE-20260601-001", DateTime.Now, 0.5M, ["99"], CargoAttributeCodes.General, "普通客户"); fees.Should().BeEmpty(); } [Fact] public void CountryEstimate_WhenCountryConfiguredWithPostcodeRules_ShouldReturnDetailZones() { var snapshot = BuildUserSnapshot(); snapshot.PriceAreaInfos = [ new KpiAreaSnapshotRuleDto { Id = 11, CountryCode = "US", PostcodeRange = "(90000,99999]" } ]; // 精确匹配(无邮编)应失败 ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(snapshot, "US", null, null, null) .Should().BeNull(); // 国家估算应返回卖价明细分区键 ParcelBillingCalculator.CollectCountryEstimateZoneKeys(snapshot, "US") .Should().Contain(["3"]); } [Fact] public void CountryEstimate_WhenCountryNotConfigured_ShouldReturnEmpty() { var snapshot = BuildUserSnapshot(); snapshot.PriceAreaInfos = [ new KpiAreaSnapshotRuleDto { Id = 11, CountryCode = "US", PostcodeRange = "(90000,99999]" } ]; ParcelBillingCalculator.CollectCountryEstimateZoneKeys(snapshot, "DE") .Should().BeEmpty(); } }