admin/src/Oms.Tests/ParcelBillingCalculatorTests.cs
zzh 5e26de6ff3 feat: remove ChargingTime and bill receivables by weight stage
Drop sell/buy price charging-time config; write receivable fees after forecast or receive chargeable weight, hook inbound billing, fix order list status filter, and add DB migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 16:24:53 +08:00

357 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 小包预报应收/应付计费单元测试(与设计文档 §4.4.3.1 续单价示例对齐)。
/// </summary>
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<string, decimal> { ["客户组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<string, string>
{
["分区"] = "3",
["重量"] = "(0.5-1]"
},
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 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<string, decimal> { ["普通客户"] = 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
}