using Fake.DependencyInjection; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Integrations.Itfx; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.CustomersAggregate.Entities; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; namespace Oms.Application.Integrations.SmallBagTrack; public interface ISmallBagTrackUpsertRequestBuilder { Task BuildAsync( Order order, OrderParcelReceiverInput? receiverInput = null, Customer? customer = null, CancellationToken cancellationToken = default); } /// /// 组装 UpsertSmallBag 请求:ApiCode 优先订单/发布快照中的 supplierCode,再 fallback live itfx。 /// internal sealed class SmallBagTrackUpsertRequestBuilder( IOrderRepository orderRepository, ICustomerRepository customerRepository, IServiceChannelRepository serviceChannelRepository, IServiceChannelGroupRepository serviceChannelGroupRepository, IItfxMetadataClient itfxMetadataClient, ISalesProductRepository salesProductRepository, AllocationProviderSnapshotEnricher allocationProviderSnapshotEnricher) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency { private const string RulesOperatorType = "rules"; public async Task BuildAsync( Order order, OrderParcelReceiverInput? receiverInput = null, Customer? customer = null, CancellationToken cancellationToken = default) { customer ??= await ResolveCustomerAsync(order, cancellationToken); var recipients = await orderRepository.GetRecipientsByOrderIdsAsync([order.Id], cancellationToken); var recipient = recipients.FirstOrDefault(); var apiCode = await ResolveTrackApiCodeAsync(order, cancellationToken); return new SmallBagTrackUpsertRequest { Client = customer?.CustomerCode?.Trim() ?? string.Empty, OrderNo = order.ServiceNo?.Trim() ?? string.Empty, TrackNo = ResolveTrackNo(order), OtherNo = order.ClientNo?.Trim() ?? string.Empty, Channel = order.ChannelCode?.Trim() ?? string.Empty, Product = order.ProductCode?.Trim() ?? string.Empty, ApiCode = apiCode, Consignee = new SmallBagTrackConsigneeRequest { CountryCode = receiverInput?.CountryCode?.Trim() ?? order.ConsigneeCountry?.Trim() ?? string.Empty, Province = receiverInput?.Province?.Trim() ?? recipient?.Province?.Trim() ?? string.Empty, Postcode = receiverInput?.ZipCode?.Trim() ?? recipient?.ZipCode?.Trim() ?? string.Empty, City = receiverInput?.City?.Trim() ?? recipient?.City?.Trim() ?? string.Empty, Email = receiverInput?.Email?.Trim() ?? recipient?.Email?.Trim() ?? string.Empty } }; } internal static string ResolveTrackNo(Order order) { var trackNo = order.TrackNo?.Trim(); if (!string.IsNullOrWhiteSpace(trackNo)) { return trackNo; } return order.ServiceNo?.Trim() ?? string.Empty; } internal static string? PickSupplierCodeFromProviders( IEnumerable providers, string? channelCode) { var list = providers.ToList(); if (list.Count == 0) { return null; } if (!string.IsNullOrWhiteSpace(channelCode)) { var matched = list.FirstOrDefault(provider => string.Equals(provider.ChannelCode?.Trim(), channelCode.Trim(), StringComparison.OrdinalIgnoreCase)); var matchedCode = NullIfEmpty(matched?.SupplierCode); if (!string.IsNullOrWhiteSpace(matchedCode)) { return matchedCode; } } foreach (var provider in list.OrderBy(x => x.SerialNumber)) { var code = NullIfEmpty(provider.SupplierCode); if (!string.IsNullOrWhiteSpace(code)) { return code; } } return null; } private async Task ResolveCustomerAsync(Order order, CancellationToken cancellationToken) { if (!order.CustomerId.HasValue) { return null; } return await customerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value, cancellationToken); } private async Task ResolveTrackApiCodeAsync(Order order, CancellationToken cancellationToken) { var fromOrder = NullIfEmpty(order.DownstreamSupplierCode); if (!string.IsNullOrWhiteSpace(fromOrder)) { return fromOrder; } var fromProductSnapshot = await ResolveApiCodeFromProductRulesSnapshotAsync(order, cancellationToken); if (!string.IsNullOrWhiteSpace(fromProductSnapshot)) { return fromProductSnapshot; } var fromChannelSnapshot = await ResolveApiCodeFromChannelRulesSnapshotAsync(order, cancellationToken); if (!string.IsNullOrWhiteSpace(fromChannelSnapshot)) { return fromChannelSnapshot; } var fromChannel = await ResolveApiCodeFromChannelCodeAsync(order.ChannelCode, cancellationToken); if (!string.IsNullOrWhiteSpace(fromChannel)) { return fromChannel; } var provisional = await ResolveProvisionalApiCodeFromProductAsync(order, cancellationToken); return provisional ?? string.Empty; } private async Task ResolveApiCodeFromProductRulesSnapshotAsync( Order order, CancellationToken cancellationToken) { var productCode = order.ProductCode?.Trim(); if (string.IsNullOrWhiteSpace(productCode)) { return null; } var product = await salesProductRepository.GetByProductCodeAsync(productCode, cancellationToken); if (product == null) { return null; } var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime; var rulesVersion = order.SalesProductRulesVersion?.Trim(); SalesProductPublishRecord? rulesRecord = null; if (!string.IsNullOrWhiteSpace(rulesVersion) && !rulesVersion.StartsWith("LIVE-", StringComparison.OrdinalIgnoreCase)) { rulesRecord = await salesProductRepository.GetPublishRecordByVersionAsync( product.Id, RulesOperatorType, rulesVersion, cancellationToken); } else { rulesRecord = await salesProductRepository.GetEffectivePublishRecordAsync( product.Id, RulesOperatorType, bindTime, cancellationToken); } var rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(rulesRecord?.Content); if (rulesSnapshot?.Providers == null || rulesSnapshot.Providers.Count == 0) { return null; } var providers = rulesSnapshot.Providers; if (providers.Any(AllocationProviderSnapshotEnricher.NeedsDownstreamEnrichment)) { await allocationProviderSnapshotEnricher.EnrichAsync(providers, cancellationToken); } return PickSupplierCodeFromProviders(providers, order.ChannelCode); } private async Task ResolveApiCodeFromChannelRulesSnapshotAsync( Order order, CancellationToken cancellationToken) { var channelCode = order.ChannelCode?.Trim(); if (string.IsNullOrWhiteSpace(channelCode)) { return null; } var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken); if (channel == null) { return null; } var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime; var rulesRecord = await serviceChannelRepository.GetEffectivePublishRecordAsync( channel.Id, RulesOperatorType, bindTime, cancellationToken); if (rulesRecord?.Content == null) { return null; } var rulesSnapshot = OmsDocHelper.DeserializeJson(rulesRecord.Content); return NullIfEmpty(rulesSnapshot?.Downstream?.SupplierCode); } private async Task ResolveApiCodeFromChannelCodeAsync( string? channelCode, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelCode)) { return null; } var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode.Trim(), cancellationToken); if (channel == null) { return null; } return await ResolveApiCodeFromServiceChannelAsync(channel, cancellationToken); } private async Task ResolveApiCodeFromServiceChannelAsync( ServiceChannel channel, CancellationToken cancellationToken) { if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0) { return null; } var group = await serviceChannelGroupRepository.FirstOrDefaultAsync( x => x.Id == channel.GroupId.Value, cancellationToken); if (group == null || group.ApiServiceProviderId <= 0) { return null; } var serviceProvider = await itfxMetadataClient.GetServiceProviderAsync( group.ApiServiceProviderId, cancellationToken); return NullIfEmpty(serviceProvider?.Code); } /// /// 创单尚未排货时,用销售产品排货规则里首个服务渠道的 API 服务商 code 作为轨迹 ApiCode 预占位。 /// private async Task ResolveProvisionalApiCodeFromProductAsync( Order order, CancellationToken cancellationToken) { var productCode = order.ProductCode?.Trim(); if (string.IsNullOrWhiteSpace(productCode)) { return null; } var product = await salesProductRepository.GetByProductCodeAsync(productCode, cancellationToken); if (product == null) { return null; } var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id, cancellationToken); var channelIds = providers .Where(x => x.ServiceChannelId.HasValue) .Select(x => x.ServiceChannelId!.Value) .Distinct() .ToArray(); if (channelIds.Length == 0) { return null; } var channels = await serviceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id)); foreach (var channel in channels.OrderBy(x => x.Id)) { var apiCode = await ResolveApiCodeFromServiceChannelAsync(channel, cancellationToken); if (!string.IsNullOrWhiteSpace(apiCode)) { return apiCode; } } return null; } private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); }