fix: 收紧分区 range、进位相接与仅国家估算口径
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
进位区间改为左开右闭且禁止空洞,邮编 range 按数字/字母数字比较,查价仅国家时不再串用别国 zone。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c91d8ca0e6
commit
b748aa4000
@ -113,6 +113,7 @@ public class WeightRoundingService : ApplicationService
|
|||||||
public async Task AddInfoAsync(WeightRoundingInfoSaveInput input)
|
public async Task AddInfoAsync(WeightRoundingInfoSaveInput input)
|
||||||
{
|
{
|
||||||
await GetEntityOrThrowAsync(input.RoundingId);
|
await GetEntityOrThrowAsync(input.RoundingId);
|
||||||
|
await ValidateInfoRangeAsync(input);
|
||||||
await WeightRoundingRepository.InsertInfoAsync(new WeightRoundingInfo
|
await WeightRoundingRepository.InsertInfoAsync(new WeightRoundingInfo
|
||||||
{
|
{
|
||||||
RoundingId = input.RoundingId,
|
RoundingId = input.RoundingId,
|
||||||
@ -134,6 +135,7 @@ public class WeightRoundingService : ApplicationService
|
|||||||
{
|
{
|
||||||
var entity = await GetInfoOrThrowAsync(input.Id);
|
var entity = await GetInfoOrThrowAsync(input.Id);
|
||||||
await GetEntityOrThrowAsync(input.RoundingId);
|
await GetEntityOrThrowAsync(input.RoundingId);
|
||||||
|
await ValidateInfoRangeAsync(input, entity.Id);
|
||||||
entity.RoundingId = input.RoundingId;
|
entity.RoundingId = input.RoundingId;
|
||||||
entity.StartValue = input.StartValue;
|
entity.StartValue = input.StartValue;
|
||||||
entity.EndValue = input.EndValue;
|
entity.EndValue = input.EndValue;
|
||||||
@ -202,6 +204,16 @@ public class WeightRoundingService : ApplicationService
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateInfoRangeAsync(WeightRoundingInfoSaveInput input, int? excludeId = null)
|
||||||
|
{
|
||||||
|
var existed = await WeightRoundingRepository.GetAllInfosByRoundingIdAsync(input.RoundingId);
|
||||||
|
WeightRoundingRangeGuard.EnsureContiguous(
|
||||||
|
input.StartValue,
|
||||||
|
input.EndValue,
|
||||||
|
existed,
|
||||||
|
excludeId ?? input.Id);
|
||||||
|
}
|
||||||
|
|
||||||
private static WeightRoundingOutput MapOutput(WeightRounding entity)
|
private static WeightRoundingOutput MapOutput(WeightRounding entity)
|
||||||
{
|
{
|
||||||
return new WeightRoundingOutput
|
return new WeightRoundingOutput
|
||||||
|
|||||||
@ -66,8 +66,9 @@ internal static class ParcelBillingCalculator
|
|||||||
public static decimal ApplyRounding(decimal rawWeight, IReadOnlyList<WeightRoundingInfo> ranges)
|
public static decimal ApplyRounding(decimal rawWeight, IReadOnlyList<WeightRoundingInfo> ranges)
|
||||||
{
|
{
|
||||||
var hit = ranges
|
var hit = ranges
|
||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.StartValue)
|
||||||
.FirstOrDefault(x => rawWeight >= x.StartValue && rawWeight <= x.EndValue);
|
.ThenBy(x => x.Id)
|
||||||
|
.FirstOrDefault(x => WeightInLeftOpenRightClosedInterval(rawWeight, x.StartValue, x.EndValue));
|
||||||
if (hit == null)
|
if (hit == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("计重进位区间未匹配");
|
throw new InvalidOperationException("计重进位区间未匹配");
|
||||||
@ -129,8 +130,8 @@ internal static class ParcelBillingCalculator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查价仅填国家时:若快照已配置该目的国分区(即使规则带邮编区间),
|
/// 查价仅填国家时:只使用该国分区规则对应的 zone 键(及与这些键重合的卖价明细分区)。
|
||||||
/// 则返回卖价明细中的分区键供估算报价;未配置该国家时返回空(不跨国家兜底)。
|
/// 未配置该国家时返回空;不把别国明细分区拿来估算。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<string> CollectCountryEstimateZoneKeys(
|
public static IReadOnlyList<string> CollectCountryEstimateZoneKeys(
|
||||||
SalesProductPricePublishSnapshotDto? priceSnapshot,
|
SalesProductPricePublishSnapshotDto? priceSnapshot,
|
||||||
@ -143,15 +144,34 @@ internal static class ParcelBillingCalculator
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var countryConfigured = priceSnapshot.PriceAreaInfos.Any(rule =>
|
var countryRules = priceSnapshot.PriceAreaInfos
|
||||||
string.IsNullOrWhiteSpace(rule.CountryCode)
|
.Where(rule => string.Equals(rule.CountryCode, countryCode, StringComparison.OrdinalIgnoreCase))
|
||||||
|| string.Equals(rule.CountryCode, countryCode, StringComparison.OrdinalIgnoreCase));
|
.ToList();
|
||||||
if (!countryConfigured)
|
if (countryRules.Count == 0)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return CollectDistinctDetailZoneKeys(priceSnapshot);
|
var allowed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var rule in countryRules)
|
||||||
|
{
|
||||||
|
foreach (var key in BuildPriceZoneKeys(rule, priceSnapshot.PriceAreaName, priceSnapshot.PriceAreaId))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
allowed.Add(key.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var detailZones = CollectDistinctDetailZoneKeys(priceSnapshot);
|
||||||
|
var mapped = detailZones.Where(zone => allowed.Contains(zone)).ToList();
|
||||||
|
if (mapped.Count > 0)
|
||||||
|
{
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowed.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? ResolveZoneName(KpiAreaInfo? matched, KpiArea? area)
|
public static string? ResolveZoneName(KpiAreaInfo? matched, KpiArea? area)
|
||||||
@ -2170,7 +2190,7 @@ internal static class ParcelBillingCalculator
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return false;
|
throw new InvalidOperationException($"计重规则条件无效:{condition}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,11 @@ internal sealed class PriceCheckInputProfile
|
|||||||
missing.Add(PriceCheckMissingFieldCodes.PostalCode);
|
missing.Add(PriceCheckMissingFieldCodes.PostalCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasPostal && !hasCity)
|
||||||
|
{
|
||||||
|
missing.Add(PriceCheckMissingFieldCodes.City);
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasProvince)
|
if (!hasProvince)
|
||||||
{
|
{
|
||||||
missing.Add(PriceCheckMissingFieldCodes.Province);
|
missing.Add(PriceCheckMissingFieldCodes.Province);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
@ -65,6 +66,79 @@ public static class GeoPartitionMatcher
|
|||||||
return chars.Length == 0 ? string.Empty : new string(chars).ToUpperInvariant();
|
return chars.Length == 0 ? string.Empty : new string(chars).ToUpperInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规范化后的邮编比较:纯数字按等长补零;相同字母前缀+数字后缀按数值;其余按 Ordinal。
|
||||||
|
/// </summary>
|
||||||
|
public static int CompareNormalizedZip(string? left, string? right)
|
||||||
|
{
|
||||||
|
var a = left ?? string.Empty;
|
||||||
|
var b = right ?? string.Empty;
|
||||||
|
if (a.Length == 0 && b.Length == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsDigits(a) && IsDigits(b))
|
||||||
|
{
|
||||||
|
var width = Math.Max(a.Length, b.Length);
|
||||||
|
return string.CompareOrdinal(a.PadLeft(width, '0'), b.PadLeft(width, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TrySplitLetterPrefixNumber(a, out var prefixA, out var numberA, out var restA)
|
||||||
|
&& TrySplitLetterPrefixNumber(b, out var prefixB, out var numberB, out var restB)
|
||||||
|
&& string.Equals(prefixA, prefixB, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var numberCmp = numberA.CompareTo(numberB);
|
||||||
|
return numberCmp != 0 ? numberCmp : string.CompareOrdinal(restA, restB);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.CompareOrdinal(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDigits(string value) =>
|
||||||
|
value.Length > 0 && value.All(char.IsAsciiDigit);
|
||||||
|
|
||||||
|
private static bool TrySplitLetterPrefixNumber(
|
||||||
|
string zip,
|
||||||
|
out string prefix,
|
||||||
|
out int number,
|
||||||
|
out string rest)
|
||||||
|
{
|
||||||
|
prefix = string.Empty;
|
||||||
|
number = 0;
|
||||||
|
rest = string.Empty;
|
||||||
|
if (zip.Length == 0 || !char.IsAsciiLetter(zip[0]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < zip.Length && char.IsAsciiLetter(zip[i]))
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= zip.Length || !char.IsAsciiDigit(zip[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix = zip[..i];
|
||||||
|
var digitStart = i;
|
||||||
|
while (i < zip.Length && char.IsAsciiDigit(zip[i]))
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(zip[digitStart..i], NumberStyles.None, CultureInfo.InvariantCulture, out number))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rest = zip[i..];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static string NormalizeRegion(string? value) =>
|
public static string NormalizeRegion(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
|
string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
@ -116,8 +190,8 @@ public static class GeoPartitionMatcher
|
|||||||
}
|
}
|
||||||
else if (rule.MatchMode == ModeRange)
|
else if (rule.MatchMode == ModeRange)
|
||||||
{
|
{
|
||||||
hit = zipStd.CompareTo(rule.ZipStartStd) >= 0
|
hit = CompareNormalizedZip(zipStd, rule.ZipStartStd) >= 0
|
||||||
&& zipStd.CompareTo(rule.ZipEndStd) <= 0;
|
&& CompareNormalizedZip(zipStd, rule.ZipEndStd) <= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit)
|
if (hit)
|
||||||
@ -565,7 +639,7 @@ public static class GeoPartitionMatcher
|
|||||||
throw new InvalidOperationException("邮编区间必须填写截止邮编");
|
throw new InvalidOperationException("邮编区间必须填写截止邮编");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zipLeft.CompareTo(zipRight) > 0)
|
if (CompareNormalizedZip(zipLeft, zipRight) > 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("起始邮编不能大于截止邮编");
|
throw new InvalidOperationException("起始邮编不能大于截止邮编");
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/Oms.Application/Shared/WeightRoundingRangeGuard.cs
Normal file
75
src/Oms.Application/Shared/WeightRoundingRangeGuard.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计重进位区间校验:同一进位制下左开右闭 (start, end],端点相接不算重叠,排序后不允许空洞。
|
||||||
|
/// </summary>
|
||||||
|
public static class WeightRoundingRangeGuard
|
||||||
|
{
|
||||||
|
public static void EnsureValidRange(decimal startValue, decimal endValue)
|
||||||
|
{
|
||||||
|
if (startValue >= endValue)
|
||||||
|
{
|
||||||
|
throw new BusinessException("起始值须小于截止值");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// (a, b] 与 (c, d] 是否重叠。
|
||||||
|
/// </summary>
|
||||||
|
public static bool Overlaps(decimal startA, decimal endA, decimal startB, decimal endB) =>
|
||||||
|
startA < endB && startB < endA;
|
||||||
|
|
||||||
|
public static void EnsureNoOverlap(
|
||||||
|
decimal startValue,
|
||||||
|
decimal endValue,
|
||||||
|
IEnumerable<WeightRoundingInfo> existing,
|
||||||
|
int? excludeId = null)
|
||||||
|
{
|
||||||
|
EnsureValidRange(startValue, endValue);
|
||||||
|
foreach (var item in existing)
|
||||||
|
{
|
||||||
|
if (excludeId is > 0 && item.Id == excludeId.Value)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Overlaps(startValue, endValue, item.StartValue, item.EndValue))
|
||||||
|
{
|
||||||
|
throw new BusinessException(
|
||||||
|
$"起始区间 ({startValue}, {endValue}] 与已有区间 ({item.StartValue}, {item.EndValue}] 重叠");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnsureContiguous(
|
||||||
|
decimal startValue,
|
||||||
|
decimal endValue,
|
||||||
|
IEnumerable<WeightRoundingInfo> existing,
|
||||||
|
int? excludeId = null)
|
||||||
|
{
|
||||||
|
EnsureNoOverlap(startValue, endValue, existing, excludeId);
|
||||||
|
var merged = existing
|
||||||
|
.Where(item => excludeId is not > 0 || item.Id != excludeId.Value)
|
||||||
|
.Select(item => (Start: item.StartValue, End: item.EndValue))
|
||||||
|
.Append((Start: startValue, End: endValue))
|
||||||
|
.OrderBy(x => x.Start)
|
||||||
|
.ThenBy(x => x.End)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
for (var i = 0; i < merged.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var currentEnd = merged[i].End;
|
||||||
|
var nextStart = merged[i + 1].Start;
|
||||||
|
if (currentEnd == nextStart)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException(
|
||||||
|
$"起始区间必须首尾相接(左开右闭):({merged[i].Start}, {currentEnd}] 与 ({nextStart}, {merged[i + 1].End}] 之间存在缺口");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -59,6 +59,38 @@ public class GeoPartitionMatcherTests
|
|||||||
GeoPartitionMatcher.IsHit(rules, "US", null, null, "90200").Should().BeFalse();
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "90200").Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZipRange_NumericDifferentLength_ShouldCompareAsNumbers()
|
||||||
|
{
|
||||||
|
var rules = CompileEntities(Range("US", "9", "10"));
|
||||||
|
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "9").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "10").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "8").Should().BeFalse();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "11").Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZipRange_LeadingZeros_ShouldKeepZipWidth()
|
||||||
|
{
|
||||||
|
var rules = CompileEntities(Range("US", "02100", "02200"));
|
||||||
|
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "02115").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "02100").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "02200").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "US", null, null, "02099").Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZipRange_AlphaNumericPrefix_ShouldCompareNumericTail()
|
||||||
|
{
|
||||||
|
var rules = CompileEntities(Range("GB", "YO0", "YO9"));
|
||||||
|
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "GB", null, null, "YO0").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "GB", null, null, "YO9").Should().BeTrue();
|
||||||
|
GeoPartitionMatcher.IsHit(rules, "GB", null, null, "YO10").Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CountryFallback_ShouldHitWhenZipMisses()
|
public void CountryFallback_ShouldHitWhenZipMisses()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -48,6 +48,29 @@ public class ParcelBillingCalculatorTests
|
|||||||
rounded.Should().Be(1.5M);
|
rounded.Should().Be(1.5M);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyRounding_LeftOpenRightClosed_BoundaryGoesToLeftRange()
|
||||||
|
{
|
||||||
|
var ranges = new[]
|
||||||
|
{
|
||||||
|
ParcelBillingTestData.RoundingRange(0, 10, "0.5"),
|
||||||
|
ParcelBillingTestData.RoundingRange(10, 30, "1"),
|
||||||
|
};
|
||||||
|
|
||||||
|
ParcelBillingCalculator.ApplyRounding(10M, ranges).Should().Be(10M);
|
||||||
|
ParcelBillingCalculator.ApplyRounding(10.01M, ranges).Should().Be(11M);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyRounding_LeftOpenRightClosed_StartIsExclusive()
|
||||||
|
{
|
||||||
|
var ranges = new[] { ParcelBillingTestData.RoundingRange(0, 30, "0.5") };
|
||||||
|
|
||||||
|
var act = () => ParcelBillingCalculator.ApplyRounding(0M, ranges);
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>().WithMessage("*计重进位区间未匹配*");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 应收(卖价)
|
#region 应收(卖价)
|
||||||
|
|||||||
@ -308,23 +308,38 @@ public class PriceCheckSnapshotVerificationTests
|
|||||||
public void CountryEstimate_WhenCountryConfiguredWithPostcodeRules_ShouldReturnDetailZones()
|
public void CountryEstimate_WhenCountryConfiguredWithPostcodeRules_ShouldReturnDetailZones()
|
||||||
{
|
{
|
||||||
var snapshot = BuildUserSnapshot();
|
var snapshot = BuildUserSnapshot();
|
||||||
|
snapshot.Prices.Add(PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
|
||||||
|
{
|
||||||
|
Id = 9,
|
||||||
|
Status = 2,
|
||||||
|
CostItem = "freight",
|
||||||
|
Details = [Detail(90, 9, "total", "99", "0,2", 1)]
|
||||||
|
}));
|
||||||
snapshot.PriceAreaInfos =
|
snapshot.PriceAreaInfos =
|
||||||
[
|
[
|
||||||
new KpiAreaSnapshotRuleDto
|
new KpiAreaSnapshotRuleDto
|
||||||
{
|
{
|
||||||
Id = 11,
|
Id = 3,
|
||||||
CountryCode = "US",
|
CountryCode = "US",
|
||||||
PostcodeRange = "(90000,99999]"
|
PostcodeRange = "(90000,99999]"
|
||||||
|
},
|
||||||
|
new KpiAreaSnapshotRuleDto
|
||||||
|
{
|
||||||
|
Id = 99,
|
||||||
|
CountryCode = "DE",
|
||||||
|
PostcodeRange = "(00000,99999]"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 精确匹配(无邮编)应失败
|
|
||||||
ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(snapshot, "US", null, null, null)
|
ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(snapshot, "US", null, null, null)
|
||||||
.Should().BeNull();
|
.Should().BeNull();
|
||||||
|
|
||||||
// 国家估算应返回卖价明细分区键
|
|
||||||
ParcelBillingCalculator.CollectCountryEstimateZoneKeys(snapshot, "US")
|
ParcelBillingCalculator.CollectCountryEstimateZoneKeys(snapshot, "US")
|
||||||
.Should().Contain(["3"]);
|
.Should().Contain("3")
|
||||||
|
.And.NotContain("99");
|
||||||
|
ParcelBillingCalculator.CollectCountryEstimateZoneKeys(snapshot, "DE")
|
||||||
|
.Should().Contain("99")
|
||||||
|
.And.NotContain("3");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
97
src/Oms.Tests/WeightRoundingRangeGuardTests.cs
Normal file
97
src/Oms.Tests/WeightRoundingRangeGuardTests.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Tests;
|
||||||
|
|
||||||
|
public class WeightRoundingRangeGuardTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AdjacentLeftOpenRightClosed_ShouldNotOverlap()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(1, 0, 30) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureNoOverlap(30, 60, existing);
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContainedRange_ShouldOverlap()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(1, 0, 30) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureNoOverlap(5, 10, existing);
|
||||||
|
|
||||||
|
act.Should().Throw<BusinessException>().WithMessage("*重叠*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CrossingRange_ShouldOverlap()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(1, 0, 30) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureNoOverlap(20, 40, existing);
|
||||||
|
|
||||||
|
act.Should().Throw<BusinessException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateSameRow_ShouldIgnoreSelf()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(8, 0, 30), Range(9, 30, 60) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureNoOverlap(0, 30, existing, excludeId: 8);
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StartNotLessThanEnd_ShouldFail()
|
||||||
|
{
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureNoOverlap(30, 30, []);
|
||||||
|
|
||||||
|
act.Should().Throw<BusinessException>().WithMessage("*起始值须小于截止值*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HoleBetweenRanges_ShouldFail()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(1, 0, 10) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureContiguous(20, 30, existing);
|
||||||
|
|
||||||
|
act.Should().Throw<BusinessException>().WithMessage("*缺口*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdjacentRanges_ShouldBeContiguous()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(1, 0, 10) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureContiguous(10, 25, existing);
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateSameRow_Contiguous_ShouldIgnoreSelf()
|
||||||
|
{
|
||||||
|
var existing = new[] { Range(8, 0, 30), Range(9, 30, 60) };
|
||||||
|
|
||||||
|
var act = () => WeightRoundingRangeGuard.EnsureContiguous(0, 30, existing, excludeId: 8);
|
||||||
|
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WeightRoundingInfo Range(int id, decimal start, decimal end) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
RoundingId = 1,
|
||||||
|
StartValue = start,
|
||||||
|
EndValue = end,
|
||||||
|
Public = "0.5"
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user