fix: 查价运费匹配与费用公式展示优化
修复卖价明细英文货物类型无法匹配导致 freight 为 0 的问题;查价返回算法算式、其他费用明细及中文货物属性名;运费列仅汇总 freight 费用项。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
07fc7fffe7
commit
1c0dce9956
27
src/Oms.Application/Dtos/Orders/OrderPriceCheckFeeOutput.cs
Normal file
27
src/Oms.Application/Dtos/Orders/OrderPriceCheckFeeOutput.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 查价结果中的非运费明细(操作费等)。
|
||||
/// </summary>
|
||||
public class OrderPriceCheckFeeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 费用项目编码(如 handling)。
|
||||
/// </summary>
|
||||
public string CostItem { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 费用名称(如 操作费)。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单项算式片段(如 23)。
|
||||
/// </summary>
|
||||
public string FormulaPart { get; set; } = string.Empty;
|
||||
}
|
||||
@ -65,6 +65,11 @@ public class OrderPriceCheckOutput
|
||||
/// </summary>
|
||||
public decimal OtherFees { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 非运费明细(操作费等,含名称与金额)。
|
||||
/// </summary>
|
||||
public List<OrderPriceCheckFeeOutput> OtherFeeList { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 总成本。
|
||||
/// </summary>
|
||||
|
||||
15
src/Oms.Application/Services/Orders/BillingFeeLine.cs
Normal file
15
src/Oms.Application/Services/Orders/BillingFeeLine.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Services.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 计费试算单行(含展示用算式片段)。
|
||||
/// </summary>
|
||||
internal sealed class BillingFeeLine
|
||||
{
|
||||
public OrderFee Fee { get; init; } = null!;
|
||||
|
||||
public string PriceName { get; init; } = string.Empty;
|
||||
|
||||
public string FormulaPart { get; init; } = string.Empty;
|
||||
}
|
||||
@ -83,7 +83,7 @@ public partial class OrderService
|
||||
if (attempt.Success)
|
||||
{
|
||||
var quote = attempt.Quote!;
|
||||
var (freight, otherFees, totalCost, costFormula) = ParcelBillingCalculator.SummarizeFees(quote.Fees);
|
||||
var summary = ParcelBillingCalculator.SummarizeQuoteFees(quote.FeeLines);
|
||||
result.Items.Add(new OrderPriceCheckOutput
|
||||
{
|
||||
Id = quote.ProductId,
|
||||
@ -96,12 +96,13 @@ public partial class OrderService
|
||||
DeliveryTime = $"{product.MinTimeliness ?? 0}-{product.MaxTimeliness ?? 0}",
|
||||
VolumetricWeight = quote.VolumetricWeight,
|
||||
ChargeableWeight = quote.ChargeableWeight,
|
||||
CostFormula = costFormula,
|
||||
Freight = freight,
|
||||
OtherFees = otherFees,
|
||||
TotalCost = totalCost,
|
||||
CostFormula = summary.CostFormula,
|
||||
Freight = summary.Freight,
|
||||
OtherFees = summary.OtherFees,
|
||||
OtherFeeList = summary.OtherFeeList,
|
||||
TotalCost = summary.TotalCost,
|
||||
MaxDeliveryDays = product.MaxTimeliness,
|
||||
TotalCostNum = totalCost,
|
||||
TotalCostNum = summary.TotalCost,
|
||||
RulesVersion = quote.RulesVersion,
|
||||
PriceVersion = quote.PriceVersion,
|
||||
IsRemote = quote.IsRemote
|
||||
@ -141,9 +142,7 @@ public partial class OrderService
|
||||
return codes.Select(code =>
|
||||
{
|
||||
var attribute = ParcelBillingCalculator.MapCargoTypeToAttribute(code);
|
||||
var display = string.IsNullOrWhiteSpace(code)
|
||||
? ParcelBillingCalculator.GetCargoAttributeLabel(attribute)
|
||||
: code;
|
||||
var display = ParcelBillingCalculator.GetCargoAttributeLabel(attribute);
|
||||
return new PriceCheckCargoTarget(code, display, attribute);
|
||||
}).ToList();
|
||||
}
|
||||
@ -337,7 +336,7 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
var chargeableWeight = forecast.ChargeableWeight ?? 0;
|
||||
var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees(
|
||||
var feeLines = ParcelBillingCalculator.CalculateQuoteReceivableFeeLines(
|
||||
snapshotBinding.Price,
|
||||
snapshotBinding.PriceVersion,
|
||||
snapshotBinding.PriceEffectiveTime,
|
||||
@ -348,7 +347,7 @@ public partial class OrderService
|
||||
context.WarehouseId,
|
||||
context.WarehouseName);
|
||||
|
||||
if (fees.Count == 0)
|
||||
if (feeLines.Count == 0)
|
||||
{
|
||||
if (!ParcelBillingCalculator.HasEnabledQuotePriceItems(snapshotBinding.Price))
|
||||
{
|
||||
@ -391,7 +390,7 @@ public partial class OrderService
|
||||
ChargeableWeight = chargeableWeight,
|
||||
ZoneName = zoneKeys[0],
|
||||
IsRemote = isRemote,
|
||||
Fees = fees
|
||||
FeeLines = feeLines
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -356,7 +356,7 @@ internal static class ParcelBillingCalculator
|
||||
CustomerGroup? customerGroup,
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null) =>
|
||||
CalculateReceivableFees(
|
||||
CalculateQuoteReceivableFeeLines(
|
||||
priceSnapshot,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
@ -364,9 +364,34 @@ internal static class ParcelBillingCalculator
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
warehouseName).Select(x => x.Fee).ToList();
|
||||
|
||||
public static List<BillingFeeLine> CalculateQuoteReceivableFeeLines(
|
||||
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||
string priceVersion,
|
||||
DateTime priceEffectiveTime,
|
||||
decimal chargeableWeight,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup,
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null)
|
||||
{
|
||||
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
|
||||
p.Status, p.CostItem, p.PriceName, p.Currency, p.Details));
|
||||
return CalculateReceivableFeeLines(
|
||||
items,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
customerGroup,
|
||||
OrderFeeChargingStageCodes.Forecast,
|
||||
warehouseId,
|
||||
warehouseName);
|
||||
}
|
||||
|
||||
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||
HasEnabledReceivablePriceItems(priceSnapshot);
|
||||
@ -451,7 +476,7 @@ internal static class ParcelBillingCalculator
|
||||
var cargoMatchedDetails = zoneMatchedDetails.Where(detail =>
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
return MatchCargoTypeFactor(factors, cargoLabel);
|
||||
return MatchCargoTypeFactor(factors, cargoAttribute);
|
||||
}).ToList();
|
||||
|
||||
if (cargoMatchedDetails.Count == 0)
|
||||
@ -581,7 +606,7 @@ internal static class ParcelBillingCalculator
|
||||
string? warehouseName = null)
|
||||
{
|
||||
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
|
||||
p.Status, p.CostItem, p.Currency, p.Details));
|
||||
p.Status, p.CostItem, p.PriceName, p.Currency, p.Details));
|
||||
return CalculateReceivableFeesFromItems(
|
||||
items,
|
||||
priceVersion,
|
||||
@ -691,26 +716,71 @@ internal static class ParcelBillingCalculator
|
||||
public static (decimal Freight, decimal OtherFees, decimal TotalCost, string CostFormula) SummarizeFees(
|
||||
IReadOnlyList<OrderFee> fees)
|
||||
{
|
||||
if (fees.Count == 0)
|
||||
var summary = SummarizeQuoteFees(fees.Select(f => new BillingFeeLine
|
||||
{
|
||||
return (0, 0, 0, string.Empty);
|
||||
}
|
||||
|
||||
var freight = fees
|
||||
.Where(x => x.CostItem.Equals("freight", StringComparison.OrdinalIgnoreCase)
|
||||
|| x.CostItem.Contains("运费", StringComparison.OrdinalIgnoreCase))
|
||||
.Sum(x => x.Amount);
|
||||
if (freight <= 0)
|
||||
{
|
||||
freight = fees[0].Amount;
|
||||
}
|
||||
|
||||
var total = fees.Sum(x => x.Amount);
|
||||
var otherFees = Math.Max(0, total - freight);
|
||||
var formula = string.Join(" + ", fees.Select(x => $"{x.CostItem}:{x.Amount:0.####}"));
|
||||
return (freight, otherFees, total, formula);
|
||||
Fee = f,
|
||||
PriceName = ResolveFeeDisplayName(f.CostItem, null),
|
||||
FormulaPart = FormatDecimal(f.Amount)
|
||||
}).ToList());
|
||||
return (summary.Freight, summary.OtherFees, summary.TotalCost, summary.CostFormula);
|
||||
}
|
||||
|
||||
public static QuoteFeeSummary SummarizeQuoteFees(IReadOnlyList<BillingFeeLine> feeLines)
|
||||
{
|
||||
if (feeLines.Count == 0)
|
||||
{
|
||||
return new QuoteFeeSummary();
|
||||
}
|
||||
|
||||
var freightLines = feeLines.Where(x => IsFreightCostItem(x.Fee.CostItem)).ToList();
|
||||
var otherLines = feeLines.Where(x => !IsFreightCostItem(x.Fee.CostItem)).ToList();
|
||||
var freight = freightLines.Sum(x => x.Fee.Amount);
|
||||
var otherFees = otherLines.Sum(x => x.Fee.Amount);
|
||||
var total = feeLines.Sum(x => x.Fee.Amount);
|
||||
var formulaParts = feeLines
|
||||
.Select(x => x.FormulaPart)
|
||||
.Where(part => !string.IsNullOrWhiteSpace(part))
|
||||
.ToList();
|
||||
var costFormula = formulaParts.Count > 0 ? string.Join("+", formulaParts) : FormatDecimal(total);
|
||||
|
||||
return new QuoteFeeSummary
|
||||
{
|
||||
Freight = freight,
|
||||
OtherFees = otherFees,
|
||||
TotalCost = total,
|
||||
CostFormula = costFormula,
|
||||
OtherFeeList = otherLines.Select(x => new OrderPriceCheckFeeOutput
|
||||
{
|
||||
CostItem = x.Fee.CostItem,
|
||||
Name = ResolveFeeDisplayName(x.Fee.CostItem, x.PriceName),
|
||||
Amount = x.Fee.Amount,
|
||||
FormulaPart = x.FormulaPart
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsFreightCostItem(string costItem) =>
|
||||
costItem.Equals("freight", StringComparison.OrdinalIgnoreCase)
|
||||
|| costItem.Contains("运费", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static string ResolveFeeDisplayName(string costItem, string? priceName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(priceName))
|
||||
{
|
||||
return priceName.Trim();
|
||||
}
|
||||
|
||||
return costItem.ToLowerInvariant() switch
|
||||
{
|
||||
"freight" => "运费",
|
||||
"handling" => "操作费",
|
||||
_ => costItem
|
||||
};
|
||||
}
|
||||
|
||||
internal static string FormatDecimal(decimal value) =>
|
||||
value.ToString("0.####", CultureInfo.InvariantCulture);
|
||||
|
||||
public static OrderWeight BuildForecastWeight(ParcelQuoteContext context) =>
|
||||
new()
|
||||
{
|
||||
@ -723,6 +793,29 @@ internal static class ParcelBillingCalculator
|
||||
};
|
||||
|
||||
private static List<OrderFee> CalculateReceivableFeesFromItems(
|
||||
IEnumerable<ReceivableBillingPriceItem> priceItems,
|
||||
string priceVersion,
|
||||
DateTime priceEffectiveTime,
|
||||
decimal chargeableWeight,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup,
|
||||
string chargingStage,
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null) =>
|
||||
CalculateReceivableFeeLines(
|
||||
priceItems,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
customerGroup,
|
||||
chargingStage,
|
||||
warehouseId,
|
||||
warehouseName).Select(x => x.Fee).ToList();
|
||||
|
||||
private static List<BillingFeeLine> CalculateReceivableFeeLines(
|
||||
IEnumerable<ReceivableBillingPriceItem> priceItems,
|
||||
string priceVersion,
|
||||
DateTime priceEffectiveTime,
|
||||
@ -734,7 +827,7 @@ internal static class ParcelBillingCalculator
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null)
|
||||
{
|
||||
var fees = new List<OrderFee>();
|
||||
var fees = new List<BillingFeeLine>();
|
||||
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
|
||||
{
|
||||
var detail = MatchPriceDetail(
|
||||
@ -758,15 +851,29 @@ internal static class ParcelBillingCalculator
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
warehouseName);
|
||||
fees.Add(new OrderFee
|
||||
var formulaPart = BuildDetailFormulaExpression(
|
||||
priceItem.Details,
|
||||
detail,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
chargeableWeight,
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
warehouseName);
|
||||
fees.Add(new BillingFeeLine
|
||||
{
|
||||
Type = OrderFeeTypeCodes.Receivable,
|
||||
CostItem = priceItem.CostItem,
|
||||
Amount = amount,
|
||||
Currency = priceItem.Currency,
|
||||
PriceNo = priceVersion,
|
||||
PriceEffectiveTime = priceEffectiveTime,
|
||||
ChargingStage = chargingStage
|
||||
Fee = new OrderFee
|
||||
{
|
||||
Type = OrderFeeTypeCodes.Receivable,
|
||||
CostItem = priceItem.CostItem,
|
||||
Amount = amount,
|
||||
Currency = priceItem.Currency,
|
||||
PriceNo = priceVersion,
|
||||
PriceEffectiveTime = priceEffectiveTime,
|
||||
ChargingStage = chargingStage
|
||||
},
|
||||
PriceName = priceItem.PriceName,
|
||||
FormulaPart = formulaPart
|
||||
});
|
||||
}
|
||||
|
||||
@ -827,6 +934,7 @@ internal static class ParcelBillingCalculator
|
||||
private readonly record struct ReceivableBillingPriceItem(
|
||||
byte Status,
|
||||
string CostItem,
|
||||
string PriceName,
|
||||
string Currency,
|
||||
List<ChannelPriceDetailOutput> Details);
|
||||
|
||||
@ -844,7 +952,6 @@ internal static class ParcelBillingCalculator
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null)
|
||||
{
|
||||
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
|
||||
ChannelPriceDetailOutput? bestMatch = null;
|
||||
var bestIntervalWidth = decimal.MaxValue;
|
||||
|
||||
@ -856,7 +963,7 @@ internal static class ParcelBillingCalculator
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!MatchCargoTypeFactor(factors, cargoLabel))
|
||||
if (!MatchCargoTypeFactor(factors, cargoAttribute))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -960,15 +1067,30 @@ internal static class ParcelBillingCalculator
|
||||
private static bool TryGetWeightFactorText(Dictionary<string, string> factors, out string weightText) =>
|
||||
TryGetFactorValue(factors, out weightText, "重量", "weight") && !string.IsNullOrWhiteSpace(weightText);
|
||||
|
||||
private static bool MatchCargoTypeFactor(Dictionary<string, string> factors, string cargoLabel)
|
||||
/// <summary>
|
||||
/// 货物类型因子匹配:明细可能存中文标签(普货)或英文 code(general),查价侧统一为 cargoAttribute。
|
||||
/// </summary>
|
||||
private static bool MatchCargoTypeFactor(Dictionary<string, string> factors, string cargoAttribute)
|
||||
{
|
||||
if (!TryGetFactorValue(factors, out var actual, "货物类型", "cargo") || string.IsNullOrWhiteSpace(actual))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(actual.Trim(), cargoLabel, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actual.Trim(), cargoLabel, StringComparison.Ordinal);
|
||||
var trimmed = actual.Trim();
|
||||
var expectedLabel = GetCargoAttributeLabel(cargoAttribute);
|
||||
if (string.Equals(trimmed, expectedLabel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(trimmed, cargoAttribute, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var actualAttribute = MapCargoTypeToAttribute(trimmed);
|
||||
return string.Equals(actualAttribute, cargoAttribute, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool MatchWarehouseFactor(
|
||||
@ -1062,7 +1184,6 @@ internal static class ParcelBillingCalculator
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null)
|
||||
{
|
||||
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
|
||||
return NormalizeCalculationFormula(detail.CalculationFormula) switch
|
||||
{
|
||||
"unit" => CalculateUnitPrice(detail, chargeableWeight, customerGroup),
|
||||
@ -1071,7 +1192,7 @@ internal static class ParcelBillingCalculator
|
||||
allDetails,
|
||||
detail,
|
||||
zoneKeys,
|
||||
cargoLabel,
|
||||
cargoAttribute,
|
||||
chargeableWeight,
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
@ -1080,6 +1201,102 @@ internal static class ParcelBillingCalculator
|
||||
};
|
||||
}
|
||||
|
||||
internal static string BuildDetailFormulaExpression(
|
||||
IReadOnlyList<ChannelPriceDetailOutput> allDetails,
|
||||
ChannelPriceDetailOutput detail,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
decimal chargeableWeight,
|
||||
CustomerGroup? customerGroup,
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null)
|
||||
{
|
||||
return NormalizeCalculationFormula(detail.CalculationFormula) switch
|
||||
{
|
||||
"unit" => BuildUnitFormulaExpression(detail, chargeableWeight, customerGroup),
|
||||
"total" => FormatDecimal(
|
||||
ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0),
|
||||
"continue" => BuildContinueFormulaExpression(
|
||||
allDetails,
|
||||
detail,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
chargeableWeight,
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
warehouseName),
|
||||
_ => FormatDecimal(detail.CalculatedValue ?? 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildUnitFormulaExpression(
|
||||
ChannelPriceDetailOutput detail,
|
||||
decimal chargeableWeight,
|
||||
CustomerGroup? customerGroup)
|
||||
{
|
||||
var unitPrice = ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0;
|
||||
return $"{FormatDecimal(unitPrice)}*{FormatDecimal(chargeableWeight)}";
|
||||
}
|
||||
|
||||
private static string BuildContinueFormulaExpression(
|
||||
IReadOnlyList<ChannelPriceDetailOutput> allDetails,
|
||||
ChannelPriceDetailOutput detail,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
decimal chargeableWeight,
|
||||
CustomerGroup? customerGroup,
|
||||
int? warehouseId = null,
|
||||
string? warehouseName = null)
|
||||
{
|
||||
var factors = ParseFactorDictionary(detail.FactorValuesJson);
|
||||
if (TryGetWeightFactorText(factors, out var weightText)
|
||||
&& TryParseWeightInterval(weightText, out var rangeStart, out _)
|
||||
&& TryFindAdjacentTotalBasePrice(
|
||||
allDetails,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
rangeStart,
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
warehouseName,
|
||||
out var basePrice))
|
||||
{
|
||||
if (chargeableWeight <= rangeStart)
|
||||
{
|
||||
return FormatDecimal(basePrice);
|
||||
}
|
||||
|
||||
var continueUnitPrice = ResolveCustomerGroupPrice(detail, customerGroup) ?? detail.CalculatedValue ?? 0;
|
||||
return
|
||||
$"{FormatDecimal(basePrice)}+({FormatDecimal(chargeableWeight)}-{FormatDecimal(rangeStart)})*{FormatDecimal(continueUnitPrice)}";
|
||||
}
|
||||
|
||||
var continuePricing = TryReadContinuePricing(factors, detail);
|
||||
var firstWeight = continuePricing?.FirstWeight ?? 1M;
|
||||
var firstPrice = continuePricing?.FirstPrice ?? detail.CalculatedValue ?? 0;
|
||||
var continueUnit = continuePricing?.ContinueUnit ?? 0.5M;
|
||||
var continuePrice = continuePricing?.ContinuePrice ?? firstPrice;
|
||||
|
||||
if (customerGroup.HasValue)
|
||||
{
|
||||
var groupPrice = ResolveCustomerGroupPrice(detail, customerGroup);
|
||||
if (groupPrice.HasValue)
|
||||
{
|
||||
firstPrice = groupPrice.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (chargeableWeight <= firstWeight)
|
||||
{
|
||||
return FormatDecimal(firstPrice);
|
||||
}
|
||||
|
||||
var overflow = chargeableWeight - firstWeight;
|
||||
var continueUnits = Math.Ceiling(overflow / continueUnit);
|
||||
return
|
||||
$"{FormatDecimal(firstPrice)}+{FormatDecimal(continueUnits)}*{FormatDecimal(continuePrice)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单价:计费重 × 单价。
|
||||
/// </summary>
|
||||
@ -1099,7 +1316,7 @@ internal static class ParcelBillingCalculator
|
||||
IReadOnlyList<ChannelPriceDetailOutput> allDetails,
|
||||
ChannelPriceDetailOutput detail,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoLabel,
|
||||
string cargoAttribute,
|
||||
decimal chargeableWeight,
|
||||
CustomerGroup? customerGroup,
|
||||
int? warehouseId = null,
|
||||
@ -1111,7 +1328,7 @@ internal static class ParcelBillingCalculator
|
||||
&& TryFindAdjacentTotalBasePrice(
|
||||
allDetails,
|
||||
zoneKeys,
|
||||
cargoLabel,
|
||||
cargoAttribute,
|
||||
rangeStart,
|
||||
customerGroup,
|
||||
warehouseId,
|
||||
@ -1139,7 +1356,7 @@ internal static class ParcelBillingCalculator
|
||||
private static bool TryFindAdjacentTotalBasePrice(
|
||||
IReadOnlyList<ChannelPriceDetailOutput> allDetails,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoLabel,
|
||||
string cargoAttribute,
|
||||
decimal continueRangeStart,
|
||||
CustomerGroup? customerGroup,
|
||||
int? warehouseId,
|
||||
@ -1151,7 +1368,7 @@ internal static class ParcelBillingCalculator
|
||||
{
|
||||
var factors = ParseFactorDictionary(candidate.FactorValuesJson);
|
||||
if (!ZoneMatches(factors, zoneKeys)
|
||||
|| !MatchCargoTypeFactor(factors, cargoLabel)
|
||||
|| !MatchCargoTypeFactor(factors, cargoAttribute)
|
||||
|| !MatchWarehouseFactor(factors, warehouseId, warehouseName))
|
||||
{
|
||||
continue;
|
||||
|
||||
@ -25,5 +25,5 @@ internal sealed class ParcelQuoteResult
|
||||
|
||||
public bool IsRemote { get; init; }
|
||||
|
||||
public IReadOnlyList<OrderFee> Fees { get; init; } = [];
|
||||
public IReadOnlyList<BillingFeeLine> FeeLines { get; init; } = [];
|
||||
}
|
||||
|
||||
19
src/Oms.Application/Services/Orders/QuoteFeeSummary.cs
Normal file
19
src/Oms.Application/Services/Orders/QuoteFeeSummary.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
|
||||
namespace Oms.Application.Services.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 查价费用汇总(含算式与其他费用明细)。
|
||||
/// </summary>
|
||||
internal sealed class QuoteFeeSummary
|
||||
{
|
||||
public decimal Freight { get; init; }
|
||||
|
||||
public decimal OtherFees { get; init; }
|
||||
|
||||
public decimal TotalCost { get; init; }
|
||||
|
||||
public string CostFormula { get; init; } = string.Empty;
|
||||
|
||||
public List<OrderPriceCheckFeeOutput> OtherFeeList { get; init; } = [];
|
||||
}
|
||||
@ -62,6 +62,102 @@ public class PriceCheckSnapshotVerificationTests
|
||||
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 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<string, string>
|
||||
{
|
||||
["分区"] = zone,
|
||||
["重量"] = weight,
|
||||
["货物类型"] = cargoType,
|
||||
["仓库"] = warehouseId
|
||||
},
|
||||
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = normalPrice }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void EnglishCargoTypeFactor_ShouldMatchCargoAttributeForFreight()
|
||||
{
|
||||
var snapshot = new SalesProductPricePublishSnapshotDto
|
||||
{
|
||||
SalesProductId = 1,
|
||||
Prices =
|
||||
[
|
||||
PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput
|
||||
{
|
||||
Id = 1,
|
||||
Status = 1,
|
||||
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 = 1,
|
||||
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,
|
||||
CustomerGroup.Normal,
|
||||
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,
|
||||
CustomerGroup.Normal,
|
||||
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,
|
||||
CustomerGroup.Normal,
|
||||
warehouseId: 2);
|
||||
|
||||
electronicsFees.Single(x => x.CostItem == "freight").Amount.Should().Be(11M);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseWeightInterval_SingleValue_ShouldBeLeftOpenRightClosedFromZero()
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user