using Fake.Application;
using Fake.DependencyInjection;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders.Issues;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Orders;
///
/// 承运推送揽收件重尺后,按当前销售产品卖价/买价出账。
///
public class PickupOrderFeePushService : IScopedDependency
{
private readonly IPickupOrderRepository _pickupOrderRepository;
private readonly ISalesProductRepository _salesProductRepository;
private readonly IServiceChannelRepository _serviceChannelRepository;
private readonly IWeightRuleRepository _weightRuleRepository;
private readonly IWeightRoundingRepository _weightRoundingRepository;
private readonly IKpiAreaRepository _kpiAreaRepository;
private readonly IWarehouseAddressRepository _warehouseAddressRepository;
private readonly KpiAreaPackService _kpiAreaPackService;
public PickupOrderFeePushService(
IPickupOrderRepository pickupOrderRepository,
ISalesProductRepository salesProductRepository,
IServiceChannelRepository serviceChannelRepository,
IWeightRuleRepository weightRuleRepository,
IWeightRoundingRepository weightRoundingRepository,
IKpiAreaRepository kpiAreaRepository,
IWarehouseAddressRepository warehouseAddressRepository,
KpiAreaPackService kpiAreaPackService)
{
_pickupOrderRepository = pickupOrderRepository;
_salesProductRepository = salesProductRepository;
_serviceChannelRepository = serviceChannelRepository;
_weightRuleRepository = weightRuleRepository;
_weightRoundingRepository = weightRoundingRepository;
_kpiAreaRepository = kpiAreaRepository;
_warehouseAddressRepository = warehouseAddressRepository;
_kpiAreaPackService = kpiAreaPackService;
}
public async Task PushFeeAsync(PickupOpenApiPushFeeInput? input)
{
if (input == null)
{
throw new BusinessException("请求体不能为空");
}
var carrierWaybillNo = (input.CarrierWaybillNo ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(carrierWaybillNo))
{
throw new BusinessException("carrierWaybillNo(承运运单号)不能为空");
}
if (input.TotalActualWeight <= 0)
{
throw new BusinessException("totalActualWeight 必须大于 0");
}
var sizes = NormalizeSizes(input.PackageSizes);
if (sizes.Count == 0)
{
throw new BusinessException("packageSizes 不能为空");
}
var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo)
?? throw new BusinessException($"揽收单不存在:承运运单号 {carrierWaybillNo}");
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
{
throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用");
}
if (PickupOrderStatusCodes.IsTerminal(pickup.Status)
&& pickup.Status != PickupOrderStatusCodes.Inbound)
{
throw new BusinessException("当前揽收单状态不可接收件重尺");
}
if (string.IsNullOrWhiteSpace(pickup.CarrierService))
{
throw new BusinessException("揽收单未选择承运服务(销售产品),无法计费");
}
var product = await _salesProductRepository.GetByProductCodeAsync(pickup.CarrierService.Trim())
?? throw new BusinessException($"未找到承运服务对应销售产品「{pickup.CarrierService}」");
var totalVolume = sizes.Sum(x => x.Length * x.Width * x.Height * x.Count);
var weightRow = new OrderWeight
{
Type = OrderWeightTypeCodes.Receive,
Length = totalVolume,
Width = 1,
Height = 1,
ActualWeight = input.TotalActualWeight,
Cubic = ParcelBillingCalculator.CalculateCubic(totalVolume, 1, 1)
};
decimal volumetricWeight;
decimal chargeableWeight;
try
{
(volumetricWeight, chargeableWeight) = await CalculateChargeableAsync(product, weightRow);
}
catch (InvalidOperationException ex)
{
throw new BusinessException($"计费重计算失败:{ex.Message}");
}
var countryCode = string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
? "CN"
: pickup.SenderCountryCode.Trim().ToUpperInvariant();
var priceSnapshot = await BuildSellPriceSnapshotAsync(product);
var zoneKeys = ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(
priceSnapshot,
countryCode,
pickup.SenderProvince,
pickup.SenderCity,
pickup.SenderZipCode);
if (zoneKeys == null || zoneKeys.Count == 0)
{
throw new BusinessException("卖价分区未匹配,请检查揽收地址与产品价格分区");
}
var warehouse = await _warehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == pickup.WarehouseId);
var warehouseName = warehouse?.WarehouseName;
var priceVersion = DateTime.Now.ToString("yyyyMMddHHmmss");
var priceEffectiveTime = DateTime.Now;
var cargoAttribute = CargoAttributeCodes.General;
var conditionContext = OrderRuleEvaluationContext.FromOrderOnly(new Order
{
ProductCode = product.ProductCode,
ConsigneeCountry = countryCode,
CargoAttribute = cargoAttribute,
WarehouseId = pickup.WarehouseId,
CustomerId = pickup.CustomerId
});
var receivableLines = ParcelBillingCalculator.CalculateQuoteReceivableFeeLines(
priceSnapshot,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
priceTierKey: null,
pickup.WarehouseId,
warehouseName,
conditionContext);
if (receivableLines.Count == 0 && ParcelBillingCalculator.HasEnabledReceivablePriceItems(priceSnapshot))
{
var detail = ParcelBillingCalculator.ExplainQuotePriceMiss(
priceSnapshot,
chargeableWeight,
zoneKeys,
cargoAttribute,
null,
pickup.WarehouseId,
warehouseName);
throw new BusinessException($"卖价未匹配到有效价格:{detail}");
}
var payable = await CalculatePayableAsync(
product,
pickup,
zoneKeys,
chargeableWeight,
cargoAttribute,
warehouseName,
priceVersion,
priceEffectiveTime,
conditionContext);
var receivableFees = receivableLines.Select(x => ToFeeLine(x.Fee)).ToList();
var payableFees = payable.Select(ToFeeLine).ToList();
var receivableTotal = receivableFees.Sum(x => x.Amount);
var payableTotal = payableFees.Sum(x => x.Amount);
var freight = receivableFees.FirstOrDefault()?.Amount ?? receivableTotal;
var other = Math.Max(0, receivableTotal - freight);
pickup.TotalActualWeight = input.TotalActualWeight;
pickup.VolumetricWeight = volumetricWeight;
pickup.ChargeableWeight = chargeableWeight;
pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes);
pickup.CarrierFreightWeight = input.CarrierFreightWeight;
pickup.CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount;
pickup.CarrierQuotedReceivableAmount = input.CarrierQuotedReceivableAmount;
pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput
{
ValueInfoList = input.ValueInfoList ?? []
});
pickup.ReceivableFeeJson = OmsDocHelper.SerializeJson(receivableFees);
pickup.PayableFeeJson = OmsDocHelper.SerializeJson(payableFees);
pickup.FreightAmount = freight;
pickup.OtherFeeAmount = other;
pickup.ReceivableAmount = receivableTotal;
pickup.CarrierPayableAmount = payableTotal;
pickup.FeePushedAt = DateTime.Now;
pickup.UpdateTime = DateTime.Now;
await _pickupOrderRepository.UpdateAsync(pickup);
return new PickupOpenApiPushFeeResult
{
PickupNo = pickup.PickupNo,
TotalActualWeight = input.TotalActualWeight,
ChargeableWeight = chargeableWeight,
VolumetricWeight = volumetricWeight,
ReceivableAmount = receivableTotal,
CarrierPayableAmount = payableTotal,
CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount
};
}
private async Task<(decimal VolumetricWeight, decimal ChargeableWeight)> CalculateChargeableAsync(
SalesProduct product,
OrderWeight weight)
{
if (product.WeightRuleId is not > 0)
{
throw new BusinessException("未配置计重规则");
}
var ruleInfos = await _weightRuleRepository.GetAllInfosByRuleIdAsync(product.WeightRuleId.Value);
var (volumetric, raw) = ParcelBillingCalculator.CalculateRawBillableWeight(weight, ruleInfos);
if (product.WeightRoundingId is not > 0)
{
throw new BusinessException("未配置计重进位");
}
var ranges = await _weightRoundingRepository.GetAllInfosByRoundingIdAsync(product.WeightRoundingId.Value);
var chargeable = ParcelBillingCalculator.ApplyRounding(raw, ranges);
return (volumetric, chargeable);
}
private async Task BuildSellPriceSnapshotAsync(SalesProduct product)
{
var snapshot = new SalesProductPricePublishSnapshotDto
{
SalesProductId = product.Id,
Prices = (await GetSellPriceOutputsAsync(product.Id)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
};
await PriceAreaSnapshotHelper.ApplyLivePriceAreaInfosAsync(
snapshot,
product,
_kpiAreaRepository,
_kpiAreaPackService);
return snapshot;
}
private async Task> GetSellPriceOutputsAsync(int salesProductId)
{
var prices = await _salesProductRepository.GetPricesAsync(salesProductId);
var priceIds = prices.Select(x => x.Id).ToArray();
var factorMap = (await _salesProductRepository.GetPriceFactorsAsync(priceIds))
.GroupBy(x => x.PriceId)
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
var detailMap = (await _salesProductRepository.GetPriceDetailsAsync(priceIds))
.GroupBy(x => x.PriceId)
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
return prices.Select(price => new SalesProductPriceListOutput
{
Id = price.Id,
SalesProductId = price.SalesProductId,
SerialNumber = price.SerialNumber,
Status = (byte)price.Status,
PriceName = price.PriceName,
CostItem = price.CostItem,
CostUnit = price.CostUnit,
Currency = price.Currency,
ChargingCondition = price.ChargingCondition,
Factors = factorMap.TryGetValue(price.Id, out var factors)
? factors.Select(f => new ChannelPriceFactorOutput
{
Id = f.Id,
PriceId = f.PriceId,
SerialNumber = f.SerialNumber,
Factor = f.Factor,
FactorValues = OmsDocHelper.DeserializeJson>(f.FactorValues) ?? []
}).ToList()
: [],
Details = detailMap.TryGetValue(price.Id, out var details)
? details.Select(d => new ChannelPriceDetailOutput
{
Id = d.Id,
PriceId = d.PriceId,
FactorValuesJson = OmsDocHelper.DeserializeJsonObject(d.FactorValuesJson),
CalculationFormula = d.CalculationFormula,
CalculatedValue = d.CalculatedValue,
CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(d.CustomerGroupValues)
}).ToList()
: []
}).ToList();
}
private async Task> CalculatePayableAsync(
SalesProduct product,
PickupOrder pickup,
IReadOnlyList zoneKeys,
decimal chargeableWeight,
string cargoAttribute,
string? warehouseName,
string priceVersion,
DateTime priceEffectiveTime,
OrderRuleEvaluationContext conditionContext)
{
var providers = await _salesProductRepository.GetAllocationProvidersAsync(product.Id);
var channelIds = providers.Where(x => x.ServiceChannelId is > 0).Select(x => x.ServiceChannelId!.Value).Distinct().ToArray();
var channels = channelIds.Length == 0
? []
: await _serviceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id));
var channelNameMap = channels.ToDictionary(x => x.Id, x => x.ChannelName);
var snapshots = providers.Select(provider => new SalesProductAllocationProviderSnapshotDto
{
Id = provider.Id,
SalesProductId = provider.SalesProductId,
SerialNumber = provider.SerialNumber,
ProviderName = provider.ProviderName,
ServiceChannelId = provider.ServiceChannelId,
ServiceChannelName = provider.ServiceChannelId is > 0
&& channelNameMap.TryGetValue(provider.ServiceChannelId.Value, out var name)
? name
: null,
Weight = provider.Weight,
CustomerIds = OmsDocHelper.DeserializeJson>(provider.CustomerIds) ?? [],
MatchCondition = provider.MatchCondition
}).ToList();
var filtered = AllocationResolver.FilterProvidersDetailed(snapshots, pickup.CustomerId, conditionContext).Passed;
var provider = filtered.OrderBy(x => x.SerialNumber).FirstOrDefault(x => x.ServiceChannelId is > 0);
if (provider?.ServiceChannelId is not > 0)
{
throw new BusinessException("未配置可用排货服务渠道,无法计算应付");
}
var channelPrices = await GetChannelPriceOutputsAsync(provider.ServiceChannelId.Value);
if (channelPrices.Count == 0)
{
throw new BusinessException($"服务渠道 {provider.ServiceChannelId} 无买价");
}
var payable = ParcelBillingCalculator.CalculatePayableFees(
channelPrices,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
OrderFeeChargingStageCodes.Forecast,
pickup.WarehouseId,
warehouseName,
conditionContext);
if (payable.Count == 0)
{
throw new BusinessException($"服务渠道 {provider.ServiceChannelId} 买价未匹配");
}
return payable;
}
private async Task> GetChannelPriceOutputsAsync(int serviceChannelId)
{
var prices = await _serviceChannelRepository.GetPricesAsync(serviceChannelId);
var priceIds = prices.Select(x => x.Id).ToArray();
var factorMap = (await _serviceChannelRepository.GetPriceFactorsAsync(priceIds))
.GroupBy(x => x.PriceId)
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
var detailMap = (await _serviceChannelRepository.GetPriceDetailsAsync(priceIds))
.GroupBy(x => x.PriceId)
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
return prices.Select(price => new ServiceChannelPriceListOutput
{
Id = price.Id,
ServiceChannelId = price.ServiceChannelId,
SerialNumber = price.SerialNumber,
Status = (byte)price.Status,
PriceName = price.PriceName,
CostItem = price.CostItem,
CostUnit = price.CostUnit,
Currency = price.Currency,
ChargingCondition = price.ChargingCondition,
Factors = factorMap.TryGetValue(price.Id, out var factors)
? factors.Select(f => new ChannelPriceFactorOutput
{
Id = f.Id,
PriceId = f.PriceId,
SerialNumber = f.SerialNumber,
Factor = f.Factor,
FactorValues = OmsDocHelper.DeserializeJson>(f.FactorValues) ?? []
}).ToList()
: [],
Details = detailMap.TryGetValue(price.Id, out var details)
? details.Select(d => new ChannelPriceDetailOutput
{
Id = d.Id,
PriceId = d.PriceId,
FactorValuesJson = OmsDocHelper.DeserializeJsonObject(d.FactorValuesJson),
CalculationFormula = d.CalculationFormula,
CalculatedValue = d.CalculatedValue,
CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(d.CustomerGroupValues)
}).ToList()
: []
}).ToList();
}
private static List NormalizeSizes(List? list)
{
return (list ?? [])
.Select(x => new PickupPackageSizeOutput
{
Length = x.Length,
Width = x.Width,
Height = x.Height,
Count = x.Count <= 0 ? 1 : x.Count
})
.Where(x => x.Length > 0 && x.Width > 0 && x.Height > 0)
.ToList();
}
private static PickupFeeLineOutput ToFeeLine(OrderFee fee) =>
new()
{
CostItem = fee.CostItem,
Amount = fee.Amount,
Currency = fee.Currency ?? string.Empty
};
}