修复订单隔离和查价
This commit is contained in:
parent
00835bfadd
commit
b7ed14d6af
@ -84,13 +84,13 @@ public partial class OrderService
|
|||||||
SyncWeightRows(weights, forecast);
|
SyncWeightRows(weights, forecast);
|
||||||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||||||
|
|
||||||
var zoneName = await MatchPriceZoneAsync(
|
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
||||||
product,
|
product,
|
||||||
country.CountryCode,
|
country.CountryCode,
|
||||||
input.Receiver?.Province,
|
input.Receiver?.Province,
|
||||||
input.Receiver?.City,
|
input.Receiver?.City,
|
||||||
input.Receiver?.ZipCode);
|
input.Receiver?.ZipCode);
|
||||||
if (string.IsNullOrWhiteSpace(zoneName))
|
if (zoneKeys == null || zoneKeys.Count == 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException("价格分区未匹配(规则快照)");
|
throw new BusinessException("价格分区未匹配(规则快照)");
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ public partial class OrderService
|
|||||||
snapshotBinding.PriceVersion,
|
snapshotBinding.PriceVersion,
|
||||||
snapshotBinding.PriceEffectiveTime,
|
snapshotBinding.PriceEffectiveTime,
|
||||||
forecast.ChargeableWeight ?? 0,
|
forecast.ChargeableWeight ?? 0,
|
||||||
zoneName,
|
zoneKeys,
|
||||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||||||
customer?.CustomerGroup);
|
customer?.CustomerGroup);
|
||||||
|
|
||||||
@ -398,7 +398,7 @@ public partial class OrderService
|
|||||||
zipCode));
|
zipCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> MatchPriceZoneAsync(
|
private async Task<IReadOnlyList<string>?> ResolvePriceZoneKeysAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
string countryCode,
|
string countryCode,
|
||||||
string? province,
|
string? province,
|
||||||
@ -407,10 +407,15 @@ public partial class OrderService
|
|||||||
{
|
{
|
||||||
if (!product.PriceAreaId.HasValue || product.PriceAreaId <= 0)
|
if (!product.PriceAreaId.HasValue || product.PriceAreaId <= 0)
|
||||||
{
|
{
|
||||||
return "默认分区";
|
return new[] { "默认分区" };
|
||||||
}
|
}
|
||||||
|
|
||||||
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == product.PriceAreaId.Value);
|
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == product.PriceAreaId.Value);
|
||||||
|
if (area == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.PriceAreaId.Value);
|
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(product.PriceAreaId.Value);
|
||||||
var matched = rules.FirstOrDefault(rule => ParcelBillingCalculator.IsAreaMatched(
|
var matched = rules.FirstOrDefault(rule => ParcelBillingCalculator.IsAreaMatched(
|
||||||
rule,
|
rule,
|
||||||
@ -418,7 +423,8 @@ public partial class OrderService
|
|||||||
province,
|
province,
|
||||||
city,
|
city,
|
||||||
zipCode));
|
zipCode));
|
||||||
return ParcelBillingCalculator.ResolveZoneName(matched, area);
|
var keys = ParcelBillingCalculator.BuildPriceZoneKeys(matched, area);
|
||||||
|
return keys.Count > 0 ? keys : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyChargeableWeightAsync(SalesProduct product, OrderWeight forecast)
|
private async Task ApplyChargeableWeightAsync(SalesProduct product, OrderWeight forecast)
|
||||||
|
|||||||
@ -148,27 +148,27 @@ public partial class OrderService
|
|||||||
var forecast = ParcelBillingCalculator.BuildForecastWeight(context);
|
var forecast = ParcelBillingCalculator.BuildForecastWeight(context);
|
||||||
await ApplyChargeableWeightAsync(product, forecast);
|
await ApplyChargeableWeightAsync(product, forecast);
|
||||||
|
|
||||||
var zoneName = await MatchPriceZoneAsync(
|
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
||||||
product,
|
product,
|
||||||
context.CountryCode,
|
context.CountryCode,
|
||||||
context.Province,
|
context.Province,
|
||||||
context.City,
|
context.City,
|
||||||
context.PostalCode);
|
context.PostalCode);
|
||||||
if (string.IsNullOrWhiteSpace(zoneName))
|
if (zoneKeys == null || zoneKeys.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees(
|
||||||
snapshotBinding.Price,
|
snapshotBinding.Price,
|
||||||
snapshotBinding.PriceVersion,
|
snapshotBinding.PriceVersion,
|
||||||
snapshotBinding.PriceEffectiveTime,
|
snapshotBinding.PriceEffectiveTime,
|
||||||
forecast.ChargeableWeight ?? 0,
|
forecast.ChargeableWeight ?? 0,
|
||||||
zoneName,
|
zoneKeys,
|
||||||
context.CargoAttribute,
|
context.CargoAttribute,
|
||||||
customerGroup);
|
customerGroup);
|
||||||
|
|
||||||
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshotBinding.Price))
|
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledQuotePriceItems(snapshotBinding.Price))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -189,7 +189,7 @@ public partial class OrderService
|
|||||||
PriceVersion = snapshotBinding.PriceVersion,
|
PriceVersion = snapshotBinding.PriceVersion,
|
||||||
VolumetricWeight = forecast.VolumetricWeight ?? 0,
|
VolumetricWeight = forecast.VolumetricWeight ?? 0,
|
||||||
ChargeableWeight = forecast.ChargeableWeight ?? 0,
|
ChargeableWeight = forecast.ChargeableWeight ?? 0,
|
||||||
ZoneName = zoneName,
|
ZoneName = zoneKeys[0],
|
||||||
IsRemote = isRemote,
|
IsRemote = isRemote,
|
||||||
Fees = fees
|
Fees = fees
|
||||||
};
|
};
|
||||||
|
|||||||
@ -147,12 +147,52 @@ internal static class ParcelBillingCalculator
|
|||||||
return area?.Name;
|
return area?.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<OrderFee> CalculateForecastReceivableFees(
|
public static IReadOnlyList<string> BuildPriceZoneKeys(KpiAreaInfo? matched, KpiArea? area)
|
||||||
|
{
|
||||||
|
var keys = new List<string>();
|
||||||
|
if (matched != null)
|
||||||
|
{
|
||||||
|
var name = ResolveZoneName(matched, area);
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
keys.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.Add(matched.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
if (!string.IsNullOrWhiteSpace(matched.Province))
|
||||||
|
{
|
||||||
|
keys.Add(matched.Province);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(matched.CountryName))
|
||||||
|
{
|
||||||
|
keys.Add(matched.CountryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (area != null)
|
||||||
|
{
|
||||||
|
keys.Add(area.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
if (!string.IsNullOrWhiteSpace(area.Name))
|
||||||
|
{
|
||||||
|
keys.Add(area.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(area.SystemName))
|
||||||
|
{
|
||||||
|
keys.Add(area.SystemName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<OrderFee> CalculateQuoteReceivableFees(
|
||||||
SalesProductPricePublishSnapshotDto priceSnapshot,
|
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||||
string priceVersion,
|
string priceVersion,
|
||||||
DateTime priceEffectiveTime,
|
DateTime priceEffectiveTime,
|
||||||
decimal chargeableWeight,
|
decimal chargeableWeight,
|
||||||
string zoneName,
|
IReadOnlyList<string> zoneKeys,
|
||||||
string cargoAttribute,
|
string cargoAttribute,
|
||||||
CustomerGroup? customerGroup)
|
CustomerGroup? customerGroup)
|
||||||
{
|
{
|
||||||
@ -164,9 +204,53 @@ internal static class ParcelBillingCalculator
|
|||||||
priceVersion,
|
priceVersion,
|
||||||
priceEffectiveTime,
|
priceEffectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneName,
|
zoneKeys,
|
||||||
|
cargoAttribute,
|
||||||
|
customerGroup,
|
||||||
|
onlyForecastChargingTime: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||||
|
priceSnapshot.Prices.Any(x => x.Status == 1);
|
||||||
|
|
||||||
|
public static List<OrderFee> CalculateForecastReceivableFees(
|
||||||
|
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||||
|
string priceVersion,
|
||||||
|
DateTime priceEffectiveTime,
|
||||||
|
decimal chargeableWeight,
|
||||||
|
string zoneName,
|
||||||
|
string cargoAttribute,
|
||||||
|
CustomerGroup? customerGroup) =>
|
||||||
|
CalculateForecastReceivableFees(
|
||||||
|
priceSnapshot,
|
||||||
|
priceVersion,
|
||||||
|
priceEffectiveTime,
|
||||||
|
chargeableWeight,
|
||||||
|
new[] { zoneName },
|
||||||
cargoAttribute,
|
cargoAttribute,
|
||||||
customerGroup);
|
customerGroup);
|
||||||
|
|
||||||
|
public static List<OrderFee> CalculateForecastReceivableFees(
|
||||||
|
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||||
|
string priceVersion,
|
||||||
|
DateTime priceEffectiveTime,
|
||||||
|
decimal chargeableWeight,
|
||||||
|
IReadOnlyList<string> zoneKeys,
|
||||||
|
string cargoAttribute,
|
||||||
|
CustomerGroup? customerGroup)
|
||||||
|
{
|
||||||
|
var items = priceSnapshot.Prices.Select(p => new BillingPriceItem(
|
||||||
|
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
|
||||||
|
return CalculateForecastFees(
|
||||||
|
items,
|
||||||
|
OrderFeeTypeCodes.Receivable,
|
||||||
|
priceVersion,
|
||||||
|
priceEffectiveTime,
|
||||||
|
chargeableWeight,
|
||||||
|
zoneKeys,
|
||||||
|
cargoAttribute,
|
||||||
|
customerGroup,
|
||||||
|
onlyForecastChargingTime: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -189,9 +273,10 @@ internal static class ParcelBillingCalculator
|
|||||||
priceVersion,
|
priceVersion,
|
||||||
priceEffectiveTime,
|
priceEffectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneName,
|
new[] { zoneName },
|
||||||
cargoAttribute,
|
cargoAttribute,
|
||||||
customerGroup);
|
customerGroup,
|
||||||
|
onlyForecastChargingTime: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasEnabledForecastPriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
public static bool HasEnabledForecastPriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||||
@ -294,19 +379,20 @@ internal static class ParcelBillingCalculator
|
|||||||
string priceVersion,
|
string priceVersion,
|
||||||
DateTime priceEffectiveTime,
|
DateTime priceEffectiveTime,
|
||||||
decimal chargeableWeight,
|
decimal chargeableWeight,
|
||||||
string zoneName,
|
IReadOnlyList<string> zoneKeys,
|
||||||
string cargoAttribute,
|
string cargoAttribute,
|
||||||
CustomerGroup? customerGroup)
|
CustomerGroup? customerGroup,
|
||||||
|
bool onlyForecastChargingTime)
|
||||||
{
|
{
|
||||||
var fees = new List<OrderFee>();
|
var fees = new List<OrderFee>();
|
||||||
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
|
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
|
||||||
{
|
{
|
||||||
if (!IsForecastChargingTime(priceItem.ChargingTime))
|
if (onlyForecastChargingTime && !IsForecastChargingTime(priceItem.ChargingTime))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var detail = MatchPriceDetail(priceItem.Details, zoneName, cargoAttribute, chargeableWeight);
|
var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight);
|
||||||
if (detail == null)
|
if (detail == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -343,16 +429,19 @@ internal static class ParcelBillingCalculator
|
|||||||
|
|
||||||
private static ChannelPriceDetailOutput? MatchPriceDetail(
|
private static ChannelPriceDetailOutput? MatchPriceDetail(
|
||||||
IReadOnlyList<ChannelPriceDetailOutput> details,
|
IReadOnlyList<ChannelPriceDetailOutput> details,
|
||||||
string zoneName,
|
IReadOnlyList<string> zoneKeys,
|
||||||
string cargoAttribute,
|
string cargoAttribute,
|
||||||
decimal chargeableWeight)
|
decimal chargeableWeight)
|
||||||
{
|
{
|
||||||
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
|
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
|
||||||
|
var tierCandidates = new List<(ChannelPriceDetailOutput Detail, decimal Tier)>();
|
||||||
|
ChannelPriceDetailOutput? rangeHit = null;
|
||||||
|
ChannelPriceDetailOutput? genericHit = null;
|
||||||
|
|
||||||
foreach (var detail in details)
|
foreach (var detail in details)
|
||||||
{
|
{
|
||||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||||
if (!MatchFactorValue(factors, "分区", zoneName)
|
if (!ZoneMatches(factors, zoneKeys))
|
||||||
&& !MatchFactorValue(factors, "价格分区", zoneName))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -363,15 +452,107 @@ internal static class ParcelBillingCalculator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MatchWeightRange(factors, chargeableWeight))
|
if (TryParseWeightRange(factors, out var rangeStart, out var rangeEnd))
|
||||||
{
|
{
|
||||||
|
if (chargeableWeight >= rangeStart && chargeableWeight <= rangeEnd)
|
||||||
|
{
|
||||||
|
rangeHit = detail;
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return detail;
|
if (TryParseSingleWeightTier(factors, out var tier))
|
||||||
|
{
|
||||||
|
tierCandidates.Add((detail, tier));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
genericHit = detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (rangeHit != null)
|
||||||
|
{
|
||||||
|
return rangeHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tierCandidates.Count > 0)
|
||||||
|
{
|
||||||
|
var ceiling = tierCandidates
|
||||||
|
.Where(x => x.Tier >= chargeableWeight)
|
||||||
|
.OrderBy(x => x.Tier)
|
||||||
|
.Select(x => x.Detail)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (ceiling != null)
|
||||||
|
{
|
||||||
|
return ceiling;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tierCandidates.OrderByDescending(x => x.Tier).First().Detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return genericHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ZoneMatches(Dictionary<string, string> factors, IReadOnlyList<string> zoneKeys)
|
||||||
|
{
|
||||||
|
if (!factors.TryGetValue("分区", out var zoneValue) || string.IsNullOrWhiteSpace(zoneValue))
|
||||||
|
{
|
||||||
|
if (!factors.TryGetValue("价格分区", out zoneValue) || string.IsNullOrWhiteSpace(zoneValue))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = zoneValue.Trim();
|
||||||
|
return zoneKeys.Any(key => string.Equals(key.Trim(), expected, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseWeightRange(
|
||||||
|
Dictionary<string, string> factors,
|
||||||
|
out decimal start,
|
||||||
|
out decimal end)
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
end = 0;
|
||||||
|
if (!factors.TryGetValue("重量", out var rangeText) || string.IsNullOrWhiteSpace(rangeText))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = rangeText.Split('-', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length != 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decimal.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out start))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decimal.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out end))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseSingleWeightTier(Dictionary<string, string> factors, out decimal tier)
|
||||||
|
{
|
||||||
|
tier = 0;
|
||||||
|
if (!factors.TryGetValue("重量", out var text) || string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.Contains('-', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out tier);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> ParseFactorDictionary(object? factorValuesJson)
|
private static Dictionary<string, string> ParseFactorDictionary(object? factorValuesJson)
|
||||||
@ -404,23 +585,7 @@ internal static class ParcelBillingCalculator
|
|||||||
|
|
||||||
private static bool MatchWeightRange(Dictionary<string, string> factors, decimal chargeableWeight)
|
private static bool MatchWeightRange(Dictionary<string, string> factors, decimal chargeableWeight)
|
||||||
{
|
{
|
||||||
if (!factors.TryGetValue("重量", out var rangeText) || string.IsNullOrWhiteSpace(rangeText))
|
if (!TryParseWeightRange(factors, out var start, out var end))
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = rangeText.Split('-', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
if (parts.Length != 2)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decimal.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var start))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decimal.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var end))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -434,7 +599,7 @@ internal static class ParcelBillingCalculator
|
|||||||
CustomerGroup? customerGroup)
|
CustomerGroup? customerGroup)
|
||||||
{
|
{
|
||||||
var formula = detail.CalculationFormula?.Trim() ?? string.Empty;
|
var formula = detail.CalculationFormula?.Trim() ?? string.Empty;
|
||||||
if (formula is "单价" or "固定价")
|
if (formula is "单价" or "固定价" or "total" or "总价")
|
||||||
{
|
{
|
||||||
return ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0;
|
return ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0;
|
||||||
}
|
}
|
||||||
@ -513,7 +678,7 @@ internal static class ParcelBillingCalculator
|
|||||||
|
|
||||||
private static decimal? ResolveCustomerGroupPrice(ChannelPriceDetailOutput detail, CustomerGroup? customerGroup)
|
private static decimal? ResolveCustomerGroupPrice(ChannelPriceDetailOutput detail, CustomerGroup? customerGroup)
|
||||||
{
|
{
|
||||||
if (!customerGroup.HasValue || detail.CustomerGroupValues == null)
|
if (detail.CustomerGroupValues == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -525,18 +690,28 @@ internal static class ParcelBillingCalculator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = customerGroup.Value switch
|
if (customerGroup.HasValue)
|
||||||
{
|
{
|
||||||
CustomerGroup.Vip1 => "客户组1",
|
var key = customerGroup.Value switch
|
||||||
CustomerGroup.Vip2 => "客户组2",
|
{
|
||||||
CustomerGroup.Vip3 => "客户组3",
|
CustomerGroup.Normal => "普通客户",
|
||||||
CustomerGroup.Vip4 => "客户组4",
|
CustomerGroup.Vip1 => "客户组1",
|
||||||
CustomerGroup.Vip5 => "客户组5",
|
CustomerGroup.Vip2 => "客户组2",
|
||||||
_ => null
|
CustomerGroup.Vip3 => "客户组3",
|
||||||
};
|
CustomerGroup.Vip4 => "客户组4",
|
||||||
if (key != null && map.TryGetValue(key, out var value))
|
CustomerGroup.Vip5 => "客户组5",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key != null && map.TryGetValue(key, out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.TryGetValue("普通客户", out var normalPrice))
|
||||||
{
|
{
|
||||||
return value;
|
return normalPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -67,10 +67,11 @@ public class ParcelBillingCalculatorDemoTests
|
|||||||
var kpiArea = new KpiArea { Id = 1, Name = "美西分区" };
|
var kpiArea = new KpiArea { Id = 1, Name = "美西分区" };
|
||||||
var zoneMatched = ParcelBillingCalculator.IsAreaMatched(
|
var zoneMatched = ParcelBillingCalculator.IsAreaMatched(
|
||||||
kpiRule, quoteContext.CountryCode, quoteContext.Province, quoteContext.City, quoteContext.PostalCode);
|
kpiRule, quoteContext.CountryCode, quoteContext.Province, quoteContext.City, quoteContext.PostalCode);
|
||||||
|
var zoneKeys = ParcelBillingCalculator.BuildPriceZoneKeys(kpiRule, kpiArea);
|
||||||
var zoneName = ParcelBillingCalculator.ResolveZoneName(kpiRule, kpiArea) ?? "美西";
|
var zoneName = ParcelBillingCalculator.ResolveZoneName(kpiRule, kpiArea) ?? "美西";
|
||||||
|
|
||||||
zoneMatched.Should().BeTrue($"邮编 {quoteContext.PostalCode} 应命中美西分区");
|
zoneMatched.Should().BeTrue($"邮编 {quoteContext.PostalCode} 应命中美西分区");
|
||||||
_output.WriteLine($"分区匹配: {zoneMatched}, 分区名: {zoneName}");
|
_output.WriteLine($"分区匹配: {zoneMatched}, 分区名: {zoneName}, 分区键: {string.Join(',', zoneKeys)}");
|
||||||
|
|
||||||
// —— 3. 计重(可在 ParcelBillingCalculator.CalculateRawBillableWeight 打断点)——
|
// —— 3. 计重(可在 ParcelBillingCalculator.CalculateRawBillableWeight 打断点)——
|
||||||
var forecastWeight = ParcelBillingCalculator.BuildForecastWeight(quoteContext);
|
var forecastWeight = ParcelBillingCalculator.BuildForecastWeight(quoteContext);
|
||||||
@ -99,7 +100,7 @@ public class ParcelBillingCalculatorDemoTests
|
|||||||
priceVersion,
|
priceVersion,
|
||||||
effectiveTime,
|
effectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneName,
|
new[] { "美西" },
|
||||||
quoteContext.CargoAttribute,
|
quoteContext.CargoAttribute,
|
||||||
null);
|
null);
|
||||||
|
|
||||||
@ -127,7 +128,7 @@ public class ParcelBillingCalculatorDemoTests
|
|||||||
priceVersion,
|
priceVersion,
|
||||||
effectiveTime,
|
effectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneName,
|
"美西",
|
||||||
quoteContext.CargoAttribute,
|
quoteContext.CargoAttribute,
|
||||||
null);
|
null);
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,54 @@ public class ParcelBillingCalculatorTests
|
|||||||
fees[0].Amount.Should().Be(15M);
|
fees[0].Amount.Should().Be(15M);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateQuoteReceivableFees_ShouldSupportOutboundTotalPriceTiers()
|
||||||
|
{
|
||||||
|
var details = new[]
|
||||||
|
{
|
||||||
|
ParcelBillingTestData.TotalTierDetail("3", 0.5M, 11M),
|
||||||
|
ParcelBillingTestData.TotalTierDetail("3", 1M, 12M),
|
||||||
|
ParcelBillingTestData.TotalTierDetail("3", 1.5M, 12M),
|
||||||
|
ParcelBillingTestData.TotalTierDetail("3", 2M, 14M),
|
||||||
|
};
|
||||||
|
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||||
|
ParcelBillingTestData.ReceivablePriceItem("outbound", 1, "freight", details));
|
||||||
|
|
||||||
|
var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees(
|
||||||
|
snapshot,
|
||||||
|
_meta.PriceVersion,
|
||||||
|
_meta.EffectiveTime,
|
||||||
|
1M,
|
||||||
|
new[] { "3" },
|
||||||
|
"general",
|
||||||
|
null);
|
||||||
|
|
||||||
|
fees.Should().ContainSingle();
|
||||||
|
fees[0].Amount.Should().Be(12M);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateForecastReceivableFees_ShouldApplyNormalCustomerLabel()
|
||||||
|
{
|
||||||
|
var detail = ParcelBillingTestData.TotalTierDetail("3", 1M, 99M);
|
||||||
|
detail.CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 12M };
|
||||||
|
|
||||||
|
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||||
|
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail));
|
||||||
|
|
||||||
|
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||||
|
snapshot,
|
||||||
|
_meta.PriceVersion,
|
||||||
|
_meta.EffectiveTime,
|
||||||
|
1M,
|
||||||
|
new[] { "3" },
|
||||||
|
"general",
|
||||||
|
CustomerGroup.Normal);
|
||||||
|
|
||||||
|
fees.Should().ContainSingle();
|
||||||
|
fees[0].Amount.Should().Be(12M);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HasEnabledForecastPriceItems_ShouldDetectForecastEnabledItems()
|
public void HasEnabledForecastPriceItems_ShouldDetectForecastEnabledItems()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -92,6 +92,22 @@ internal static class ParcelBillingTestData
|
|||||||
FactorValuesJson = new Dictionary<string, string> { ["分区"] = zone }
|
FactorValuesJson = new Dictionary<string, string> { ["分区"] = zone }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static ChannelPriceDetailOutput TotalTierDetail(
|
||||||
|
string zone,
|
||||||
|
decimal weightTier,
|
||||||
|
decimal normalCustomerPrice) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
CalculationFormula = "total",
|
||||||
|
CalculatedValue = 0,
|
||||||
|
FactorValuesJson = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["分区"] = zone,
|
||||||
|
["重量"] = weightTier.ToString(CultureInfo.InvariantCulture)
|
||||||
|
},
|
||||||
|
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = normalCustomerPrice }
|
||||||
|
};
|
||||||
|
|
||||||
public static WeightRuleInfo GwRule(string condition = "gw>=vw", string factor = "6000") =>
|
public static WeightRuleInfo GwRule(string condition = "gw>=vw", string factor = "6000") =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user