All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
库内 SKU 与 createOrderOne items 对齐 productId/nonStdProductId/stdProductId;无值时仍序列化为 null。 Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
8.4 KiB
C#
228 lines
8.4 KiB
C#
using Fake.ObjectStorage;
|
||
using Oms.Application.Dtos.Orders;
|
||
using Oms.Application.Storage;
|
||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||
using Oms.Domain.OrdersAggregate.Consts;
|
||
using Oms.Domain.OrdersAggregate.Entities;
|
||
using Oms.Domain.SystemAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Shared;
|
||
|
||
/// <summary>
|
||
/// 从订单表及关联子表组装 itfx createOrderOne 标准请求体。
|
||
/// </summary>
|
||
public static class OrderDownstreamCreateOneBodyBuilder
|
||
{
|
||
public static async Task<ItfxCreateOrderOneBody> BuildAsync(
|
||
Order order,
|
||
OrderSender? sender,
|
||
OrderRecipient? recipient,
|
||
IReadOnlyList<OrderDeclaration> declarations,
|
||
IReadOnlyList<OrderWeight> weights,
|
||
ICountryRepository countryRepository,
|
||
IServiceChannelRepository serviceChannelRepository,
|
||
IObjectStorage? fileStorage = null,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
var countryIds = new List<int>();
|
||
if (sender?.Country is > 0)
|
||
{
|
||
countryIds.Add(sender.Country.Value);
|
||
}
|
||
|
||
if (recipient?.Country is > 0)
|
||
{
|
||
countryIds.Add(recipient.Country.Value);
|
||
}
|
||
|
||
var countries = countryIds.Count == 0
|
||
? []
|
||
: await countryRepository.GetListAsync(x => countryIds.Contains(x.Id));
|
||
var idToCode = countries.ToDictionary(x => x.Id, x => x.CountryCode);
|
||
|
||
var forecast = weights.FirstOrDefault(x =>
|
||
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase))
|
||
?? weights.FirstOrDefault();
|
||
|
||
var productCode = await ResolveForecastProductCodeAsync(
|
||
order,
|
||
serviceChannelRepository,
|
||
cancellationToken);
|
||
|
||
return new ItfxCreateOrderOneBody
|
||
{
|
||
ClientNo = order.ClientNo,
|
||
TrackNo = order.TrackNo,
|
||
ServiceNo = order.ServiceNo,
|
||
ProductCode = productCode,
|
||
CargoAttribute = order.CargoAttribute,
|
||
WarehouseId = order.WarehouseId,
|
||
CustomerId = order.CustomerId,
|
||
Length = forecast?.Length,
|
||
Width = forecast?.Width,
|
||
Height = forecast?.Height,
|
||
ActualWeight = forecast?.ActualWeight,
|
||
VolumetricWeight = forecast?.VolumetricWeight,
|
||
ChargeableWeight = forecast?.ChargeableWeight,
|
||
Cubic = forecast?.Cubic,
|
||
IsCod = order.IsCod > 0,
|
||
CodCurrency = order.CodCurrency,
|
||
CodSum = FormatDecimal(order.CodSum),
|
||
DeclaredCurrency = order.DeclaredCurrency,
|
||
DeclaredSum = FormatDecimal(order.DeclaredSum),
|
||
Quantity = order.Quantity?.ToString(),
|
||
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
|
||
Sender = MapSender(sender, idToCode),
|
||
Items = declarations.Select(x => MapItem(x, fileStorage)).ToList()
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// createOrderOne.productCode = 渠道「预报渠道 code」:
|
||
/// 优先订单固化的 ChannelForecastCode,否则 live 查 t_service_channel.ForecastCode。
|
||
/// </summary>
|
||
private static async Task<string?> ResolveForecastProductCodeAsync(
|
||
Order order,
|
||
IServiceChannelRepository serviceChannelRepository,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
var snapshotted = order.ChannelForecastCode?.Trim();
|
||
if (!string.IsNullOrWhiteSpace(snapshotted))
|
||
{
|
||
return snapshotted;
|
||
}
|
||
|
||
var channelCode = order.ChannelCode?.Trim();
|
||
if (string.IsNullOrWhiteSpace(channelCode))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
|
||
var forecastCode = channel?.ForecastCode?.Trim();
|
||
return string.IsNullOrWhiteSpace(forecastCode) ? null : forecastCode;
|
||
}
|
||
|
||
private static string? FormatDecimal(decimal? value) =>
|
||
value.HasValue ? value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;
|
||
|
||
private static string? ResolveCountryIso(int? countryId, string? orderConsigneeCountry, IReadOnlyDictionary<int, string> idToCode)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(orderConsigneeCountry))
|
||
{
|
||
var trimmed = orderConsigneeCountry.Trim();
|
||
if (trimmed.Length == 2)
|
||
{
|
||
return trimmed.ToUpperInvariant();
|
||
}
|
||
}
|
||
|
||
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var iso) && !string.IsNullOrWhiteSpace(iso))
|
||
{
|
||
return iso.Trim().ToUpperInvariant();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static OrderParcelReceiverInput? MapReceiver(
|
||
OrderRecipient? recipient,
|
||
string? orderConsigneeCountry,
|
||
IReadOnlyDictionary<int, string> idToCode)
|
||
{
|
||
if (recipient == null)
|
||
{
|
||
return string.IsNullOrWhiteSpace(orderConsigneeCountry)
|
||
? null
|
||
: new OrderParcelReceiverInput { CountryCode = orderConsigneeCountry.Trim().ToUpperInvariant() };
|
||
}
|
||
|
||
return new OrderParcelReceiverInput
|
||
{
|
||
RecipientsName = recipient.RecipientsName,
|
||
CompanyName = recipient.CompanyName,
|
||
Phone = recipient.Phone,
|
||
TelePhone = recipient.TelePhone,
|
||
Email = recipient.Email,
|
||
CountryCode = ResolveCountryIso(recipient.Country, orderConsigneeCountry, idToCode),
|
||
Province = recipient.Province,
|
||
City = recipient.City,
|
||
StreetNumber = recipient.StreetNumber,
|
||
StreetAddress1 = recipient.StreetAddress1,
|
||
StreetAddress2 = recipient.StreetAddress2,
|
||
StreetAddress3 = recipient.StreetAddress3,
|
||
ZipCode = recipient.ZipCode,
|
||
TaxType = recipient.TaxType,
|
||
TaxNo = recipient.TaxNo,
|
||
CertificateType = recipient.CertificateType,
|
||
CertificateNo = recipient.CertificateNo,
|
||
Eori = recipient.Eori,
|
||
Vat = recipient.Vat
|
||
};
|
||
}
|
||
|
||
private static OrderParcelSenderInput? MapSender(OrderSender? sender, IReadOnlyDictionary<int, string> idToCode)
|
||
{
|
||
if (sender == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return new OrderParcelSenderInput
|
||
{
|
||
SenderName = sender.SenderName,
|
||
Company = sender.Company,
|
||
Phone = sender.Phone,
|
||
TelePhone = sender.TelePhone,
|
||
Email = sender.Email,
|
||
CountryCode = ResolveCountryIso(sender.Country, null, idToCode),
|
||
Province = sender.Province,
|
||
City = sender.City,
|
||
ZipCode = sender.ZipCode,
|
||
StreetAddress1 = sender.StreetAddress1,
|
||
StreetAddress2 = sender.StreetAddress2,
|
||
StreetAddress3 = sender.StreetAddress3,
|
||
HouseNumber = sender.HouseNumber
|
||
};
|
||
}
|
||
|
||
private static OrderParcelItemInput MapItem(OrderDeclaration x, IObjectStorage? fileStorage) =>
|
||
new()
|
||
{
|
||
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,
|
||
ImHsCode = x.ImHsCode,
|
||
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||
DeclareElement = x.DeclareElement,
|
||
// 始终赋值;空白→null。配合 OrderParcelItemInput 上 JsonIgnore(Never),请求体固定出现 "xxx": null。
|
||
ProductId = NullIfWhiteSpace(x.ProductId),
|
||
NonStdProductId = NullIfWhiteSpace(x.NonStdProductId),
|
||
StdProductId = NullIfWhiteSpace(x.StdProductId),
|
||
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
|
||
Category = x.Category,
|
||
Brand = x.Brand,
|
||
BrandType = x.BrandType,
|
||
IsMagnetic = x.IsMagnetic > 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 string? NullIfWhiteSpace(string? value) =>
|
||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||
}
|