feat: remove ChargingTime and bill receivables by weight stage

Drop sell/buy price charging-time config; write receivable fees after forecast or receive chargeable weight, hook inbound billing, fix order list status filter, and add DB migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-06-26 16:24:53 +08:00
parent 91ba6d5d59
commit 5e26de6ff3
27 changed files with 484 additions and 168 deletions

2
db.sql
View File

@ -336,7 +336,6 @@ CREATE TABLE `t_sales_product_price` (
`CostItem` VARCHAR(64) NOT NULL COMMENT '费用项', `CostItem` VARCHAR(64) NOT NULL COMMENT '费用项',
`CostUnit` VARCHAR(16) NOT NULL COMMENT '费用单位:kg/票/件', `CostUnit` VARCHAR(16) NOT NULL COMMENT '费用单位:kg/票/件',
`Currency` VARCHAR(16) NOT NULL COMMENT '币种:RMB/USD/EUR', `Currency` VARCHAR(16) NOT NULL COMMENT '币种:RMB/USD/EUR',
`ChargingTime` VARCHAR(32) NULL COMMENT '收费时机:预报/出库/签收',
`ChargingCondition` VARCHAR(64) NULL COMMENT '收费条件', `ChargingCondition` VARCHAR(64) NULL COMMENT '收费条件',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
@ -466,7 +465,6 @@ CREATE TABLE `t_service_channel_price` (
`CostItem` VARCHAR(64) NOT NULL COMMENT '费用项', `CostItem` VARCHAR(64) NOT NULL COMMENT '费用项',
`CostUnit` VARCHAR(16) NOT NULL COMMENT '费用单位:kg/票/件', `CostUnit` VARCHAR(16) NOT NULL COMMENT '费用单位:kg/票/件',
`Currency` VARCHAR(16) NOT NULL COMMENT '币种:RMB/USD/EUR', `Currency` VARCHAR(16) NOT NULL COMMENT '币种:RMB/USD/EUR',
`ChargingTime` VARCHAR(32) NULL COMMENT '收费时机:预报/出库/签收',
`ChargingCondition` VARCHAR(64) NULL COMMENT '收费条件', `ChargingCondition` VARCHAR(64) NULL COMMENT '收费条件',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',

View File

@ -0,0 +1,3 @@
-- 移除卖价/买价主表「收费时机」字段(已由件重尺阶段触发算价,不再配置 ChargingTime)
ALTER TABLE `t_sales_product_price` DROP COLUMN IF EXISTS `ChargingTime`;
ALTER TABLE `t_service_channel_price` DROP COLUMN IF EXISTS `ChargingTime`;

View File

@ -45,11 +45,6 @@ public class SalesProductPriceListOutput
/// </summary> /// </summary>
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
/// <summary>
/// 计费时机。
/// </summary>
public string? ChargingTime { get; set; }
/// <summary> /// <summary>
/// 计费条件。 /// 计费条件。
/// </summary> /// </summary>

View File

@ -47,11 +47,6 @@ public class SalesProductPriceSaveInput
/// </summary> /// </summary>
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
/// <summary>
/// 计费时机。
/// </summary>
public string? ChargingTime { get; set; }
/// <summary> /// <summary>
/// 计费条件。 /// 计费条件。
/// </summary> /// </summary>

View File

@ -45,11 +45,6 @@ public class ServiceChannelPriceListOutput
/// </summary> /// </summary>
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
/// <summary>
/// 计费时机。
/// </summary>
public string? ChargingTime { get; set; }
/// <summary> /// <summary>
/// 计费条件。 /// 计费条件。
/// </summary> /// </summary>

View File

@ -47,11 +47,6 @@ public class ServiceChannelPriceSaveInput
/// </summary> /// </summary>
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
/// <summary>
/// 计费时机。
/// </summary>
public string? ChargingTime { get; set; }
/// <summary> /// <summary>
/// 计费条件。 /// 计费条件。
/// </summary> /// </summary>

View File

@ -6,22 +6,26 @@ namespace Oms.Application.Dtos.Orders;
public class OrderExportInput public class OrderExportInput
{ {
/// <summary> /// <summary>
/// 状态。 /// 状态:all 或 OrderStatusCodes 枚举值。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("status")]
public string? Status { get; set; } public string? Status { get; set; }
/// <summary> /// <summary>
/// 关键词。 /// 关键词。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("keyword")]
public string? Keyword { get; set; } public string? Keyword { get; set; }
/// <summary> /// <summary>
/// 创建TimeStart。 /// 创建TimeStart。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("createTimeStart")]
public DateTime? CreateTimeStart { get; set; } public DateTime? CreateTimeStart { get; set; }
/// <summary> /// <summary>
/// 创建TimeEnd。 /// 创建TimeEnd。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
public DateTime? CreateTimeEnd { get; set; } public DateTime? CreateTimeEnd { get; set; }
} }

View File

@ -8,30 +8,36 @@ public class OrderPaginatedInput
/// <summary> /// <summary>
/// 页码,从 1 开始。 /// 页码,从 1 开始。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("page")]
public int Page { get; set; } = 1; public int Page { get; set; } = 1;
/// <summary> /// <summary>
/// 每页条数。 /// 每页条数。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("pageSize")]
public int PageSize { get; set; } = 20; public int PageSize { get; set; } = 20;
/// <summary> /// <summary>
/// 状态。 /// 状态:all 或 OrderStatusCodes 枚举值。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("status")]
public string? Status { get; set; } public string? Status { get; set; }
/// <summary> /// <summary>
/// 关键词。 /// 关键词。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("keyword")]
public string? Keyword { get; set; } public string? Keyword { get; set; }
/// <summary> /// <summary>
/// 创建TimeStart。 /// 创建TimeStart。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("createTimeStart")]
public DateTime? CreateTimeStart { get; set; } public DateTime? CreateTimeStart { get; set; }
/// <summary> /// <summary>
/// 创建TimeEnd。 /// 创建TimeEnd。
/// </summary> /// </summary>
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
public DateTime? CreateTimeEnd { get; set; } public DateTime? CreateTimeEnd { get; set; }
} }

View File

@ -84,8 +84,6 @@ public class SalesProductPriceSnapshotItemDto
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
public string? ChargingTime { get; set; }
public string? ChargingCondition { get; set; } public string? ChargingCondition { get; set; }
public List<ChannelPriceFactorOutput> Factors { get; set; } = []; public List<ChannelPriceFactorOutput> Factors { get; set; } = [];

