Read SupplierCode from rules publish snapshots before falling back to live itfx metadata so UpsertSmallBag gets apiCode at order create time.
324 lines
11 KiB
C#
324 lines
11 KiB
C#
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<SmallBagTrackUpsertRequest> BuildAsync(
|
||
Order order,
|
||
OrderParcelReceiverInput? receiverInput = null,
|
||
Customer? customer = null,
|
||
CancellationToken cancellationToken = default);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 组装 UpsertSmallBag 请求:ApiCode 优先订单/发布快照中的 supplierCode,再 fallback live itfx。
|
||
/// </summary>
|
||
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<SmallBagTrackUpsertRequest> 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<SalesProductAllocationProviderSnapshotDto> 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<Customer?> 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<string> 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<string?> 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<string?> 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<ServiceChannelRulesPublishSnapshotDto>(rulesRecord.Content);
|
||
return NullIfEmpty(rulesSnapshot?.Downstream?.SupplierCode);
|
||
}
|
||
|
||
private async Task<string?> 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<string?> 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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创单尚未排货时,用销售产品排货规则里首个服务渠道的 API 服务商 code 作为轨迹 ApiCode 预占位。
|
||
/// </summary>
|
||
private async Task<string?> 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();
|
||
}
|