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
}