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 }; }