When rules require postal codes, still quote by same-country snapshot zones as estimated; keep blocking countries not configured at all. Co-authored-by: Cursor <cursoragent@cursor.com>
348 lines
11 KiB
C#
348 lines
11 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 验证查价卖价快照匹配:unit 公式、重量左开右闭区间。
|
||
/// </summary>
|
||
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<string, string>
|
||
{
|
||
["分区"] = zone,
|
||
["重量"] = weight
|
||
},
|
||
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 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<string, string>
|
||
{
|
||
["分区"] = zone,
|
||
["重量"] = weight,
|
||
["货物类型"] = cargoType,
|
||
["仓库"] = warehouseId
|
||
},
|
||
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 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();
|
||
}
|
||
}
|