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();
}
}