View File

@ -84,7 +84,6 @@ public partial class SalesProductService
CostItem = price.CostItem, CostItem = price.CostItem,
CostUnit = price.CostUnit, CostUnit = price.CostUnit,
Currency = price.Currency, Currency = price.Currency,
ChargingTime = price.ChargingTime,
ChargingCondition = price.ChargingCondition, ChargingCondition = price.ChargingCondition,
Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [], Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [],
Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [], Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [],
@ -423,7 +422,6 @@ public partial class SalesProductService
CostItem = output.CostItem, CostItem = output.CostItem,
CostUnit = output.CostUnit, CostUnit = output.CostUnit,
Currency = output.Currency, Currency = output.Currency,
ChargingTime = output.ChargingTime,
ChargingCondition = output.ChargingCondition, ChargingCondition = output.ChargingCondition,
Factors = output.Factors.Select(item => new ChannelPriceFactorInput Factors = output.Factors.Select(item => new ChannelPriceFactorInput
{ {
@ -455,7 +453,6 @@ public partial class SalesProductService
CostItem = output.CostItem, CostItem = output.CostItem,
CostUnit = output.CostUnit, CostUnit = output.CostUnit,
Currency = output.Currency, Currency = output.Currency,
ChargingTime = output.ChargingTime,
ChargingCondition = output.ChargingCondition, ChargingCondition = output.ChargingCondition,
Factors = output.Factors.Select(item => new ChannelPriceFactorInput Factors = output.Factors.Select(item => new ChannelPriceFactorInput
{ {

View File

@ -197,7 +197,6 @@ public partial class SalesProductService : ApplicationService
CostItem = input.CostItem, CostItem = input.CostItem,
CostUnit = input.CostUnit, CostUnit = input.CostUnit,
Currency = input.Currency, Currency = input.Currency,
ChargingTime = input.ChargingTime,
ChargingCondition = input.ChargingCondition ChargingCondition = input.ChargingCondition
}, },
BuildSalesProductPriceFactors(input.Factors), BuildSalesProductPriceFactors(input.Factors),
@ -230,7 +229,6 @@ public partial class SalesProductService : ApplicationService
price.CostItem = input.CostItem; price.CostItem = input.CostItem;
price.CostUnit = input.CostUnit; price.CostUnit = input.CostUnit;
price.Currency = input.Currency; price.Currency = input.Currency;
price.ChargingTime = input.ChargingTime;
price.ChargingCondition = input.ChargingCondition; price.ChargingCondition = input.ChargingCondition;
await SalesProductRepository.UpdatePriceAsync( await SalesProductRepository.UpdatePriceAsync(
price, price,
@ -308,7 +306,6 @@ public partial class SalesProductService : ApplicationService
CostItem = priceInput.CostItem, CostItem = priceInput.CostItem,
CostUnit = priceInput.CostUnit, CostUnit = priceInput.CostUnit,
Currency = priceInput.Currency, Currency = priceInput.Currency,
ChargingTime = priceInput.ChargingTime,
ChargingCondition = priceInput.ChargingCondition ChargingCondition = priceInput.ChargingCondition
}); });
factors.AddRange(BuildSalesProductPriceFactors(priceInput.Factors, currentTempId)); factors.AddRange(BuildSalesProductPriceFactors(priceInput.Factors, currentTempId));

View File

@ -136,7 +136,6 @@ public partial class ServiceChannelService
CostItem = price.CostItem, CostItem = price.CostItem,
CostUnit = price.CostUnit, CostUnit = price.CostUnit,
Currency = price.Currency, Currency = price.Currency,
ChargingTime = price.ChargingTime,
ChargingCondition = price.ChargingCondition, ChargingCondition = price.ChargingCondition,
Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [], Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [],
Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [], Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [],
@ -392,7 +391,6 @@ public partial class ServiceChannelService
CostItem = output.CostItem, CostItem = output.CostItem,
CostUnit = output.CostUnit, CostUnit = output.CostUnit,
Currency = output.Currency, Currency = output.Currency,
ChargingTime = output.ChargingTime,
ChargingCondition = output.ChargingCondition, ChargingCondition = output.ChargingCondition,
Factors = output.Factors.Select(item => new ChannelPriceFactorInput Factors = output.Factors.Select(item => new ChannelPriceFactorInput
{ {

View File

@ -284,7 +284,6 @@ public partial class ServiceChannelService : ApplicationService
CostItem = input.CostItem, CostItem = input.CostItem,
CostUnit = input.CostUnit, CostUnit = input.CostUnit,
Currency = input.Currency, Currency = input.Currency,
ChargingTime = input.ChargingTime,
ChargingCondition = input.ChargingCondition ChargingCondition = input.ChargingCondition
}, },
BuildPriceFactors(input.Factors), BuildPriceFactors(input.Factors),
@ -317,7 +316,6 @@ public partial class ServiceChannelService : ApplicationService
price.CostItem = input.CostItem; price.CostItem = input.CostItem;
price.CostUnit = input.CostUnit; price.CostUnit = input.CostUnit;
price.Currency = input.Currency; price.Currency = input.Currency;
price.ChargingTime = input.ChargingTime;
price.ChargingCondition = input.ChargingCondition; price.ChargingCondition = input.ChargingCondition;
await ServiceChannelRepository.UpdatePriceAsync( await ServiceChannelRepository.UpdatePriceAsync(
price, price,
@ -395,7 +393,6 @@ public partial class ServiceChannelService : ApplicationService
CostItem = priceInput.CostItem, CostItem = priceInput.CostItem,
CostUnit = priceInput.CostUnit, CostUnit = priceInput.CostUnit,
Currency = priceInput.Currency, Currency = priceInput.Currency,
ChargingTime = priceInput.ChargingTime,
ChargingCondition = priceInput.ChargingCondition ChargingCondition = priceInput.ChargingCondition
}); });
factors.AddRange(BuildPriceFactors(priceInput.Factors, currentTempId)); factors.AddRange(BuildPriceFactors(priceInput.Factors, currentTempId));

View File

@ -2,6 +2,7 @@ using Fake.Application;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Operations; using Oms.Application.Dtos.Operations;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Services.Orders;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OperationsAggregate.Entities; using Oms.Domain.OperationsAggregate.Entities;
using Oms.Domain.OperationsAggregate.Repositories; using Oms.Domain.OperationsAggregate.Repositories;
@ -18,6 +19,7 @@ public class InboundRecordService : ApplicationService
private IInboundRecordRepository InboundRecordRepository => LazyServiceProvider.GetRequiredService<IInboundRecordRepository>(); private IInboundRecordRepository InboundRecordRepository => LazyServiceProvider.GetRequiredService<IInboundRecordRepository>();
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>(); private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>(); private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
/// <summary> /// <summary>
/// 新增入库记录。 /// 新增入库记录。
@ -50,6 +52,12 @@ public class InboundRecordService : ApplicationService
}; };
var savedEntity = await InboundRecordRepository.InsertAsync(entity); var savedEntity = await InboundRecordRepository.InsertAsync(entity);
await OrderService.ApplyReceiveBillingByWaybillAsync(
input.WaybillNo,
input.Length,
input.Width,
input.Height,
input.Weight);
return ObjectMapper.Map<InboundRecord, InboundRecordOutput>(savedEntity); return ObjectMapper.Map<InboundRecord, InboundRecordOutput>(savedEntity);
} }

