using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.Shared.Enums; namespace Oms.Application.Services.Orders; public partial class OrderService { private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService(); /// /// 编辑订单详情(基本信息、发件人、收件人)。 /// [HttpPost("/order/update")] public async Task UpdateOrderDetailAsync(OrderUpdateInput input) { var order = await GetOrderOrThrowAsync(input.Id); if (input.ClientNo != null) { order.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim(); } if (input.TrackNo != null) { order.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim(); } if (input.ChannelCode != null) { order.ChannelCode = string.IsNullOrWhiteSpace(input.ChannelCode) ? null : input.ChannelCode.Trim(); } if (input.CargoAttribute != null) { order.CargoAttribute = input.CargoAttribute.Trim(); } if (input.WarehouseId.HasValue) { order.WarehouseId = input.WarehouseId.Value > 0 ? input.WarehouseId : null; } if (input.IsCod.HasValue) { order.IsCod = input.IsCod.Value ? (byte)1 : (byte)0; } if (input.CodCurrency != null) { order.CodCurrency = input.CodCurrency; } if (input.CodSum.HasValue) { order.CodSum = input.CodSum; } if (input.DeclaredCurrency != null) { order.DeclaredCurrency = input.DeclaredCurrency; } if (input.DeclaredSum.HasValue) { order.DeclaredSum = input.DeclaredSum; } if (input.Quantity.HasValue) { order.Quantity = input.Quantity; } order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); if (input.Sender != null && order.SenderId.HasValue) { var senders = await OrderRepository.GetSendersByOrderIdsAsync([order.Id]); var sender = senders.FirstOrDefault(); if (sender != null) { ApplySenderInput(sender, input.Sender); if (input.Sender.CountryCode != null) { var country = await GetCountryOrThrowAsync(input.Sender.CountryCode, "发件国家不存在"); sender.Country = country.Id; } await OrderRepository.UpdateSenderAsync(sender); } } if (input.Receiver != null && order.RecipientId.HasValue) { var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]); var recipient = recipients.FirstOrDefault(); if (recipient != null) { await ApplyReceiverInputAsync(order, recipient, input.Receiver); await OrderRepository.UpdateRecipientAsync(recipient); } } if (input.Items != null) { await OrderRepository.ReplaceDeclarationsAsync( order.Id, input.Items.Select(x => MapDeclarationInput(order.Id, x))); } return (await MapOrderOutputsAsync([order])).First(); } /// /// 编辑订单件重尺并重算计费重。 /// [HttpPost("/order/weight/update")] public async Task UpdateOrderWeightsAsync(OrderWeightUpdateInput input) { if (input.Weights.Count == 0) { throw new BusinessException("件重尺不能为空"); } var order = await GetOrderOrThrowAsync(input.Id); if (string.IsNullOrWhiteSpace(order.ProductCode)) { throw new BusinessException("订单未绑定销售产品,无法重算计费重"); } var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); if (product == null) { throw new BusinessException("销售产品不存在"); } var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id])) .OrderBy(x => x.Id) .ToList(); if (weights.Count == 0) { throw new BusinessException("订单件重尺不存在"); } var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order); var updatedTypes = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in input.Weights) { var type = item.Type?.Trim(); if (string.IsNullOrWhiteSpace(type)) { continue; } var row = weights.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); if (row == null) { throw new BusinessException($"缺少件重尺类型 {type}"); } if (item.Length.HasValue) { row.Length = item.Length; } if (item.Width.HasValue) { row.Width = item.Width; } if (item.Height.HasValue) { row.Height = item.Height; } if (item.ActualWeight.HasValue) { row.ActualWeight = item.ActualWeight; } if (row.Length.HasValue && row.Width.HasValue && row.Height.HasValue) { row.Cubic = ParcelBillingCalculator.CalculateCubic(row.Length.Value, row.Width.Value, row.Height.Value); } await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, row); updatedTypes.Add(type); } if (updatedTypes.Contains(OrderWeightTypeCodes.Forecast)) { var forecast = weights.First(x => x.Type == OrderWeightTypeCodes.Forecast); SyncWeightRows(weights, forecast); } await OrderRepository.ReplaceWeightsAsync(order.Id, weights); order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); return (await MapOrderOutputsAsync([order])).First(); } /// /// 重算订单应收费用。 /// [HttpPost("/order/fee/recalculate-receivable")] public async Task RecalculateReceivableFeesAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); await RecalculateReceivableCoreAsync(order); return (await MapOrderOutputsAsync([order])).First(); } /// /// 重算订单应付费用。 /// [HttpPost("/order/fee/recalculate-payable")] public async Task RecalculatePayableFeesAsync(OrderActionInput input) { var order = await GetOrderOrThrowAsync(input.Id); await RecalculatePayableCoreAsync(order); return (await MapOrderOutputsAsync([order])).First(); } private async Task RecalculateReceivableCoreAsync(Order order) { if (string.IsNullOrWhiteSpace(order.ProductCode)) { throw new BusinessException("订单未绑定销售产品,无法重算应收"); } var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); if (product == null) { throw new BusinessException("销售产品不存在"); } var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]); var (weightType, chargingStage) = ResolveReceivableBillingStage(order); var weightRow = weights.FirstOrDefault(x => x.Type == weightType) ?? throw new BusinessException($"缺少件重尺类型 {weightType}"); if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0) { throw new BusinessException("计费重无效,请先完善件重尺"); } var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order); 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) { throw new BusinessException("价格分区未匹配(规则快照)"); } var customer = order.CustomerId.HasValue ? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value) : null; await UpsertReceivableFeesAsync( order, snapshotBinding, weightRow.ChargeableWeight ?? 0, zoneKeys, order.CargoAttribute ?? CargoAttributeCodes.General, customer?.CustomerGroup, chargingStage, replaceExistingReceivable: true); order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); } private async Task RecalculatePayableCoreAsync(Order order) { if (string.IsNullOrWhiteSpace(order.ProductCode)) { throw new BusinessException("订单未绑定销售产品,无法重算应付"); } var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); if (product == null) { throw new BusinessException("销售产品不存在"); } var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]); var weightRow = ResolvePayableWeightRow(order, weights); if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0) { throw new BusinessException("计费重无效,请先完善件重尺"); } var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order); 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) { throw new BusinessException("价格分区未匹配(规则快照)"); } var (_, chargingStage) = ResolveReceivableBillingStage(order); var customer = order.CustomerId.HasValue ? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value) : null; await ConfirmServiceChannelAndPayableAsync( order, product, snapshotBinding, zoneKeys, weightRow.ChargeableWeight ?? 0, chargingStage, customer?.CustomerGroup); order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); } private static OrderWeight ResolvePayableWeightRow(Order order, IReadOnlyList weights) { var (_, chargingStage) = ResolveReceivableBillingStage(order); var weightType = chargingStage switch { OrderFeeChargingStageCodes.Inbound => OrderWeightTypeCodes.Receive, _ => OrderWeightTypeCodes.Downstream }; return weights.FirstOrDefault(x => x.Type == weightType) ?? weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Downstream) ?? throw new BusinessException($"缺少件重尺类型 {weightType}"); } private async Task> BuildChannelPriceOutputsAsync(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(MapChannelPriceFactorOutput).ToList() : [], Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapChannelPriceDetailOutput).ToList() : [] }).ToList(); } private static ChannelPriceFactorOutput MapChannelPriceFactorOutput(ServiceChannelPriceFactor entity) => new() { Id = entity.Id, PriceId = entity.PriceId, SerialNumber = entity.SerialNumber, Factor = entity.Factor, FactorValues = OmsDocHelper.DeserializeJson>(entity.FactorValues) ?? [] }; private static ChannelPriceDetailOutput MapChannelPriceDetailOutput(ServiceChannelPriceDetail entity) => new() { Id = entity.Id, PriceId = entity.PriceId, FactorValuesJson = OmsDocHelper.DeserializeJsonObject(entity.FactorValuesJson), CalculationFormula = entity.CalculationFormula, CalculatedValue = entity.CalculatedValue, CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(entity.CustomerGroupValues) }; private static (string WeightType, string ChargingStage) ResolveReceivableBillingStage(Order order) => order.OrderStatus switch { OrderStatusCodes.Received or OrderStatusCodes.PendingPayment or OrderStatusCodes.PendingShip => (OrderWeightTypeCodes.Receive, OrderFeeChargingStageCodes.Inbound), OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed => (OrderWeightTypeCodes.Ship, OrderFeeChargingStageCodes.Outbound), _ => (OrderWeightTypeCodes.Forecast, OrderFeeChargingStageCodes.Forecast) }; private static OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x) { return new OrderDeclaration { OrderId = orderId, CnName = x.CnName, EnName = x.EnName, Sku = x.Sku, Unit = x.Unit, UnitPrice = x.UnitPrice, UnitWeight = x.UnitWeight, Quantity = x.Quantity, Amount = x.Amount, Weight = x.Weight, HsCode = x.ExHsCode ?? x.ImHsCode, ImHsCode = x.ImHsCode, ExHsCode = x.ExHsCode ?? x.ImHsCode, SkuImage = x.SkuImage, Category = x.Category, Brand = x.Brand, BrandType = x.BrandType, IsMagnetic = x.IsMagnetic ? (byte)1 : (byte)0, Distribution = x.Distribution, Material = x.Material, EnMaterial = x.EnMaterial, Purpose = x.Purpose, Attributes = x.Attributes, OriginCountry = x.OriginCountry, SalesLink = x.SalesLink, Model = x.Model, Specifications = x.Specifications, Comment = x.Comment }; } private static void ApplySenderInput(OrderSender sender, OrderParcelSenderInput input) { if (input.SenderName != null) { sender.SenderName = input.SenderName; } if (input.Company != null) { sender.Company = input.Company; } if (input.Phone != null) { sender.Phone = input.Phone; } if (input.TelePhone != null) { sender.TelePhone = input.TelePhone; } if (input.Email != null) { sender.Email = input.Email; } if (input.Province != null) { sender.Province = input.Province; } if (input.City != null) { sender.City = input.City; } if (input.ZipCode != null) { sender.ZipCode = input.ZipCode; } if (input.StreetAddress1 != null) { sender.StreetAddress1 = input.StreetAddress1; } if (input.StreetAddress2 != null) { sender.StreetAddress2 = input.StreetAddress2; } if (input.StreetAddress3 != null) { sender.StreetAddress3 = input.StreetAddress3; } if (input.HouseNumber != null) { sender.HouseNumber = input.HouseNumber; } } private async Task ApplyReceiverInputAsync(Order order, OrderRecipient recipient, OrderParcelReceiverInput input) { if (input.RecipientsName != null) { recipient.RecipientsName = input.RecipientsName; } if (input.CompanyName != null) { recipient.CompanyName = input.CompanyName; } if (input.Phone != null) { recipient.Phone = input.Phone; } if (input.TelePhone != null) { recipient.TelePhone = input.TelePhone; } if (input.Email != null) { recipient.Email = input.Email; } if (input.Province != null) { recipient.Province = input.Province; } if (input.City != null) { recipient.City = input.City; } if (input.StreetNumber != null) { recipient.StreetNumber = input.StreetNumber; } if (input.StreetAddress1 != null) { recipient.StreetAddress1 = input.StreetAddress1; } if (input.StreetAddress2 != null) { recipient.StreetAddress2 = input.StreetAddress2; } if (input.StreetAddress3 != null) { recipient.StreetAddress3 = input.StreetAddress3; } if (input.ZipCode != null) { recipient.ZipCode = input.ZipCode; } if (input.TaxType.HasValue) { recipient.TaxType = input.TaxType; } if (input.TaxNo != null) { recipient.TaxNo = input.TaxNo; } if (input.CertificateType.HasValue) { recipient.CertificateType = input.CertificateType; } if (input.CertificateNo != null) { recipient.CertificateNo = input.CertificateNo; } if (input.Eori != null) { recipient.Eori = input.Eori; } if (input.Vat != null) { recipient.Vat = input.Vat; } if (input.CountryCode != null) { var country = await GetCountryOrThrowAsync(input.CountryCode, "收件国家不存在"); recipient.Country = country.Id; order.ConsigneeCountry = country.CountryCode; order.ConsigneeCountryName = country.ChineseName; await OrderRepository.UpdateAsync(order); } } }