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() {