View File

@ -102,24 +102,14 @@ public partial class OrderService
} }
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId); var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( await UpsertReceivableFeesAsync(
snapshotBinding.Price, order,
snapshotBinding.PriceVersion, snapshotBinding,
snapshotBinding.PriceEffectiveTime,
forecast.ChargeableWeight ?? 0, forecast.ChargeableWeight ?? 0,
zoneKeys, zoneKeys,
order.CargoAttribute ?? CargoAttributeCodes.General, order.CargoAttribute ?? CargoAttributeCodes.General,
customer?.CustomerGroup); customer?.CustomerGroup,
OrderFeeChargingStageCodes.Forecast);
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshotBinding.Price))
{
throw new BusinessException("产品卖价快照未配置或未匹配到有效价格");
}
if (fees.Count > 0)
{
await OrderRepository.UpsertFeesAsync(order.Id, fees);
}
order.OrderStatus = OrderStatusCodes.Predeclared; order.OrderStatus = OrderStatusCodes.Predeclared;
order.PredeclareFailReason = string.Empty; order.PredeclareFailReason = string.Empty;
@ -289,6 +279,116 @@ public partial class OrderService
}; };
} }
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingFromOrderAsync(SalesProduct product, Order order)
{
var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime;
SalesProductRulesPublishSnapshotDto rulesSnapshot;
string rulesVersion;
DateTime rulesEffectiveTime;
if (string.IsNullOrWhiteSpace(order.SalesProductRulesVersion)
|| order.SalesProductRulesVersion.StartsWith("LIVE-", StringComparison.OrdinalIgnoreCase))
{
rulesSnapshot = await BuildLiveRulesSnapshotAsync(product);
rulesVersion = order.SalesProductRulesVersion ?? $"LIVE-{product.Id}";
rulesEffectiveTime = bindTime;
}
else
{
var rulesRecord = await SalesProductRepository.GetPublishRecordByVersionAsync(
product.Id,
RulesOperatorType,
order.SalesProductRulesVersion);
if (rulesRecord?.Content == null)
{
throw new BusinessException("未命中订单绑定的规则快照版本");
}
rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(rulesRecord.Content)
?? throw new BusinessException("未命中订单绑定的规则快照版本");
rulesVersion = rulesRecord.Version;
rulesEffectiveTime = rulesRecord.PublishTime;
}
SalesProductPricePublishSnapshotDto priceSnapshot;
string priceVersion;
DateTime priceEffectiveTime;
if (string.IsNullOrWhiteSpace(order.SalesProductPriceVersion)
|| order.SalesProductPriceVersion.StartsWith("LIVE-", StringComparison.OrdinalIgnoreCase))
{
priceSnapshot = await BuildLivePriceSnapshotAsync(product);
if (priceSnapshot.Prices.Count == 0)
{
throw new BusinessException("未命中订单绑定的价格快照版本");
}
priceVersion = order.SalesProductPriceVersion ?? $"LIVE-{product.Id}";
priceEffectiveTime = bindTime;
}
else
{
var priceRecord = await SalesProductRepository.GetPublishRecordByVersionAsync(
product.Id,
PriceOperatorType,
order.SalesProductPriceVersion);
if (priceRecord?.Content == null)
{
throw new BusinessException("未命中订单绑定的价格快照版本");
}
priceSnapshot = OmsDocHelper.DeserializePriceSnapshot(priceRecord.Content)
?? throw new BusinessException("未命中订单绑定的价格快照版本");
if (priceSnapshot.Prices.Count == 0)
{
throw new BusinessException("未命中订单绑定的价格快照版本");
}
priceVersion = priceRecord.Version;
priceEffectiveTime = priceRecord.PublishTime;
}
return new ProductSnapshotBinding
{
RulesVersion = rulesVersion,
PriceVersion = priceVersion,
RulesEffectiveTime = rulesEffectiveTime,
PriceEffectiveTime = priceEffectiveTime,
Rules = rulesSnapshot,
Price = priceSnapshot
};
}
private async Task UpsertReceivableFeesAsync(
Order order,
ProductSnapshotBinding snapshotBinding,
decimal chargeableWeight,
IReadOnlyList<string> zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
string chargingStage)
{
var fees = ParcelBillingCalculator.CalculateReceivableFees(
snapshotBinding.Price,
snapshotBinding.PriceVersion,
snapshotBinding.PriceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
customerGroup,
chargingStage);
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledReceivablePriceItems(snapshotBinding.Price))
{
throw new BusinessException("产品卖价快照未配置或未匹配到有效价格");
}
if (fees.Count > 0)
{
await OrderRepository.UpsertFeesAsync(order.Id, fees);
}
}
private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product) private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
{ {
var snapshot = new SalesProductRulesPublishSnapshotDto var snapshot = new SalesProductRulesPublishSnapshotDto
@ -340,7 +440,6 @@ public partial class OrderService
CostItem = price.CostItem, CostItem = price.CostItem,
CostUnit = price.CostUnit, CostUnit = price.CostUnit,
Currency = price.Currency, Currency = price.Currency,
ChargingTime = price.ChargingTime,
ChargingCondition = price.ChargingCondition, ChargingCondition = price.ChargingCondition,
Factors = factorMap.TryGetValue(price.Id, out var factors) Factors = factorMap.TryGetValue(price.Id, out var factors)
? factors.Select(MapPriceFactorOutput).ToList() ? factors.Select(MapPriceFactorOutput).ToList()
@ -738,6 +837,26 @@ public partial class OrderService
} }
} }
private static void SyncReceiveToShipAndDownstream(List<OrderWeight> weights, OrderWeight receive)
{
foreach (var type in new[] { OrderWeightTypeCodes.Ship, OrderWeightTypeCodes.Downstream })
{
var row = weights.FirstOrDefault(x => x.Type == type);
if (row == null)
{
continue;
}
row.Length = receive.Length;
row.Width = receive.Width;
row.Height = receive.Height;
row.ActualWeight = receive.ActualWeight;
row.VolumetricWeight = receive.VolumetricWeight;
row.ChargeableWeight = receive.ChargeableWeight;
row.Cubic = receive.Cubic;
}
}
private static Order BuildDraftOrderEntity( private static Order BuildDraftOrderEntity(
OrderParcelCreateInput input, OrderParcelCreateInput input,
SalesProduct product, SalesProduct product,
@ -908,6 +1027,122 @@ public partial class OrderService
await OrderRepository.UpdateAsync(order); await OrderRepository.UpdateAsync(order);
} }
/// <summary>
/// 入库收货后:覆盖 receive 件重尺、重算计费重,写入 inbound 阶段应收,并同步 ship/downstream 重量行。
/// </summary>
internal async Task ApplyReceiveBillingByWaybillAsync(
string waybillNo,
decimal? length,
decimal? width,
decimal? height,
decimal? actualWeight)
{
if (string.IsNullOrWhiteSpace(waybillNo))
{
return;
}
if (!length.HasValue || !width.HasValue || !height.HasValue || !actualWeight.HasValue
|| length <= 0 || width <= 0 || height <= 0 || actualWeight <= 0)
{
return;
}
var order = await OrderRepository.GetByServiceNoAsync(waybillNo.Trim());
if (order == null || string.IsNullOrWhiteSpace(order.ProductCode))
{
return;
}
if (order.OrderStatus is not (OrderStatusCodes.Predeclared or OrderStatusCodes.Received))
{
return;
}
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
if (product == null)
{
return;
}
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
.OrderBy(x => x.Id)
.ToList();
var receive = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Receive);
if (receive == null)
{
return;
}
receive.Length = length;
receive.Width = width;
receive.Height = height;
receive.ActualWeight = actualWeight;
receive.Cubic = ParcelBillingCalculator.CalculateCubic(length.Value, width.Value, height.Value);
ProductSnapshotBinding snapshotBinding;
try
{
snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
}
catch (BusinessException)
{
return;
}
try
{
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, receive);
}
catch (BusinessException)
{
return;
}
SyncReceiveToShipAndDownstream(weights, receive);
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
var recipient = recipients.FirstOrDefault();
var zoneKeys = await ResolvePriceZoneKeysAsync(
product,
snapshotBinding.Rules,
snapshotBinding.Price,
order.ConsigneeCountry ?? string.Empty,
recipient?.Province,
recipient?.City,
recipient?.ZipCode);
if (zoneKeys == null || zoneKeys.Count == 0)
{
return;
}
var customer = order.CustomerId.HasValue
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
: null;
try
{
await UpsertReceivableFeesAsync(
order,
snapshotBinding,
receive.ChargeableWeight ?? 0,
zoneKeys,
order.CargoAttribute ?? CargoAttributeCodes.General,
customer?.CustomerGroup,
OrderFeeChargingStageCodes.Inbound);
}
catch (BusinessException)
{
return;
}
order.OrderStatus = OrderStatusCodes.Received;
order.ReceiveTime = DateTime.Now;
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
}
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order) private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
{ {
var mapped = (await MapOrderOutputsAsync([order])).First(); var mapped = (await MapOrderOutputsAsync([order])).First();

View File

@ -171,7 +171,7 @@ public partial class OrderService
} }
/// <summary> /// <summary>
/// 查价试算:按国家+邮编匹配分区,在卖价快照中匹配明细(忽略收费时机)。 /// 查价试算:按国家+邮编匹配分区,在卖价快照中匹配明细。
/// </summary> /// </summary>
private async Task<PriceCheckAttemptResult> TryComputePriceCheckQuoteAsync( private async Task<PriceCheckAttemptResult> TryComputePriceCheckQuoteAsync(
SalesProduct product, SalesProduct product,

View File

@ -40,7 +40,7 @@ public partial class OrderService : ApplicationService
/// 3. 按接口约定返回列表结果。 /// 3. 按接口约定返回列表结果。
/// </remarks> /// </remarks>
[HttpPost("/order/list")] [HttpPost("/order/list")]
public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync(OrderPaginatedInput input) public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync([FromBody] OrderPaginatedInput input)
{ {
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository, CustomerRepository,
@ -204,7 +204,7 @@ public partial class OrderService : ApplicationService
/// 3. 以文件形式返回结果。 /// 3. 以文件形式返回结果。
/// </remarks> /// </remarks>
[HttpPost("/order/export")] [HttpPost("/order/export")]
public async Task<OrderExportOutput> ExportAsync(OrderExportInput input) public async Task<OrderExportOutput> ExportAsync([FromBody] OrderExportInput input)
{ {
await CleanupExpiredFilesAsync(GetOrderExportDirectory()); await CleanupExpiredFilesAsync(GetOrderExportDirectory());
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
@ -473,7 +473,9 @@ public partial class OrderService : ApplicationService
{ {
status = status?.Trim(); status = status?.Trim();
keyword = keyword?.Trim(); keyword = keyword?.Trim();
var hasStatusFilter = !string.IsNullOrWhiteSpace(status) && status != "all"; var statusValue = !string.IsNullOrWhiteSpace(status) && !string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)
? status
: null;
var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword); var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword);
var hasCustomerFilter = customerId.HasValue; var hasCustomerFilter = customerId.HasValue;
var customerIdValue = customerId.GetValueOrDefault(); var customerIdValue = customerId.GetValueOrDefault();
@ -485,7 +487,7 @@ public partial class OrderService : ApplicationService
var end = createTimeEnd.Value; var end = createTimeEnd.Value;
return order => return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) && (!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) && (statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter || (!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) || order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) || (order.ClientNo ?? string.Empty).Contains(keyword!) ||
@ -499,7 +501,7 @@ public partial class OrderService : ApplicationService
var start = createTimeStart.Value; var start = createTimeStart.Value;
return order => return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) && (!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) && (statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter || (!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) || order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) || (order.ClientNo ?? string.Empty).Contains(keyword!) ||
@ -512,7 +514,7 @@ public partial class OrderService : ApplicationService
var end = createTimeEnd.Value; var end = createTimeEnd.Value;
return order => return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) && (!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) && (statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter || (!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) || order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) || (order.ClientNo ?? string.Empty).Contains(keyword!) ||
@ -522,7 +524,7 @@ public partial class OrderService : ApplicationService
return order => return order =>
(!hasCustomerFilter || order.CustomerId == customerIdValue) && (!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) && (statusValue == null || order.OrderStatus == statusValue) &&
(!hasKeywordFilter || (!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) || order.ServiceNo.Contains(keyword!) ||
(order.ClientNo ?? string.Empty).Contains(keyword!) || (order.ClientNo ?? string.Empty).Contains(keyword!) ||

View File

@ -353,23 +353,21 @@ internal static class ParcelBillingCalculator
decimal chargeableWeight, decimal chargeableWeight,
IReadOnlyList<string> zoneKeys, IReadOnlyList<string> zoneKeys,
string cargoAttribute, string cargoAttribute,
CustomerGroup? customerGroup) CustomerGroup? customerGroup) =>
{ CalculateReceivableFees(
var items = priceSnapshot.Prices.Select(p => new BillingPriceItem( priceSnapshot,
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
return CalculateForecastFees(
items,
OrderFeeTypeCodes.Receivable,
priceVersion, priceVersion,
priceEffectiveTime, priceEffectiveTime,
chargeableWeight, chargeableWeight,
zoneKeys, zoneKeys,
cargoAttribute, cargoAttribute,
customerGroup, customerGroup,
onlyForecastChargingTime: false); OrderFeeChargingStageCodes.Forecast);
}
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) => public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
HasEnabledReceivablePriceItems(priceSnapshot);
public static bool HasEnabledReceivablePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
priceSnapshot.Prices.Any(x => x.Status == 1); priceSnapshot.Prices.Any(x => x.Status == 1);
/// <summary> /// <summary>
@ -510,14 +508,15 @@ internal static class ParcelBillingCalculator
string zoneName, string zoneName,
string cargoAttribute, string cargoAttribute,
CustomerGroup? customerGroup) => CustomerGroup? customerGroup) =>
CalculateForecastReceivableFees( CalculateReceivableFees(
priceSnapshot, priceSnapshot,
priceVersion, priceVersion,
priceEffectiveTime, priceEffectiveTime,
chargeableWeight, chargeableWeight,
new[] { zoneName }, new[] { zoneName },
cargoAttribute, cargoAttribute,
customerGroup); customerGroup,
OrderFeeChargingStageCodes.Forecast);
public static List<OrderFee> CalculateForecastReceivableFees( public static List<OrderFee> CalculateForecastReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot, SalesProductPricePublishSnapshotDto priceSnapshot,
@ -526,20 +525,67 @@ internal static class ParcelBillingCalculator
decimal chargeableWeight, decimal chargeableWeight,
IReadOnlyList<string> zoneKeys, IReadOnlyList<string> zoneKeys,
string cargoAttribute, string cargoAttribute,
CustomerGroup? customerGroup) CustomerGroup? customerGroup) =>
{ CalculateReceivableFees(
var items = priceSnapshot.Prices.Select(p => new BillingPriceItem( priceSnapshot,
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
return CalculateForecastFees(
items,
OrderFeeTypeCodes.Receivable,
priceVersion, priceVersion,
priceEffectiveTime, priceEffectiveTime,
chargeableWeight, chargeableWeight,
zoneKeys, zoneKeys,
cargoAttribute, cargoAttribute,
customerGroup, customerGroup,
onlyForecastChargingTime: true); OrderFeeChargingStageCodes.Forecast);
/// <summary>
/// 应收费用:卖价全部启用项参与;ChargingStage 由件重尺阶段决定。
/// </summary>
public static List<OrderFee> CalculateReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList<string> zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
string chargingStage)
{
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
p.Status, p.CostItem, p.Currency, p.Details));
return CalculateReceivableFeesFromItems(
items,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
customerGroup,
chargingStage);
}
/// <summary>
/// 应付费用:买价全部启用项参与;ChargingStage 由件重尺阶段决定。
/// </summary>
public static List<OrderFee> CalculatePayableFees(
IReadOnlyList<ServiceChannelPriceListOutput> prices,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList<string> zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
string chargingStage)
{
var items = prices.Select(p => new PayableBillingPriceItem(
p.Status, p.CostItem, p.Currency, p.Details));
return CalculatePayableFeesFromItems(
items,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
customerGroup,
chargingStage);
} }
/// <summary> /// <summary>
@ -552,44 +598,25 @@ internal static class ParcelBillingCalculator
decimal chargeableWeight, decimal chargeableWeight,
string zoneName, string zoneName,
string cargoAttribute, string cargoAttribute,
CustomerGroup? customerGroup) CustomerGroup? customerGroup) =>
{ CalculatePayableFees(
var items = prices.Select(p => new BillingPriceItem( prices,
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
return CalculateForecastFees(
items,
OrderFeeTypeCodes.Payable,
priceVersion, priceVersion,
priceEffectiveTime, priceEffectiveTime,
chargeableWeight, chargeableWeight,
new[] { zoneName }, new[] { zoneName },
cargoAttribute, cargoAttribute,
customerGroup, customerGroup,
onlyForecastChargingTime: true); OrderFeeChargingStageCodes.Forecast);
}
public static bool HasEnabledForecastPriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) => public static bool HasEnabledForecastPriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
priceSnapshot.Prices.Any(x => x.Status == 1 && IsForecastChargingTime(x.ChargingTime)); HasEnabledReceivablePriceItems(priceSnapshot);
public static bool HasEnabledPayablePriceItems(IReadOnlyList<ServiceChannelPriceListOutput> prices) =>
prices.Any(x => x.Status == 1);
public static bool HasEnabledForecastPayablePriceItems(IReadOnlyList<ServiceChannelPriceListOutput> prices) => public static bool HasEnabledForecastPayablePriceItems(IReadOnlyList<ServiceChannelPriceListOutput> prices) =>
prices.Any(x => x.Status == 1 && IsForecastChargingTime(x.ChargingTime)); HasEnabledPayablePriceItems(prices);
public static string NormalizeChargingTime(string? chargingTime)
{
var value = chargingTime?.Trim();
if (string.IsNullOrWhiteSpace(value))
{
return OrderFeeChargingStageCodes.Forecast;
}
return value switch
{
"预报" => OrderFeeChargingStageCodes.Forecast,
"入库" => OrderFeeChargingStageCodes.Inbound,
"出库" => OrderFeeChargingStageCodes.Outbound,
_ => value.ToLowerInvariant()
};
}
public static string GetCargoAttributeLabel(string? cargoAttribute) => cargoAttribute switch public static string GetCargoAttributeLabel(string? cargoAttribute) => cargoAttribute switch
{ {
@ -662,25 +689,19 @@ internal static class ParcelBillingCalculator
Cubic = CalculateCubic(context.Length, context.Width, context.Height) Cubic = CalculateCubic(context.Length, context.Width, context.Height)
}; };
private static List<OrderFee> CalculateForecastFees( private static List<OrderFee> CalculateReceivableFeesFromItems(
IEnumerable<BillingPriceItem> priceItems, IEnumerable<ReceivableBillingPriceItem> priceItems,
string feeType,
string priceVersion, string priceVersion,
DateTime priceEffectiveTime, DateTime priceEffectiveTime,
decimal chargeableWeight, decimal chargeableWeight,
IReadOnlyList<string> zoneKeys, IReadOnlyList<string> zoneKeys,
string cargoAttribute, string cargoAttribute,
CustomerGroup? customerGroup, CustomerGroup? customerGroup,
bool onlyForecastChargingTime) string chargingStage)
{ {
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 (onlyForecastChargingTime && !IsForecastChargingTime(priceItem.ChargingTime))
{
continue;
}
var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight); var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight);
if (detail == null) if (detail == null)
{ {
@ -696,31 +717,71 @@ internal static class ParcelBillingCalculator
customerGroup); customerGroup);
fees.Add(new OrderFee fees.Add(new OrderFee
{ {
Type = feeType, Type = OrderFeeTypeCodes.Receivable,
CostItem = priceItem.CostItem, CostItem = priceItem.CostItem,
Amount = amount, Amount = amount,
Currency = priceItem.Currency, Currency = priceItem.Currency,
PriceNo = priceVersion, PriceNo = priceVersion,
PriceEffectiveTime = priceEffectiveTime, PriceEffectiveTime = priceEffectiveTime,
ChargingStage = OrderFeeChargingStageCodes.Forecast ChargingStage = chargingStage
}); });
} }
return fees; return fees;
} }
private readonly record struct BillingPriceItem( private static List<OrderFee> CalculatePayableFeesFromItems(
IEnumerable<PayableBillingPriceItem> priceItems,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList<string> zoneKeys,
string cargoAttribute,
CustomerGroup? customerGroup,
string chargingStage)
{
var fees = new List<OrderFee>();
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
{
var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight);
if (detail == null)
{
continue;
}
var amount = CalculateDetailAmount(
priceItem.Details,
detail,
zoneKeys,
cargoAttribute,
chargeableWeight,
customerGroup);
fees.Add(new OrderFee
{
Type = OrderFeeTypeCodes.Payable,
CostItem = priceItem.CostItem,
Amount = amount,
Currency = priceItem.Currency,
PriceNo = priceVersion,
PriceEffectiveTime = priceEffectiveTime,
ChargingStage = chargingStage
});
}
return fees;
}
private readonly record struct ReceivableBillingPriceItem(
byte Status, byte Status,
string? ChargingTime,
string CostItem, string CostItem,
string Currency, string Currency,
List<ChannelPriceDetailOutput> Details); List<ChannelPriceDetailOutput> Details);
private static bool IsForecastChargingTime(string? chargingTime) private readonly record struct PayableBillingPriceItem(
{ byte Status,
var normalized = NormalizeChargingTime(chargingTime); string CostItem,
return normalized is OrderFeeChargingStageCodes.Forecast or ""; string Currency,
} List<ChannelPriceDetailOutput> Details);
private static ChannelPriceDetailOutput? MatchPriceDetail( private static ChannelPriceDetailOutput? MatchPriceDetail(
IReadOnlyList<ChannelPriceDetailOutput> details, IReadOnlyList<ChannelPriceDetailOutput> details,

View File

@ -19,7 +19,6 @@ public static class PublishSnapshotSlimmer
CostItem = price.CostItem, CostItem = price.CostItem,
CostUnit = price.CostUnit, CostUnit = price.CostUnit,
Currency = price.Currency, Currency = price.Currency,
ChargingTime = price.ChargingTime,
ChargingCondition = price.ChargingCondition, ChargingCondition = price.ChargingCondition,
Factors = price.Factors, Factors = price.Factors,
Details = price.Details Details = price.Details

View File

@ -21,7 +21,5 @@ public class SalesProductPrice : OmsAuditedEntity
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
public string? ChargingTime { get; set; }
public string? ChargingCondition { get; set; } public string? ChargingCondition { get; set; }
} }

