fix: 查价失败原因按价格项细化,指出分区/重量等具体不匹配项
新增 ExplainQuotePriceMiss 逐条说明运费/操作费等未命中原因;zone 失败时附带卖价明细分区。单元测试 6/6 通过。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
94a503afb3
commit
cd061aaaed
@ -220,10 +220,14 @@ public partial class OrderService
|
||||
var priceAreaId = ResolveEffectivePriceAreaId(product, snapshotBinding.Rules);
|
||||
if (!priceAreaId.HasValue)
|
||||
{
|
||||
var snapshotZones = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshotBinding.Price);
|
||||
var snapshotZoneHint = snapshotZones.Count > 0
|
||||
? $"卖价明细分区=[{string.Join(", ", snapshotZones)}],"
|
||||
: string.Empty;
|
||||
return PriceCheckFailure(
|
||||
product,
|
||||
"zone",
|
||||
"产品未配置价格分区(PriceAreaId),无法根据国家+邮编匹配卖价明细中的分区键");
|
||||
$"{snapshotZoneHint}产品未配置价格分区(PriceAreaId),无法根据国家+邮编匹配卖价明细中的分区键");
|
||||
}
|
||||
|
||||
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
||||
@ -235,11 +239,15 @@ public partial class OrderService
|
||||
context.PostalCode);
|
||||
if (zoneKeys == null || zoneKeys.Count == 0)
|
||||
{
|
||||
var snapshotZones = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshotBinding.Price);
|
||||
var snapshotZoneHint = snapshotZones.Count > 0
|
||||
? $"卖价明细分区=[{string.Join(", ", snapshotZones)}]。"
|
||||
: string.Empty;
|
||||
return PriceCheckFailure(
|
||||
product,
|
||||
"zone",
|
||||
$"目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 未命中价格分区规则(PriceAreaId={priceAreaId})," +
|
||||
$"卖价明细分区键(如 3) 无法解析,请检查 t_kpi_area_info 国家/邮编配置");
|
||||
$"{snapshotZoneHint}目的国 {context.CountryCode} 邮编 {FormatPostalCode(context.PostalCode)} 未命中价格分区规则(PriceAreaId={priceAreaId})," +
|
||||
"无法解析与卖价一致的分区键,请检查产品 PriceAreaId 及 t_kpi_area_info 国家/邮编配置");
|
||||
}
|
||||
|
||||
var chargeableWeight = forecast.ChargeableWeight ?? 0;
|
||||
@ -254,8 +262,7 @@ public partial class OrderService
|
||||
|
||||
if (fees.Count == 0)
|
||||
{
|
||||
var enabledItems = snapshotBinding.Price.Prices.Count(x => x.Status == 1);
|
||||
if (enabledItems == 0)
|
||||
if (!ParcelBillingCalculator.HasEnabledQuotePriceItems(snapshotBinding.Price))
|
||||
{
|
||||
return PriceCheckFailure(product, "price", "卖价快照中无启用的价格项");
|
||||
}
|
||||
@ -263,8 +270,12 @@ public partial class OrderService
|
||||
return PriceCheckFailure(
|
||||
product,
|
||||
"price",
|
||||
$"计费重 {chargeableWeight}kg,匹配分区键 [{string.Join(", ", zoneKeys)}],货物属性 {context.CargoAttribute}," +
|
||||
$"在卖价版本 {snapshotBinding.PriceVersion} 的 {enabledItems} 个启用价格项中未命中任何明细(请检查分区/重量区间/货物类型)");
|
||||
ParcelBillingCalculator.ExplainQuotePriceMiss(
|
||||
snapshotBinding.Price,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
context.CargoAttribute,
|
||||
customerGroup));
|
||||
}
|
||||
|
||||
var isRemote = await IsRemoteHitAsync(
|
||||
|
||||
@ -213,6 +213,136 @@ internal static class ParcelBillingCalculator
|
||||
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||
priceSnapshot.Prices.Any(x => x.Status == 1);
|
||||
|
||||
/// <summary>
|
||||
/// 收集卖价快照中所有启用价格项的明细分区键(去重)。
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(SalesProductPricePublishSnapshotDto priceSnapshot)
|
||||
{
|
||||
var zones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var price in priceSnapshot.Prices.Where(x => x.Status == 1))
|
||||
{
|
||||
foreach (var detail in price.Details ?? [])
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
if (TryGetZoneFactorText(factors, out var zone))
|
||||
{
|
||||
zones.Add(zone.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return zones.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查价未命中时,按价格项输出具体原因(分区/货物类型/重量区间等)。
|
||||
/// </summary>
|
||||
public static string ExplainQuotePriceMiss(
|
||||
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||
decimal chargeableWeight,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup)
|
||||
{
|
||||
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
|
||||
var enabledPrices = priceSnapshot.Prices.Where(x => x.Status == 1).ToList();
|
||||
if (enabledPrices.Count == 0)
|
||||
{
|
||||
return "卖价快照中无启用的价格项";
|
||||
}
|
||||
|
||||
var reasons = new List<string>();
|
||||
foreach (var price in enabledPrices)
|
||||
{
|
||||
var label = !string.IsNullOrWhiteSpace(price.PriceName) ? price.PriceName! : price.CostItem;
|
||||
var details = price.Details ?? [];
|
||||
if (details.Count == 0)
|
||||
{
|
||||
reasons.Add($"【{label}】价格项Id={price.Id}:无明细行");
|
||||
continue;
|
||||
}
|
||||
|
||||
var zoneMatchedDetails = details.Where(detail =>
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
return ZoneMatches(factors, zoneKeys);
|
||||
}).ToList();
|
||||
|
||||
if (zoneMatchedDetails.Count == 0)
|
||||
{
|
||||
var detailZones = details
|
||||
.Select(detail =>
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
return TryGetZoneFactorText(factors, out var zone) ? zone.Trim() : null;
|
||||
})
|
||||
.Where(zone => !string.IsNullOrWhiteSpace(zone))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
reasons.Add(
|
||||
$"【{label}】价格项Id={price.Id}:分区不匹配,明细分区=[{string.Join(", ", detailZones)}]," +
|
||||
$"解析分区键=[{string.Join(", ", zoneKeys)}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var cargoMatchedDetails = zoneMatchedDetails.Where(detail =>
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
return MatchCargoTypeFactor(factors, cargoLabel);
|
||||
}).ToList();
|
||||
|
||||
if (cargoMatchedDetails.Count == 0)
|
||||
{
|
||||
var requiredCargo = zoneMatchedDetails
|
||||
.Select(detail =>
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
return TryGetFactorValue(factors, out var cargo, "货物类型", "cargo") ? cargo.Trim() : null;
|
||||
})
|
||||
.Where(cargo => !string.IsNullOrWhiteSpace(cargo))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
var requiredText = requiredCargo.Any() ? string.Join(", ", requiredCargo) : "(明细未限定货物类型)";
|
||||
reasons.Add($"【{label}】价格项Id={price.Id}:货物类型不匹配,当前={cargoLabel},明细要求=[{requiredText}]");
|
||||
continue;
|
||||
}
|
||||
|
||||
var weightMatched = false;
|
||||
var intervals = new List<string>();
|
||||
foreach (var detail in cargoMatchedDetails)
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
if (!TryGetWeightFactorText(factors, out var weightText))
|
||||
{
|
||||
weightMatched = true;
|
||||
break;
|
||||
}
|
||||
|
||||
intervals.Add(weightText);
|
||||
if (TryParseWeightInterval(weightText, out var rangeStart, out var rangeEnd)
|
||||
&& WeightInLeftOpenRightClosedInterval(chargeableWeight, rangeStart, rangeEnd))
|
||||
{
|
||||
weightMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!weightMatched)
|
||||
{
|
||||
var intervalText = intervals.Count > 0
|
||||
? string.Join(", ", intervals.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
: "无有效重量区间";
|
||||
reasons.Add(
|
||||
$"【{label}】价格项Id={price.Id}:计费重 {chargeableWeight}kg 未落入重量区间 [{intervalText}](左开右闭 (start,end])");
|
||||
continue;
|
||||
}
|
||||
|
||||
var customerGroupLabel = customerGroup?.ToString() ?? "未登录/默认";
|
||||
reasons.Add(
|
||||
$"【{label}】价格项Id={price.Id}:明细已匹配但金额为0或客户组「{customerGroupLabel}」无对应单价");
|
||||
}
|
||||
|
||||
return string.Join(";", reasons);
|
||||
}
|
||||
|
||||
public static List<OrderFee> CalculateForecastReceivableFees(
|
||||
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||
string priceVersion,
|
||||
|
||||
@ -142,6 +142,23 @@ public class PriceCheckSnapshotVerificationTests
|
||||
fees.Sum(x => x.Amount).Should().Be(31M);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplainQuotePriceMiss_ZoneMismatch_ShouldNameEachPriceItem()
|
||||
{
|
||||
var reason = ParcelBillingCalculator.ExplainQuotePriceMiss(
|
||||
BuildUserSnapshot(),
|
||||
0.5M,
|
||||
["默认分区"],
|
||||
CargoAttributeCodes.General,
|
||||
CustomerGroup.Normal);
|
||||
|
||||
reason.Should().Contain("分区不匹配");
|
||||
reason.Should().Contain("明细分区=[3]");
|
||||
reason.Should().Contain("解析分区键=[默认分区]");
|
||||
reason.Should().Contain("freight");
|
||||
reason.Should().Contain("handling");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserSnapshot_WrongZone_ShouldReturnNoFees()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user