diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index 5ca83a0..b64264e 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -88,6 +88,7 @@ public partial class OrderService var zoneKeys = await ResolvePriceZoneKeysAsync( product, + snapshotBinding.Rules, country.CountryCode, input.Receiver?.Province, input.Receiver?.City, @@ -408,31 +409,55 @@ public partial class OrderService zipCode)); } + private static int? ResolveEffectivePriceAreaId( + SalesProduct product, + SalesProductRulesPublishSnapshotDto? rulesSnapshot) + { + if (product.PriceAreaId is > 0) + { + return product.PriceAreaId; + } + + if (rulesSnapshot?.Product?.PriceAreaId is > 0) + { + return rulesSnapshot.Product.PriceAreaId; + } + + return null; + } + private async Task?> ResolvePriceZoneKeysAsync( SalesProduct product, + SalesProductRulesPublishSnapshotDto? rulesSnapshot, string countryCode, string? province, string? city, string? zipCode) { - if (!product.PriceAreaId.HasValue || product.PriceAreaId <= 0) + var priceAreaId = ResolveEffectivePriceAreaId(product, rulesSnapshot); + if (!priceAreaId.HasValue) { - return new[] { "默认分区" }; + return null; } - var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == product.PriceAreaId.Value); + var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value); if (area == null) { return null; } - var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.PriceAreaId.Value); + var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value); var matched = rules.FirstOrDefault(rule => ParcelBillingCalculator.IsAreaMatched( rule, countryCode, province, city, zipCode)); + if (matched == null) + { + return null; + } + var keys = ParcelBillingCalculator.BuildPriceZoneKeys(matched, area); return keys.Count > 0 ? keys : null; } diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs index 53c5255..918e17b 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs @@ -217,8 +217,18 @@ public partial class OrderService return PriceCheckFailure(product, "weight", ex.Message); } + var priceAreaId = ResolveEffectivePriceAreaId(product, snapshotBinding.Rules); + if (!priceAreaId.HasValue) + { + return PriceCheckFailure( + product, + "zone", + "产品未配置价格分区(PriceAreaId),无法根据国家+邮编匹配卖价明细中的分区键"); + } + var zoneKeys = await ResolvePriceZoneKeysAsync( product, + snapshotBinding.Rules, context.CountryCode, context.Province, context.City, @@ -228,7 +238,8 @@ public partial class OrderService return PriceCheckFailure( product, "zone", - $"目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 未匹配产品价格分区(请检查 PriceAreaId 及分区规则)"); + $"目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 未命中价格分区规则(PriceAreaId={priceAreaId})," + + $"卖价明细分区键(如 3) 无法解析,请检查 t_kpi_area_info 国家/邮编配置"); } var chargeableWeight = forecast.ChargeableWeight ?? 0; diff --git a/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs b/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs index 5409de9..357432e 100644 --- a/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs +++ b/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs @@ -73,6 +73,57 @@ public class PriceCheckSnapshotVerificationTests 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 = + [ + new SalesProductPriceListOutput + { + Id = 1, + Status = 1, + CostItem = "freight", + Details = + [ + Detail(34, 1, "unit", "3", "(0-0.5]", 11), + Detail(35, 1, "unit", "3", "(0.5-1]", 12), + ] + }, + new SalesProductPriceListOutput + { + Id = 2, + Status = 1, + 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, + CustomerGroup.Normal); + + fees.Should().HaveCount(2); + fees.Sum(x => x.Amount).Should().Be(31M); + } + [Fact] public void UserSnapshot_Zone3_Weight05_ShouldMatchFreight11AndHandling20() {