diff --git a/src/Oms.Application/Dtos/Orders/OrderPriceCheckFeeOutput.cs b/src/Oms.Application/Dtos/Orders/OrderPriceCheckFeeOutput.cs
new file mode 100644
index 0000000..ca367bf
--- /dev/null
+++ b/src/Oms.Application/Dtos/Orders/OrderPriceCheckFeeOutput.cs
@@ -0,0 +1,27 @@
+namespace Oms.Application.Dtos.Orders;
+
+///
+/// 查价结果中的非运费明细(操作费等)。
+///
+public class OrderPriceCheckFeeOutput
+{
+ ///
+ /// 费用项目编码(如 handling)。
+ ///
+ public string CostItem { get; set; } = string.Empty;
+
+ ///
+ /// 费用名称(如 操作费)。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 金额。
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 单项算式片段(如 23)。
+ ///
+ public string FormulaPart { get; set; } = string.Empty;
+}
diff --git a/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs b/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
index 6b67581..1c619b4 100644
--- a/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderPriceCheckOutput.cs
@@ -65,6 +65,11 @@ public class OrderPriceCheckOutput
///
public decimal OtherFees { get; set; }
+ ///
+ /// 非运费明细(操作费等,含名称与金额)。
+ ///
+ public List OtherFeeList { get; set; } = [];
+
///
/// 总成本。
///
diff --git a/src/Oms.Application/Services/Orders/BillingFeeLine.cs b/src/Oms.Application/Services/Orders/BillingFeeLine.cs
new file mode 100644
index 0000000..b2ed61d
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/BillingFeeLine.cs
@@ -0,0 +1,15 @@
+using Oms.Domain.OrdersAggregate.Entities;
+
+namespace Oms.Application.Services.Orders;
+
+///
+/// 计费试算单行(含展示用算式片段)。
+///
+internal sealed class BillingFeeLine
+{
+ public OrderFee Fee { get; init; } = null!;
+
+ public string PriceName { get; init; } = string.Empty;
+
+ public string FormulaPart { get; init; } = string.Empty;
+}
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
index 8c585ec..d2707a6 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelQuote.cs
@@ -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
}
};
}
diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
index 64c3b38..c25c31d 100644
--- a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
+++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs
@@ -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 CalculateQuoteReceivableFeeLines(
+ SalesProductPricePublishSnapshotDto priceSnapshot,
+ string priceVersion,
+ DateTime priceEffectiveTime,
+ decimal chargeableWeight,
+ IReadOnlyList 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 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 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 CalculateReceivableFeesFromItems(
+ IEnumerable priceItems,
+ string priceVersion,
+ DateTime priceEffectiveTime,
+ decimal chargeableWeight,
+ IReadOnlyList 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 CalculateReceivableFeeLines(
IEnumerable priceItems,
string priceVersion,
DateTime priceEffectiveTime,
@@ -734,7 +827,7 @@ internal static class ParcelBillingCalculator
int? warehouseId = null,
string? warehouseName = null)
{
- var fees = new List();
+ var fees = new List();
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 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 factors, out string weightText) =>
TryGetFactorValue(factors, out weightText, "重量", "weight") && !string.IsNullOrWhiteSpace(weightText);
- private static bool MatchCargoTypeFactor(Dictionary factors, string cargoLabel)
+ ///
+ /// 货物类型因子匹配:明细可能存中文标签(普货)或英文 code(general),查价侧统一为 cargoAttribute。
+ ///
+ private static bool MatchCargoTypeFactor(Dictionary 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 allDetails,
+ ChannelPriceDetailOutput detail,
+ IReadOnlyList 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 allDetails,
+ ChannelPriceDetailOutput detail,
+ IReadOnlyList 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)}";
+ }
+
///
/// 单价:计费重 × 单价。
///
@@ -1099,7 +1316,7 @@ internal static class ParcelBillingCalculator
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
IReadOnlyList 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 allDetails,
IReadOnlyList 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;
diff --git a/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs b/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs
index 5402a5f..53ba0a1 100644
--- a/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs
+++ b/src/Oms.Application/Services/Orders/ParcelQuoteResult.cs
@@ -25,5 +25,5 @@ internal sealed class ParcelQuoteResult
public bool IsRemote { get; init; }
- public IReadOnlyList Fees { get; init; } = [];
+ public IReadOnlyList FeeLines { get; init; } = [];
}
diff --git a/src/Oms.Application/Services/Orders/QuoteFeeSummary.cs b/src/Oms.Application/Services/Orders/QuoteFeeSummary.cs
new file mode 100644
index 0000000..d2f33b8
--- /dev/null
+++ b/src/Oms.Application/Services/Orders/QuoteFeeSummary.cs
@@ -0,0 +1,19 @@
+using Oms.Application.Dtos.Orders;
+
+namespace Oms.Application.Services.Orders;
+
+///
+/// 查价费用汇总(含算式与其他费用明细)。
+///
+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 OtherFeeList { get; init; } = [];
+}
diff --git a/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs b/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs
index ff24576..7c846b9 100644
--- a/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs
+++ b/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs
@@ -62,6 +62,102 @@ public class PriceCheckSnapshotVerificationTests
CustomerGroupValues = new Dictionary { ["普通客户"] = 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
+ {
+ ["分区"] = zone,
+ ["重量"] = weight,
+ ["货物类型"] = cargoType,
+ ["仓库"] = warehouseId
+ },
+ CustomerGroupValues = new Dictionary { ["普通客户"] = 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()
{