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:
parent
91ba6d5d59
commit
5e26de6ff3
2
db.sql
2
db.sql
@ -336,7 +336,6 @@ CREATE TABLE `t_sales_product_price` (
|
||||
`CostItem` VARCHAR(64) NOT NULL COMMENT '费用项',
|
||||
`CostUnit` VARCHAR(16) NOT NULL COMMENT '费用单位:kg/票/件',
|
||||
`Currency` VARCHAR(16) NOT NULL COMMENT '币种:RMB/USD/EUR',
|
||||
`ChargingTime` VARCHAR(32) NULL COMMENT '收费时机:预报/出库/签收',
|
||||
`ChargingCondition` VARCHAR(64) NULL COMMENT '收费条件',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
@ -466,7 +465,6 @@ CREATE TABLE `t_service_channel_price` (
|
||||
`CostItem` VARCHAR(64) NOT NULL COMMENT '费用项',
|
||||
`CostUnit` VARCHAR(16) NOT NULL COMMENT '费用单位:kg/票/件',
|
||||
`Currency` VARCHAR(16) NOT NULL COMMENT '币种:RMB/USD/EUR',
|
||||
`ChargingTime` VARCHAR(32) NULL COMMENT '收费时机:预报/出库/签收',
|
||||
`ChargingCondition` VARCHAR(64) NULL COMMENT '收费条件',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
|
||||
3
sql/migrate_drop_charging_time.sql
Normal file
3
sql/migrate_drop_charging_time.sql
Normal 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`;
|
||||
@ -45,11 +45,6 @@ public class SalesProductPriceListOutput
|
||||
/// </summary>
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计费时机。
|
||||
/// </summary>
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费条件。
|
||||
/// </summary>
|
||||
|
||||
@ -47,11 +47,6 @@ public class SalesProductPriceSaveInput
|
||||
/// </summary>
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计费时机。
|
||||
/// </summary>
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费条件。
|
||||
/// </summary>
|
||||
|
||||
@ -45,11 +45,6 @@ public class ServiceChannelPriceListOutput
|
||||
/// </summary>
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计费时机。
|
||||
/// </summary>
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费条件。
|
||||
/// </summary>
|
||||
|
||||
@ -47,11 +47,6 @@ public class ServiceChannelPriceSaveInput
|
||||
/// </summary>
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 计费时机。
|
||||
/// </summary>
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费条件。
|
||||
/// </summary>
|
||||
|
||||
@ -6,22 +6,26 @@ namespace Oms.Application.Dtos.Orders;
|
||||
public class OrderExportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态:all 或 OrderStatusCodes 枚举值。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键词。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("keyword")]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建TimeStart。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("createTimeStart")]
|
||||
public DateTime? CreateTimeStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建TimeEnd。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
|
||||
public DateTime? CreateTimeEnd { get; set; }
|
||||
}
|
||||
|
||||
@ -8,30 +8,36 @@ public class OrderPaginatedInput
|
||||
/// <summary>
|
||||
/// 页码,从 1 开始。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("page")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("pageSize")]
|
||||
public int PageSize { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态:all 或 OrderStatusCodes 枚举值。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键词。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("keyword")]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建TimeStart。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("createTimeStart")]
|
||||
public DateTime? CreateTimeStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建TimeEnd。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("createTimeEnd")]
|
||||
public DateTime? CreateTimeEnd { get; set; }
|
||||
}
|
||||
|
||||
@ -84,8 +84,6 @@ public class SalesProductPriceSnapshotItemDto
|
||||
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
public string? ChargingCondition { get; set; }
|
||||
|
||||
public List<ChannelPriceFactorOutput> Factors { get; set; } = [];
|
||||
|
||||
@ -84,7 +84,6 @@ public partial class SalesProductService
|
||||
CostItem = price.CostItem,
|
||||
CostUnit = price.CostUnit,
|
||||
Currency = price.Currency,
|
||||
ChargingTime = price.ChargingTime,
|
||||
ChargingCondition = price.ChargingCondition,
|
||||
Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [],
|
||||
Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [],
|
||||
@ -423,7 +422,6 @@ public partial class SalesProductService
|
||||
CostItem = output.CostItem,
|
||||
CostUnit = output.CostUnit,
|
||||
Currency = output.Currency,
|
||||
ChargingTime = output.ChargingTime,
|
||||
ChargingCondition = output.ChargingCondition,
|
||||
Factors = output.Factors.Select(item => new ChannelPriceFactorInput
|
||||
{
|
||||
@ -455,7 +453,6 @@ public partial class SalesProductService
|
||||
CostItem = output.CostItem,
|
||||
CostUnit = output.CostUnit,
|
||||
Currency = output.Currency,
|
||||
ChargingTime = output.ChargingTime,
|
||||
ChargingCondition = output.ChargingCondition,
|
||||
Factors = output.Factors.Select(item => new ChannelPriceFactorInput
|
||||
{
|
||||
|
||||
@ -197,7 +197,6 @@ public partial class SalesProductService : ApplicationService
|
||||
CostItem = input.CostItem,
|
||||
CostUnit = input.CostUnit,
|
||||
Currency = input.Currency,
|
||||
ChargingTime = input.ChargingTime,
|
||||
ChargingCondition = input.ChargingCondition
|
||||
},
|
||||
BuildSalesProductPriceFactors(input.Factors),
|
||||
@ -230,7 +229,6 @@ public partial class SalesProductService : ApplicationService
|
||||
price.CostItem = input.CostItem;
|
||||
price.CostUnit = input.CostUnit;
|
||||
price.Currency = input.Currency;
|
||||
price.ChargingTime = input.ChargingTime;
|
||||
price.ChargingCondition = input.ChargingCondition;
|
||||
await SalesProductRepository.UpdatePriceAsync(
|
||||
price,
|
||||
@ -308,7 +306,6 @@ public partial class SalesProductService : ApplicationService
|
||||
CostItem = priceInput.CostItem,
|
||||
CostUnit = priceInput.CostUnit,
|
||||
Currency = priceInput.Currency,
|
||||
ChargingTime = priceInput.ChargingTime,
|
||||
ChargingCondition = priceInput.ChargingCondition
|
||||
});
|
||||
factors.AddRange(BuildSalesProductPriceFactors(priceInput.Factors, currentTempId));
|
||||
|
||||
@ -136,7 +136,6 @@ public partial class ServiceChannelService
|
||||
CostItem = price.CostItem,
|
||||
CostUnit = price.CostUnit,
|
||||
Currency = price.Currency,
|
||||
ChargingTime = price.ChargingTime,
|
||||
ChargingCondition = price.ChargingCondition,
|
||||
Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [],
|
||||
Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [],
|
||||
@ -392,7 +391,6 @@ public partial class ServiceChannelService
|
||||
CostItem = output.CostItem,
|
||||
CostUnit = output.CostUnit,
|
||||
Currency = output.Currency,
|
||||
ChargingTime = output.ChargingTime,
|
||||
ChargingCondition = output.ChargingCondition,
|
||||
Factors = output.Factors.Select(item => new ChannelPriceFactorInput
|
||||
{
|
||||
|
||||
@ -284,7 +284,6 @@ public partial class ServiceChannelService : ApplicationService
|
||||
CostItem = input.CostItem,
|
||||
CostUnit = input.CostUnit,
|
||||
Currency = input.Currency,
|
||||
ChargingTime = input.ChargingTime,
|
||||
ChargingCondition = input.ChargingCondition
|
||||
},
|
||||
BuildPriceFactors(input.Factors),
|
||||
@ -317,7 +316,6 @@ public partial class ServiceChannelService : ApplicationService
|
||||
price.CostItem = input.CostItem;
|
||||
price.CostUnit = input.CostUnit;
|
||||
price.Currency = input.Currency;
|
||||
price.ChargingTime = input.ChargingTime;
|
||||
price.ChargingCondition = input.ChargingCondition;
|
||||
await ServiceChannelRepository.UpdatePriceAsync(
|
||||
price,
|
||||
@ -395,7 +393,6 @@ public partial class ServiceChannelService : ApplicationService
|
||||
CostItem = priceInput.CostItem,
|
||||
CostUnit = priceInput.CostUnit,
|
||||
Currency = priceInput.Currency,
|
||||
ChargingTime = priceInput.ChargingTime,
|
||||
ChargingCondition = priceInput.ChargingCondition
|
||||
});
|
||||
factors.AddRange(BuildPriceFactors(priceInput.Factors, currentTempId));
|
||||
|
||||
@ -2,6 +2,7 @@ using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Operations;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OperationsAggregate.Entities;
|
||||
using Oms.Domain.OperationsAggregate.Repositories;
|
||||
@ -18,6 +19,7 @@ public class InboundRecordService : ApplicationService
|
||||
private IInboundRecordRepository InboundRecordRepository => LazyServiceProvider.GetRequiredService<IInboundRecordRepository>();
|
||||
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||||
|
||||
/// <summary>
|
||||
/// 新增入库记录。
|
||||
@ -50,6 +52,12 @@ public class InboundRecordService : ApplicationService
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -102,24 +102,14 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
|
||||
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||
snapshotBinding.Price,
|
||||
snapshotBinding.PriceVersion,
|
||||
snapshotBinding.PriceEffectiveTime,
|
||||
await UpsertReceivableFeesAsync(
|
||||
order,
|
||||
snapshotBinding,
|
||||
forecast.ChargeableWeight ?? 0,
|
||||
zoneKeys,
|
||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||||
customer?.CustomerGroup);
|
||||
|
||||
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledForecastPriceItems(snapshotBinding.Price))
|
||||
{
|
||||
throw new BusinessException("产品卖价快照未配置或未匹配到有效价格");
|
||||
}
|
||||
|
||||
if (fees.Count > 0)
|
||||
{
|
||||
await OrderRepository.UpsertFeesAsync(order.Id, fees);
|
||||
}
|
||||
customer?.CustomerGroup,
|
||||
OrderFeeChargingStageCodes.Forecast);
|
||||
|
||||
order.OrderStatus = OrderStatusCodes.Predeclared;
|
||||
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)
|
||||
{
|
||||
var snapshot = new SalesProductRulesPublishSnapshotDto
|
||||
@ -340,7 +440,6 @@ public partial class OrderService
|
||||
CostItem = price.CostItem,
|
||||
CostUnit = price.CostUnit,
|
||||
Currency = price.Currency,
|
||||
ChargingTime = price.ChargingTime,
|
||||
ChargingCondition = price.ChargingCondition,
|
||||
Factors = factorMap.TryGetValue(price.Id, out var factors)
|
||||
? 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(
|
||||
OrderParcelCreateInput input,
|
||||
SalesProduct product,
|
||||
@ -908,6 +1027,122 @@ public partial class OrderService
|
||||
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)
|
||||
{
|
||||
var mapped = (await MapOrderOutputsAsync([order])).First();
|
||||
|
||||
@ -171,7 +171,7 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查价试算:按国家+邮编匹配分区,在卖价快照中匹配明细(忽略收费时机)。
|
||||
/// 查价试算:按国家+邮编匹配分区,在卖价快照中匹配明细。
|
||||
/// </summary>
|
||||
private async Task<PriceCheckAttemptResult> TryComputePriceCheckQuoteAsync(
|
||||
SalesProduct product,
|
||||
|
||||
@ -40,7 +40,7 @@ public partial class OrderService : ApplicationService
|
||||
/// 3. 按接口约定返回列表结果。
|
||||
/// </remarks>
|
||||
[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(
|
||||
CustomerRepository,
|
||||
@ -204,7 +204,7 @@ public partial class OrderService : ApplicationService
|
||||
/// 3. 以文件形式返回结果。
|
||||
/// </remarks>
|
||||
[HttpPost("/order/export")]
|
||||
public async Task<OrderExportOutput> ExportAsync(OrderExportInput input)
|
||||
public async Task<OrderExportOutput> ExportAsync([FromBody] OrderExportInput input)
|
||||
{
|
||||
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
|
||||
@ -473,7 +473,9 @@ public partial class OrderService : ApplicationService
|
||||
{
|
||||
status = status?.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 hasCustomerFilter = customerId.HasValue;
|
||||
var customerIdValue = customerId.GetValueOrDefault();
|
||||
@ -485,7 +487,7 @@ public partial class OrderService : ApplicationService
|
||||
var end = createTimeEnd.Value;
|
||||
return order =>
|
||||
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
|
||||
(!hasStatusFilter || order.OrderStatus == status) &&
|
||||
(statusValue == null || order.OrderStatus == statusValue) &&
|
||||
(!hasKeywordFilter ||
|
||||
order.ServiceNo.Contains(keyword!) ||
|
||||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
|
||||
@ -499,7 +501,7 @@ public partial class OrderService : ApplicationService
|
||||
var start = createTimeStart.Value;
|
||||
return order =>
|
||||
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
|
||||
(!hasStatusFilter || order.OrderStatus == status) &&
|
||||
(statusValue == null || order.OrderStatus == statusValue) &&
|
||||
(!hasKeywordFilter ||
|
||||
order.ServiceNo.Contains(keyword!) ||
|
||||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
|
||||
@ -512,7 +514,7 @@ public partial class OrderService : ApplicationService
|
||||
var end = createTimeEnd.Value;
|
||||
return order =>
|
||||
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
|
||||
(!hasStatusFilter || order.OrderStatus == status) &&
|
||||
(statusValue == null || order.OrderStatus == statusValue) &&
|
||||
(!hasKeywordFilter ||
|
||||
order.ServiceNo.Contains(keyword!) ||
|
||||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
|
||||
@ -522,7 +524,7 @@ public partial class OrderService : ApplicationService
|
||||
|
||||
return order =>
|
||||
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
|
||||
(!hasStatusFilter || order.OrderStatus == status) &&
|
||||
(statusValue == null || order.OrderStatus == statusValue) &&
|
||||
(!hasKeywordFilter ||
|
||||
order.ServiceNo.Contains(keyword!) ||
|
||||
(order.ClientNo ?? string.Empty).Contains(keyword!) ||
|
||||
|
||||
@ -353,23 +353,21 @@ internal static class ParcelBillingCalculator
|
||||
decimal chargeableWeight,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup)
|
||||
{
|
||||
var items = priceSnapshot.Prices.Select(p => new BillingPriceItem(
|
||||
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
|
||||
return CalculateForecastFees(
|
||||
items,
|
||||
OrderFeeTypeCodes.Receivable,
|
||||
CustomerGroup? customerGroup) =>
|
||||
CalculateReceivableFees(
|
||||
priceSnapshot,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
customerGroup,
|
||||
onlyForecastChargingTime: false);
|
||||
}
|
||||
OrderFeeChargingStageCodes.Forecast);
|
||||
|
||||
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||
HasEnabledReceivablePriceItems(priceSnapshot);
|
||||
|
||||
public static bool HasEnabledReceivablePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||
priceSnapshot.Prices.Any(x => x.Status == 1);
|
||||
|
||||
/// <summary>
|
||||
@ -510,14 +508,15 @@ internal static class ParcelBillingCalculator
|
||||
string zoneName,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup) =>
|
||||
CalculateForecastReceivableFees(
|
||||
CalculateReceivableFees(
|
||||
priceSnapshot,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
new[] { zoneName },
|
||||
cargoAttribute,
|
||||
customerGroup);
|
||||
customerGroup,
|
||||
OrderFeeChargingStageCodes.Forecast);
|
||||
|
||||
public static List<OrderFee> CalculateForecastReceivableFees(
|
||||
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||
@ -526,20 +525,67 @@ internal static class ParcelBillingCalculator
|
||||
decimal chargeableWeight,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup)
|
||||
{
|
||||
var items = priceSnapshot.Prices.Select(p => new BillingPriceItem(
|
||||
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
|
||||
return CalculateForecastFees(
|
||||
items,
|
||||
OrderFeeTypeCodes.Receivable,
|
||||
CustomerGroup? customerGroup) =>
|
||||
CalculateReceivableFees(
|
||||
priceSnapshot,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
zoneKeys,
|
||||
cargoAttribute,
|
||||
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>
|
||||
@ -552,44 +598,25 @@ internal static class ParcelBillingCalculator
|
||||
decimal chargeableWeight,
|
||||
string zoneName,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup)
|
||||
{
|
||||
var items = prices.Select(p => new BillingPriceItem(
|
||||
p.Status, p.ChargingTime, p.CostItem, p.Currency, p.Details));
|
||||
return CalculateForecastFees(
|
||||
items,
|
||||
OrderFeeTypeCodes.Payable,
|
||||
CustomerGroup? customerGroup) =>
|
||||
CalculatePayableFees(
|
||||
prices,
|
||||
priceVersion,
|
||||
priceEffectiveTime,
|
||||
chargeableWeight,
|
||||
new[] { zoneName },
|
||||
cargoAttribute,
|
||||
customerGroup,
|
||||
onlyForecastChargingTime: true);
|
||||
}
|
||||
OrderFeeChargingStageCodes.Forecast);
|
||||
|
||||
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) =>
|
||||
prices.Any(x => x.Status == 1 && IsForecastChargingTime(x.ChargingTime));
|
||||
|
||||
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()
|
||||
};
|
||||
}
|
||||
HasEnabledPayablePriceItems(prices);
|
||||
|
||||
public static string GetCargoAttributeLabel(string? cargoAttribute) => cargoAttribute switch
|
||||
{
|
||||
@ -662,25 +689,19 @@ internal static class ParcelBillingCalculator
|
||||
Cubic = CalculateCubic(context.Length, context.Width, context.Height)
|
||||
};
|
||||
|
||||
private static List<OrderFee> CalculateForecastFees(
|
||||
IEnumerable<BillingPriceItem> priceItems,
|
||||
string feeType,
|
||||
private static List<OrderFee> CalculateReceivableFeesFromItems(
|
||||
IEnumerable<ReceivableBillingPriceItem> priceItems,
|
||||
string priceVersion,
|
||||
DateTime priceEffectiveTime,
|
||||
decimal chargeableWeight,
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup,
|
||||
bool onlyForecastChargingTime)
|
||||
string chargingStage)
|
||||
{
|
||||
var fees = new List<OrderFee>();
|
||||
foreach (var priceItem in priceItems.Where(x => x.Status == 1))
|
||||
{
|
||||
if (onlyForecastChargingTime && !IsForecastChargingTime(priceItem.ChargingTime))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var detail = MatchPriceDetail(priceItem.Details, zoneKeys, cargoAttribute, chargeableWeight);
|
||||
if (detail == null)
|
||||
{
|
||||
@ -696,31 +717,71 @@ internal static class ParcelBillingCalculator
|
||||
customerGroup);
|
||||
fees.Add(new OrderFee
|
||||
{
|
||||
Type = feeType,
|
||||
Type = OrderFeeTypeCodes.Receivable,
|
||||
CostItem = priceItem.CostItem,
|
||||
Amount = amount,
|
||||
Currency = priceItem.Currency,
|
||||
PriceNo = priceVersion,
|
||||
PriceEffectiveTime = priceEffectiveTime,
|
||||
ChargingStage = OrderFeeChargingStageCodes.Forecast
|
||||
ChargingStage = chargingStage
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
string? ChargingTime,
|
||||
string CostItem,
|
||||
string Currency,
|
||||
List<ChannelPriceDetailOutput> Details);
|
||||
|
||||
private static bool IsForecastChargingTime(string? chargingTime)
|
||||
{
|
||||
var normalized = NormalizeChargingTime(chargingTime);
|
||||
return normalized is OrderFeeChargingStageCodes.Forecast or "";
|
||||
}
|
||||
private readonly record struct PayableBillingPriceItem(
|
||||
byte Status,
|
||||
string CostItem,
|
||||
string Currency,
|
||||
List<ChannelPriceDetailOutput> Details);
|
||||
|
||||
private static ChannelPriceDetailOutput? MatchPriceDetail(
|
||||
IReadOnlyList<ChannelPriceDetailOutput> details,
|
||||
|
||||
@ -19,7 +19,6 @@ public static class PublishSnapshotSlimmer
|
||||
CostItem = price.CostItem,
|
||||
CostUnit = price.CostUnit,
|
||||
Currency = price.Currency,
|
||||
ChargingTime = price.ChargingTime,
|
||||
ChargingCondition = price.ChargingCondition,
|
||||
Factors = price.Factors,
|
||||
Details = price.Details
|
||||
|
||||
@ -21,7 +21,5 @@ public class SalesProductPrice : OmsAuditedEntity
|
||||
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
public string? ChargingCondition { get; set; }
|
||||
}
|
||||
|
||||
@ -21,7 +21,5 @@ public class ServiceChannelPrice : OmsAuditedEntity
|
||||
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
public string? ChargingTime { get; set; }
|
||||
|
||||
public string? ChargingCondition { get; set; }
|
||||
}
|
||||
|
||||
@ -52,6 +52,12 @@ public interface ISalesProductRepository : IRepository<SalesProduct>
|
||||
|
||||
Task<SalesProductPublishRecord?> GetPublishRecordByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<SalesProductPublishRecord?> GetPublishRecordByVersionAsync(
|
||||
int salesProductId,
|
||||
string operatorType,
|
||||
string version,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<SalesProductPublishRecord?> GetEffectivePublishRecordAsync(
|
||||
int salesProductId,
|
||||
string operatorType,
|
||||
|
||||
@ -195,6 +195,28 @@ public class SalesProductRepository : OmsSqlSugarRepository<SalesProduct>, ISale
|
||||
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(
|
||||
int salesProductId,
|
||||
string operatorType,
|
||||
|
||||
@ -89,10 +89,10 @@ public class ParcelBillingCalculatorDemoTests
|
||||
var (priceVersion, effectiveTime) = ParcelBillingTestData.SnapshotMeta();
|
||||
var receivableSnapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
"预报", 1, "freight",
|
||||
1, "freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail()),
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
"预报", 1, "操作费",
|
||||
1, "操作费",
|
||||
ParcelBillingTestData.FixedPriceDetail(5M)));
|
||||
|
||||
var receivableFees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||
@ -119,7 +119,7 @@ public class ParcelBillingCalculatorDemoTests
|
||||
// —— 5. 应付(买价)——
|
||||
var payablePrices = ParcelBillingTestData.BuildPayablePrices(
|
||||
ParcelBillingTestData.PayablePriceItem(
|
||||
"预报", 1, "freight",
|
||||
1, "freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail(
|
||||
firstWeight: 1M, firstPrice: 12M, continueUnit: 0.5M, continuePrice: 4M)));
|
||||
|
||||
|
||||
@ -61,7 +61,6 @@ public class ParcelBillingCalculatorTests
|
||||
{
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
"预报",
|
||||
1,
|
||||
"freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail()));
|
||||
@ -83,30 +82,50 @@ public class ParcelBillingCalculatorTests
|
||||
fees[0].PriceNo.Should().Be(_meta.PriceVersion);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("入库")]
|
||||
[InlineData("出库")]
|
||||
[InlineData("inbound")]
|
||||
[InlineData("outbound")]
|
||||
public void CalculateForecastReceivableFees_ShouldSkipNonForecastChargingTime(string chargingTime)
|
||||
[Fact]
|
||||
public void CalculateReceivableFees_ShouldIncludeAllEnabledPriceItems()
|
||||
{
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
chargingTime,
|
||||
1,
|
||||
"freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail()));
|
||||
|
||||
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||
var fees = ParcelBillingCalculator.CalculateReceivableFees(
|
||||
snapshot,
|
||||
_meta.PriceVersion,
|
||||
_meta.EffectiveTime,
|
||||
1.2M,
|
||||
Zone,
|
||||
new[] { Zone },
|
||||
"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]
|
||||
@ -114,12 +133,10 @@ public class ParcelBillingCalculatorTests
|
||||
{
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
"预报",
|
||||
1,
|
||||
"freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail()),
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
"预报",
|
||||
1,
|
||||
"操作费",
|
||||
ParcelBillingTestData.FixedPriceDetail(5M)));
|
||||
@ -150,7 +167,7 @@ public class ParcelBillingCalculatorTests
|
||||
detail.CustomerGroupValues = new Dictionary<string, decimal> { ["客户组1"] = 15M };
|
||||
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail));
|
||||
ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail));
|
||||
|
||||
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||
snapshot,
|
||||
@ -170,7 +187,6 @@ public class ParcelBillingCalculatorTests
|
||||
{
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem(
|
||||
"outbound",
|
||||
1,
|
||||
"freight",
|
||||
ParcelBillingTestData.TotalTierDetail("3", 0.5M, 10M, weightInterval: "(0-0.5]"),
|
||||
@ -200,7 +216,7 @@ public class ParcelBillingCalculatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateQuoteReceivableFees_ShouldSupportOutboundTotalPriceTiers()
|
||||
public void CalculateQuoteReceivableFees_ShouldSupportTotalPriceTiers()
|
||||
{
|
||||
var details = new[]
|
||||
{
|
||||
@ -210,7 +226,7 @@ public class ParcelBillingCalculatorTests
|
||||
ParcelBillingTestData.TotalTierDetail("3", 2M, 14M),
|
||||
};
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem("outbound", 1, "freight", details));
|
||||
ParcelBillingTestData.ReceivablePriceItem(1, "freight", details));
|
||||
|
||||
var fees = ParcelBillingCalculator.CalculateQuoteReceivableFees(
|
||||
snapshot,
|
||||
@ -232,7 +248,7 @@ public class ParcelBillingCalculatorTests
|
||||
detail.CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = 12M };
|
||||
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail));
|
||||
ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail));
|
||||
|
||||
var fees = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||
snapshot,
|
||||
@ -248,12 +264,13 @@ public class ParcelBillingCalculatorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasEnabledForecastPriceItems_ShouldDetectForecastEnabledItems()
|
||||
public void HasEnabledReceivablePriceItems_ShouldDetectAnyEnabledItems()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@ -271,7 +288,6 @@ public class ParcelBillingCalculatorTests
|
||||
{
|
||||
var prices = ParcelBillingTestData.BuildPayablePrices(
|
||||
ParcelBillingTestData.PayablePriceItem(
|
||||
"预报",
|
||||
1,
|
||||
"freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail(
|
||||
@ -300,7 +316,6 @@ public class ParcelBillingCalculatorTests
|
||||
{
|
||||
var prices = ParcelBillingTestData.BuildPayablePrices(
|
||||
ParcelBillingTestData.PayablePriceItem(
|
||||
"预报",
|
||||
0,
|
||||
"freight",
|
||||
ParcelBillingTestData.ContinueFreightDetail()));
|
||||
@ -315,7 +330,7 @@ public class ParcelBillingCalculatorTests
|
||||
null);
|
||||
|
||||
fees.Should().BeEmpty();
|
||||
ParcelBillingCalculator.HasEnabledForecastPayablePriceItems(prices).Should().BeFalse();
|
||||
ParcelBillingCalculator.HasEnabledPayablePriceItems(prices).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -323,9 +338,9 @@ public class ParcelBillingCalculatorTests
|
||||
{
|
||||
var detail = ParcelBillingTestData.ContinueFreightDetail();
|
||||
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
|
||||
ParcelBillingTestData.ReceivablePriceItem("预报", 1, "freight", detail));
|
||||
ParcelBillingTestData.ReceivablePriceItem(1, "freight", detail));
|
||||
var payablePrices = ParcelBillingTestData.BuildPayablePrices(
|
||||
ParcelBillingTestData.PayablePriceItem("预报", 1, "freight", detail));
|
||||
ParcelBillingTestData.PayablePriceItem(1, "freight", detail));
|
||||
|
||||
var receivable = ParcelBillingCalculator.CalculateForecastReceivableFees(
|
||||
snapshot, _meta.PriceVersion, _meta.EffectiveTime, 1.2M, Zone, "general", null);
|
||||
|
||||
@ -9,7 +9,7 @@ using Oms.Domain.OrdersAggregate.Consts;
|
||||
namespace Oms.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 验证查价卖价快照匹配:忽略收费时机、unit 公式、重量左开右闭区间。
|
||||
/// 验证查价卖价快照匹配:unit 公式、重量左开右闭区间。
|
||||
/// </summary>
|
||||
public class PriceCheckSnapshotVerificationTests
|
||||
{
|
||||
@ -23,7 +23,6 @@ public class PriceCheckSnapshotVerificationTests
|
||||
Id = 1,
|
||||
Status = 1,
|
||||
CostItem = "freight",
|
||||
ChargingTime = "outbound",
|
||||
Details =
|
||||
[
|
||||
Detail(24, 1, "total", "3", "0,0.5", 11),
|
||||
@ -37,7 +36,6 @@ public class PriceCheckSnapshotVerificationTests
|
||||
Id = 2,
|
||||
Status = 1,
|
||||
CostItem = "handling",
|
||||
ChargingTime = "outbound",
|
||||
Details = [Detail(28, 2, "total", "3", "0,10", 20)]
|
||||
})
|
||||
]
|
||||
|
||||
@ -24,7 +24,6 @@ internal static class ParcelBillingTestData
|
||||
params ServiceChannelPriceListOutput[] prices) => prices.ToList();
|
||||
|
||||
public static SalesProductPriceListOutput ReceivablePriceItem(
|
||||
string chargingTime,
|
||||
byte status,
|
||||
string costItem,
|
||||
params ChannelPriceDetailOutput[] details) =>
|
||||
@ -33,12 +32,10 @@ internal static class ParcelBillingTestData
|
||||
Status = status,
|
||||
CostItem = costItem,
|
||||
Currency = "CNY",
|
||||
ChargingTime = chargingTime,
|
||||
Details = details.ToList()
|
||||
};
|
||||
|
||||
public static ServiceChannelPriceListOutput PayablePriceItem(
|
||||
string chargingTime,
|
||||
byte status,
|
||||
string costItem,
|
||||
params ChannelPriceDetailOutput[] details) =>
|
||||
@ -47,7 +44,6 @@ internal static class ParcelBillingTestData
|
||||
Status = status,
|
||||
CostItem = costItem,
|
||||
Currency = "CNY",
|
||||
ChargingTime = chargingTime,
|
||||
Details = details.ToList()
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user