using FluentAssertions; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Tests.Support; namespace Oms.Tests; /// /// 小包预报应收/应付计费单元测试(与设计文档 §4.4.3.1 续单价示例对齐)。 /// public class ParcelBillingCalculatorTests { private const string Zone = "美西"; private readonly (string PriceVersion, DateTime EffectiveTime) _meta = ParcelBillingTestData.SnapshotMeta(); #region 计重 [Fact] public void CalculateRawBillableWeight_ShouldUseGrossWeightWhenConditionHits() { var forecast = new OrderWeight { Length = 30, Width = 20, Height = 10, ActualWeight = 1.5M }; var rules = new[] { ParcelBillingTestData.GwRule() }; var (vw, raw) = ParcelBillingCalculator.CalculateRawBillableWeight(forecast, rules); vw.Should().Be(1M); raw.Should().Be(1.5M); } [Fact] public void ApplyRounding_ShouldCeilToConfiguredUnit() { var ranges = new[] { ParcelBillingTestData.RoundingRange(0, 30, "0.5") }; var rounded = ParcelBillingCalculator.ApplyRounding(1.23M, ranges); rounded.Should().Be(1.5M); } #endregion #region 应收(卖价) [Theory] [InlineData(0.8, 18)] [InlineData(1.2, 24)] [InlineData(1.6, 30)] public void CalculateForecastReceivableFees_ContinuePrice_ShouldMatchDesignExamples( double chargeableWeight, decimal expectedFreight) { var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem( 1, "freight", ParcelBillingTestData.ContinueFreightDetail())); var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, (decimal)chargeableWeight, Zone, "general", null); fees.Should().ContainSingle(); fees[0].Type.Should().Be(OrderFeeTypeCodes.Receivable); fees[0].CostItem.Should().Be("freight"); fees[0].Amount.Should().Be(expectedFreight); fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Forecast); fees[0].PriceNo.Should().Be(_meta.PriceVersion); } [Fact] public void CalculateReceivableFees_ShouldIncludeAllEnabledPriceItems() { var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem( 1, "freight", ParcelBillingTestData.ContinueFreightDetail())); var fees = ParcelBillingCalculator.CalculateReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, new[] { Zone }, "general", null, OrderFeeChargingStageCodes.Forecast); fees.Should().ContainSingle(); fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Forecast); } [Fact] public void CalculateReceivableFees_InboundStage_ShouldSetChargingStage() { var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem( 1, "freight", ParcelBillingTestData.ContinueFreightDetail())); var fees = ParcelBillingCalculator.CalculateReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, new[] { Zone }, "general", null, OrderFeeChargingStageCodes.Inbound); fees.Should().ContainSingle(); fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Inbound); } [Fact] public void CalculateForecastReceivableFees_ShouldIncludeForecastAndFixedPriceItems() { var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem( 1, "freight", ParcelBillingTestData.ContinueFreightDetail()), ParcelBillingTestData.ReceivablePriceItem( 1, "操作费", ParcelBillingTestData.FixedPriceDetail(5M))); var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 0.8M, Zone, "general", null); fees.Should().HaveCount(2); fees.Should().Contain(x => x.CostItem == "freight" && x.Amount == 18M); fees.Should().Contain(x => x.CostItem == "操作费" && x.Amount == 5M); var summary = ParcelBillingCalculator.SummarizeFees(fees); summary.Freight.Should().Be(18M); summary.OtherFees.Should().Be(5M); summary.TotalCost.Should().Be(23M); } [Fact] public void CalculateForecastReceivableFees_UnitFormula_ShouldMultiplyChargeableWeight() { var detail = ParcelBillingTestData.UnitPriceDetail(15M); detail.CustomerGroupValues = new Dictionary { ["客户组1"] = 15M }; var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail)); var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 2M, Zone, "general", CustomerGroup.Vip1); fees.Should().ContainSingle(); fees[0].Amount.Should().Be(30M); } [Fact] public void CalculateQuoteReceivableFees_TieredContinueWithTotalBase_Weight08_ShouldBe106() { var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem( 1, "freight", ParcelBillingTestData.TotalTierDetail("3", 0.5M, 10M, weightInterval: "(0-0.5]"), new ChannelPriceDetailOutput { CalculationFormula = "continue", CalculatedValue = 0, FactorValuesJson = new Dictionary { ["分区"] = "3", ["重量"] = "(0.5-1]" }, CustomerGroupValues = new Dictionary { ["普通客户"] = 2M } })); var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 0.8M, new[] { "3" }, "general", CustomerGroup.Normal); fees.Should().ContainSingle(); fees[0].Amount.Should().Be(10.6M); } [Fact] public void CalculateQuoteReceivableFees_ShouldSupportTotalPriceTiers() { var details = new[] { ParcelBillingTestData.TotalTierDetail("3", 0.5M, 11M), ParcelBillingTestData.TotalTierDetail("3", 1M, 12M), ParcelBillingTestData.TotalTierDetail("3", 1.5M, 12M), ParcelBillingTestData.TotalTierDetail("3", 2M, 14M), }; var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem(1, "freight", details)); var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1M, new[] { "3" }, "general", null); fees.Should().ContainSingle(); fees[0].Amount.Should().Be(12M); } [Fact] public void CalculateForecastReceivableFees_ShouldApplyNormalCustomerLabel() { var detail = ParcelBillingTestData.TotalTierDetail("3", 1M, 99M); detail.CustomerGroupValues = new Dictionary { ["普通客户"] = 12M }; var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail)); var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1M, new[] { "3" }, "general", CustomerGroup.Normal); fees.Should().ContainSingle(); fees[0].Amount.Should().Be(12M); } [Fact] public void HasEnabledReceivablePriceItems_ShouldDetectAnyEnabledItems() { var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem(1, "freight", ParcelBillingTestData.ContinueFreightDetail()), ParcelBillingTestData.ReceivablePriceItem(0, "freight", ParcelBillingTestData.ContinueFreightDetail())); ParcelBillingCalculator.HasEnabledReceivablePriceItems(snapshot).Should().BeTrue(); ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshot).Should().BeTrue(); } #endregion #region 应付(买价) [Theory] [InlineData(0.8, 12)] [InlineData(1.2, 16)] [InlineData(1.6, 20)] public void CalculateForecastPayableFees_ContinuePrice_ShouldUseSameEngine( double chargeableWeight, decimal expectedFreight) { var prices = ParcelBillingTestData.BuildPayablePrices( ParcelBillingTestData.PayablePriceItem( 1, "freight", ParcelBillingTestData.ContinueFreightDetail( firstWeight: 1M, firstPrice: 12M, continueUnit: 0.5M, continuePrice: 4M))); var fees = ParcelBillingCalculator.CalculateForecastPayableFees( prices, _meta.PriceVersion, _meta.EffectiveTime, (decimal)chargeableWeight, Zone, "general", null); fees.Should().ContainSingle(); fees[0].Type.Should().Be(OrderFeeTypeCodes.Payable); fees[0].Amount.Should().Be(expectedFreight); fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Forecast); } [Fact] public void CalculateForecastPayableFees_ShouldSkipDisabledPriceItems() { var prices = ParcelBillingTestData.BuildPayablePrices( ParcelBillingTestData.PayablePriceItem( 0, "freight", ParcelBillingTestData.ContinueFreightDetail())); var fees = ParcelBillingCalculator.CalculateForecastPayableFees( prices, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null); fees.Should().BeEmpty(); ParcelBillingCalculator.HasEnabledPayablePriceItems(prices).Should().BeFalse(); } [Fact] public void ReceivableAndPayable_ShouldDifferByTypeButShareZoneMatching() { var detail = ParcelBillingTestData.ContinueFreightDetail(); var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail)); var payablePrices = ParcelBillingTestData.BuildPayablePrices( ParcelBillingTestData.PayablePriceItem(1, "freight", detail)); var receivable = ParcelBillingCalculator.CalculateForecastReceivableFees( snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null); var payable = ParcelBillingCalculator.CalculateForecastPayableFees( payablePrices, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null); receivable[0].Type.Should().Be(OrderFeeTypeCodes.Receivable); payable[0].Type.Should().Be(OrderFeeTypeCodes.Payable); receivable[0].Amount.Should().Be(payable[0].Amount); } #endregion }