View File

@ -21,7 +21,5 @@ public class ServiceChannelPrice : OmsAuditedEntity
public string Currency { get; set; } = string.Empty; public string Currency { get; set; } = string.Empty;
public string? ChargingTime { get; set; }
public string? ChargingCondition { get; set; } public string? ChargingCondition { get; set; }
} }

View File

@ -52,6 +52,12 @@ public interface ISalesProductRepository : IRepository<SalesProduct>
Task<SalesProductPublishRecord?> GetPublishRecordByIdAsync(int id, CancellationToken cancellationToken = default); Task<SalesProductPublishRecord?> GetPublishRecordByIdAsync(int id, CancellationToken cancellationToken = default);
Task<SalesProductPublishRecord?> GetPublishRecordByVersionAsync(
int salesProductId,
string operatorType,
string version,
CancellationToken cancellationToken = default);
Task<SalesProductPublishRecord?> GetEffectivePublishRecordAsync( Task<SalesProductPublishRecord?> GetEffectivePublishRecordAsync(
int salesProductId, int salesProductId,
string operatorType, string operatorType,

View File

@ -195,6 +195,28 @@ public class SalesProductRepository : OmsSqlSugarRepository<SalesProduct>, ISale
return await db.Queryable<SalesProductPublishRecord>().FirstAsync(x => x.Id == id, cancellationToken); return await db.Queryable<SalesProductPublishRecord>().FirstAsync(x => x.Id == id, cancellationToken);
} }
public async Task<SalesProductPublishRecord?> GetPublishRecordByVersionAsync(
int salesProductId,
string operatorType,
string version,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(version))
{
return null;
}
var db = await GetDbContextAsync(cancellationToken);
var records = await db.Queryable<SalesProductPublishRecord>()
.Where(x => x.SalesProductId == salesProductId
&& x.OperatorType == operatorType
&& x.Version == version)
.OrderBy(x => x.PublishTime, SqlSugar.OrderByType.Desc)
.Take(1)
.ToListAsync(cancellationToken);
return records.FirstOrDefault();
}
public async Task<SalesProductPublishRecord?> GetEffectivePublishRecordAsync( public async Task<SalesProductPublishRecord?> GetEffectivePublishRecordAsync(
int salesProductId, int salesProductId,
string operatorType, string operatorType,

View File

@ -89,10 +89,10 @@ public class ParcelBillingCalculatorDemoTests
var (priceVersion, effectiveTime) = ParcelBillingTestData.SnapshotMeta(); var (priceVersion, effectiveTime) = ParcelBillingTestData.SnapshotMeta();
var receivableSnapshot = ParcelBillingTestData.BuildReceivableSnapshot( var receivableSnapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
"预报", 1, "freight", 1, "freight",
ParcelBillingTestData.ContinueFreightDetail()), ParcelBillingTestData.ContinueFreightDetail()),
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
"预报", 1, "操作费", 1, "操作费",
ParcelBillingTestData.FixedPriceDetail(5M))); ParcelBillingTestData.FixedPriceDetail(5M)));
var receivableFees = ParcelBillingCalculator.CalculateForecastReceivableFees( var receivableFees = ParcelBillingCalculator.CalculateForecastReceivableFees(
@ -119,7 +119,7 @@ public class ParcelBillingCalculatorDemoTests
// —— 5. 应付(买价)—— // —— 5. 应付(买价)——
var payablePrices = ParcelBillingTestData.BuildPayablePrices( var payablePrices = ParcelBillingTestData.BuildPayablePrices(
ParcelBillingTestData.PayablePriceItem( ParcelBillingTestData.PayablePriceItem(
"预报", 1, "freight", 1, "freight",
ParcelBillingTestData.ContinueFreightDetail( ParcelBillingTestData.ContinueFreightDetail(
firstWeight: 1M, firstPrice: 12M, continueUnit: 0.5M, continuePrice: 4M))); firstWeight: 1M, firstPrice: 12M, continueUnit: 0.5M, continuePrice: 4M)));

View File

@ -61,7 +61,6 @@ public class ParcelBillingCalculatorTests
{ {
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
"预报",
1, 1,
"freight", "freight",
ParcelBillingTestData.ContinueFreightDetail())); ParcelBillingTestData.ContinueFreightDetail()));
@ -83,30 +82,50 @@ public class ParcelBillingCalculatorTests
fees[0].PriceNo.Should().Be(_meta.PriceVersion); fees[0].PriceNo.Should().Be(_meta.PriceVersion);
} }
[Theory] [Fact]
[InlineData("入库")] public void CalculateReceivableFees_ShouldIncludeAllEnabledPriceItems()
[InlineData("出库")]
[InlineData("inbound")]
[InlineData("outbound")]
public void CalculateForecastReceivableFees_ShouldSkipNonForecastChargingTime(string chargingTime)
{ {
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
chargingTime,
1, 1,
"freight", "freight",
ParcelBillingTestData.ContinueFreightDetail())); ParcelBillingTestData.ContinueFreightDetail()));
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( var fees = ParcelBillingCalculator.CalculateReceivableFees(
snapshot, snapshot,
_meta.PriceVersion, _meta.PriceVersion,
_meta.EffectiveTime, _meta.EffectiveTime,
1.2M, 1.2M,
Zone, new[] { Zone },
"general", "general",
null); null,
OrderFeeChargingStageCodes.Forecast);
fees.Should().BeEmpty(); fees.Should().ContainSingle();
fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Forecast);
}
[Fact]
public void CalculateReceivableFees_InboundStage_ShouldSetChargingStage()
{
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem(
1,
"freight",
ParcelBillingTestData.ContinueFreightDetail()));
var fees = ParcelBillingCalculator.CalculateReceivableFees(
snapshot,
_meta.PriceVersion,
_meta.EffectiveTime,
1.2M,
new[] { Zone },
"general",
null,
OrderFeeChargingStageCodes.Inbound);
fees.Should().ContainSingle();
fees[0].ChargingStage.Should().Be(OrderFeeChargingStageCodes.Inbound);
} }
[Fact] [Fact]
@ -114,12 +133,10 @@ public class ParcelBillingCalculatorTests
{ {
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
"预报",
1, 1,
"freight", "freight",
ParcelBillingTestData.ContinueFreightDetail()), ParcelBillingTestData.ContinueFreightDetail()),
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
"预报",
1, 1,
"操作费", "操作费",
ParcelBillingTestData.FixedPriceDetail(5M))); ParcelBillingTestData.FixedPriceDetail(5M)));
@ -150,7 +167,7 @@ public class ParcelBillingCalculatorTests
detail.CustomerGroupValues = new Dictionary<string, decimal> { ["客户组1"] = 15M }; detail.CustomerGroupValues = new Dictionary<string, decimal> { ["客户组1"] = 15M };
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail)); ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail));
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
snapshot, snapshot,
@ -170,7 +187,6 @@ public class ParcelBillingCalculatorTests
{ {
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem( ParcelBillingTestData.ReceivablePriceItem(
"outbound",
1, 1,
"freight", "freight",
ParcelBillingTestData.TotalTierDetail("3", 0.5M, 10M, weightInterval: "(0-0.5]"), ParcelBillingTestData.TotalTierDetail("3", 0.5M, 10M, weightInterval: "(0-0.5]"),
@ -200,7 +216,7 @@ public class ParcelBillingCalculatorTests
} }
[Fact] [Fact]
public void CalculateQuoteReceivableFees_ShouldSupportOutboundTotalPriceTiers() public void CalculateQuoteReceivableFees_ShouldSupportTotalPriceTiers()
{ {
var details = new[] var details = new[]
{ {
@ -210,7 +226,7 @@ public class ParcelBillingCalculatorTests
ParcelBillingTestData.TotalTierDetail("3", 2M, 14M), ParcelBillingTestData.TotalTierDetail("3", 2M, 14M),
}; };
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem("outbound", 1, "freight", details)); ParcelBillingTestData.ReceivablePriceItem(1, "freight", details));
var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees( var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees(
snapshot, snapshot,
@ -232,7 +248,7 @@ public class ParcelBillingCalculatorTests
detail.CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 12M }; detail.CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 12M };
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail)); ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail));
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees( var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
snapshot, snapshot,
@ -248,12 +264,13 @@ public class ParcelBillingCalculatorTests
} }
[Fact] [Fact]
public void HasEnabledForecastPriceItems_ShouldDetectForecastEnabledItems() public void HasEnabledReceivablePriceItems_ShouldDetectAnyEnabledItems()
{ {
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem("入库", 1, "freight", ParcelBillingTestData.ContinueFreightDetail()), ParcelBillingTestData.ReceivablePriceItem(1, "freight", ParcelBillingTestData.ContinueFreightDetail()),
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", ParcelBillingTestData.ContinueFreightDetail())); ParcelBillingTestData.ReceivablePriceItem(0, "freight", ParcelBillingTestData.ContinueFreightDetail()));
ParcelBillingCalculator.HasEnabledReceivablePriceItems(snapshot).Should().BeTrue();
ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshot).Should().BeTrue(); ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshot).Should().BeTrue();
} }
@ -271,7 +288,6 @@ public class ParcelBillingCalculatorTests
{ {
var prices = ParcelBillingTestData.BuildPayablePrices( var prices = ParcelBillingTestData.BuildPayablePrices(
ParcelBillingTestData.PayablePriceItem( ParcelBillingTestData.PayablePriceItem(
"预报",
1, 1,
"freight", "freight",
ParcelBillingTestData.ContinueFreightDetail( ParcelBillingTestData.ContinueFreightDetail(
@ -300,7 +316,6 @@ public class ParcelBillingCalculatorTests
{ {
var prices = ParcelBillingTestData.BuildPayablePrices( var prices = ParcelBillingTestData.BuildPayablePrices(
ParcelBillingTestData.PayablePriceItem( ParcelBillingTestData.PayablePriceItem(
"预报",
0, 0,
"freight", "freight",
ParcelBillingTestData.ContinueFreightDetail())); ParcelBillingTestData.ContinueFreightDetail()));
@ -315,7 +330,7 @@ public class ParcelBillingCalculatorTests
null); null);
fees.Should().BeEmpty(); fees.Should().BeEmpty();
ParcelBillingCalculator.HasEnabledForecastPayablePriceItems(prices).Should().BeFalse(); ParcelBillingCalculator.HasEnabledPayablePriceItems(prices).Should().BeFalse();
} }
[Fact] [Fact]
@ -323,9 +338,9 @@ public class ParcelBillingCalculatorTests
{ {
var detail = ParcelBillingTestData.ContinueFreightDetail(); var detail = ParcelBillingTestData.ContinueFreightDetail();
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot( var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail)); ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail));
var payablePrices = ParcelBillingTestData.BuildPayablePrices( var payablePrices = ParcelBillingTestData.BuildPayablePrices(
ParcelBillingTestData.PayablePriceItem("预报", 1, "freight", detail)); ParcelBillingTestData.PayablePriceItem(1, "freight", detail));
var receivable = ParcelBillingCalculator.CalculateForecastReceivableFees( var receivable = ParcelBillingCalculator.CalculateForecastReceivableFees(
snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null); snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null);

View File

@ -9,7 +9,7 @@ using Oms.Domain.OrdersAggregate.Consts;
namespace Oms.Tests; namespace Oms.Tests;
/// <summary> /// <summary>
/// 验证查价卖价快照匹配:忽略收费时机、unit 公式、重量左开右闭区间。 /// 验证查价卖价快照匹配:unit 公式、重量左开右闭区间。
/// </summary> /// </summary>
public class PriceCheckSnapshotVerificationTests public class PriceCheckSnapshotVerificationTests
{ {
@ -23,7 +23,6 @@ public class PriceCheckSnapshotVerificationTests
Id = 1, Id = 1,
Status = 1, Status = 1,
CostItem = "freight", CostItem = "freight",
ChargingTime = "outbound",
Details = Details =
[ [
Detail(24, 1, "total", "3", "0,0.5", 11), Detail(24, 1, "total", "3", "0,0.5", 11),
@ -37,7 +36,6 @@ public class PriceCheckSnapshotVerificationTests
Id = 2, Id = 2,
Status = 1, Status = 1,
CostItem = "handling", CostItem = "handling",
ChargingTime = "outbound",
Details = [Detail(28, 2, "total", "3", "0,10", 20)] Details = [Detail(28, 2, "total", "3", "0,10", 20)]
}) })
] ]

View File

@ -24,7 +24,6 @@ internal static class ParcelBillingTestData
params ServiceChannelPriceListOutput[] prices) => prices.ToList(); params ServiceChannelPriceListOutput[] prices) => prices.ToList();
public static SalesProductPriceListOutput ReceivablePriceItem( public static SalesProductPriceListOutput ReceivablePriceItem(
string chargingTime,
byte status, byte status,
string costItem, string costItem,
params ChannelPriceDetailOutput[] details) => params ChannelPriceDetailOutput[] details) =>
@ -33,12 +32,10 @@ internal static class ParcelBillingTestData
Status = status, Status = status,
CostItem = costItem, CostItem = costItem,
Currency = "CNY", Currency = "CNY",
ChargingTime = chargingTime,
Details = details.ToList() Details = details.ToList()
}; };
public static ServiceChannelPriceListOutput PayablePriceItem( public static ServiceChannelPriceListOutput PayablePriceItem(
string chargingTime,
byte status, byte status,
string costItem, string costItem,
params ChannelPriceDetailOutput[] details) => params ChannelPriceDetailOutput[] details) =>
@ -47,7 +44,6 @@ internal static class ParcelBillingTestData
Status = status, Status = status,
CostItem = costItem, CostItem = costItem,
Currency = "CNY", Currency = "CNY",
ChargingTime = chargingTime,
Details = details.ToList() Details = details.ToList()
}; };