using System.Text.Json;
using System.Text.Json.Nodes;
using Fake.Application;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.PlatformStore;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.WarehouseAggregate.Entities;
namespace Oms.Application.Services.Orders;
///
/// 将店铺订单转换为小包创建入参。
///
public static class PlatformStoreOrderParcelBuilder
{
private const decimal DefaultLengthCm = 10m;
private const decimal DefaultWidthCm = 10m;
private const decimal DefaultHeightCm = 10m;
private const decimal MinActualWeightKg = 0.01m;
public static OrderParcelCreateInput Build(
SalesStoreOrder storeOrder,
IReadOnlyCollection lines,
IReadOnlyDictionary skuMap,
WarehouseAddress warehouse,
string productCode,
int warehouseId,
int customerId,
OrderParcelReceiverInput? receiverOverride = null,
string? trackNoOverride = null)
{
var receiver = receiverOverride ?? ParseReceiver(storeOrder);
ValidateReceiver(receiver);
var declarationLines = BuildDeclarationLines(lines, skuMap);
var (length, width, height, actualWeightKg) = ResolveParcelMetrics(declarationLines, lines, skuMap);
return new OrderParcelCreateInput
{
RequestId = $"platform-store-order:{storeOrder.Id}",
ClientNo = storeOrder.PlatformOrderNo,
TrackNo = string.IsNullOrWhiteSpace(trackNoOverride) ? storeOrder.ShippingLabelNo : trackNoOverride.Trim(),
ProductCode = productCode,
WarehouseId = warehouseId,
CustomerId = customerId,
Length = length,
Width = width,
Height = height,
ActualWeight = actualWeightKg,
ChargeableWeight = actualWeightKg,
CargoAttribute = CargoAttributeCodes.General,
DeclaredCurrency = string.IsNullOrWhiteSpace(storeOrder.Currency) ? "USD" : storeOrder.Currency.Trim(),
DeclaredSum = storeOrder.TotalAmount,
Quantity = 1,
Receiver = receiver,
Sender = BuildSender(warehouse),
Items = declarationLines
};
}
///
/// 将下单页同款「运单基本信息」覆盖到组装好的小包入参。
///
public static void ApplyParcelBasic(
OrderParcelCreateInput parcelInput,
PlatformStoreOrderParcelBasicInput? basic,
string? clientNoOverride = null)
{
if (!string.IsNullOrWhiteSpace(clientNoOverride))
{
parcelInput.ClientNo = clientNoOverride.Trim();
}
else if (!string.IsNullOrWhiteSpace(basic?.ClientNo))
{
parcelInput.ClientNo = basic.ClientNo.Trim();
}
if (basic == null)
{
return;
}
if (!string.IsNullOrWhiteSpace(basic.CargoAttribute))
{
parcelInput.CargoAttribute = basic.CargoAttribute.Trim();
}
if (basic.ActualWeight is > 0)
{
parcelInput.ActualWeight = basic.ActualWeight;
parcelInput.ChargeableWeight = basic.ActualWeight;
}
if (basic.Quantity is > 0)
{
parcelInput.Quantity = basic.Quantity;
}
if (basic.Length is > 0)
{
parcelInput.Length = basic.Length;
}
if (basic.Width is > 0)
{
parcelInput.Width = basic.Width;
}
if (basic.Height is > 0)
{
parcelInput.Height = basic.Height;
}
if (!string.IsNullOrWhiteSpace(basic.ExportCustomsMode))
{
parcelInput.ExportCustomsMode = basic.ExportCustomsMode.Trim();
}
if (basic.ExportQualificationId is > 0)
{
parcelInput.ExportQualificationId = basic.ExportQualificationId;
}
if (!string.IsNullOrWhiteSpace(basic.PlatformCode))
{
parcelInput.PlatformCode = basic.PlatformCode.Trim();
}
if (!string.IsNullOrWhiteSpace(basic.InboundMethod))
{
parcelInput.InboundMethod = basic.InboundMethod.Trim();
}
if (basic.SenderAddressId is > 0)
{
parcelInput.SenderAddressId = basic.SenderAddressId;
}
if (basic.ExpectPickupTime.HasValue && basic.ExpectPickupTime != default)
{
parcelInput.ExpectPickupTime = basic.ExpectPickupTime;
}
if (!string.IsNullOrWhiteSpace(basic.PickupDirection))
{
parcelInput.PickupDirection = basic.PickupDirection.Trim();
}
if (!string.IsNullOrWhiteSpace(basic.CarrierCode))
{
parcelInput.CarrierCode = basic.CarrierCode.Trim();
}
if (!string.IsNullOrWhiteSpace(basic.CarrierService))
{
parcelInput.CarrierService = basic.CarrierService.Trim();
}
}
///
/// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&代发)。
///
public static OrderParcelCreateInput BuildFromPlatformLines(
SalesStoreOrder storeOrder,
IReadOnlyCollection lines,
WarehouseAddress? warehouse,
string productCode,
int? warehouseId,
int customerId,
OrderParcelReceiverInput? receiverOverride = null,
string? trackNoOverride = null)
{
var receiver = receiverOverride ?? ParseReceiver(storeOrder);
ValidateReceiver(receiver);
var declarationLines = BuildDeclarationLinesFromPlatform(lines);
var totalWeightKg = Math.Max(
declarationLines.Sum(x => x.Weight ?? 0m),
MinActualWeightKg);
return new OrderParcelCreateInput
{
RequestId = $"platform-store-order:{storeOrder.Id}",
ClientNo = storeOrder.PlatformOrderNo,
TrackNo = string.IsNullOrWhiteSpace(trackNoOverride) ? storeOrder.ShippingLabelNo : trackNoOverride?.Trim(),
ProductCode = productCode,
WarehouseId = warehouseId,
CustomerId = customerId,
Length = DefaultLengthCm,
Width = DefaultWidthCm,
Height = DefaultHeightCm,
ActualWeight = totalWeightKg,
ChargeableWeight = totalWeightKg,
CargoAttribute = CargoAttributeCodes.General,
DeclaredCurrency = string.IsNullOrWhiteSpace(storeOrder.Currency) ? "USD" : storeOrder.Currency.Trim(),
DeclaredSum = storeOrder.TotalAmount,
Quantity = 1,
Receiver = receiver,
Sender = warehouse != null ? BuildSender(warehouse) : BuildFallbackSender(storeOrder),
Items = declarationLines
};
}
private static List BuildDeclarationLinesFromPlatform(
IReadOnlyCollection lines)
{
var items = new List();
foreach (var line in lines)
{
var cnName = string.IsNullOrWhiteSpace(line.ProductName)
? (string.IsNullOrWhiteSpace(line.PlatformSku) ? "Platform Item" : line.PlatformSku.Trim())
: line.ProductName.Trim();
var sku = string.IsNullOrWhiteSpace(line.PlatformSku)
? (cnName.Length > 64 ? cnName[..64] : cnName)
: line.PlatformSku.Trim();
var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
var qty = Math.Max(1, line.Quantity);
items.Add(new OrderParcelItemInput
{
CnName = cnName,
EnName = cnName,
Sku = sku,
Unit = "PCS",
UnitPrice = unitPrice,
UnitWeight = MinActualWeightKg,
Quantity = qty,
Amount = unitPrice * qty,
Weight = MinActualWeightKg * qty
});
}
if (items.Count == 0)
{
throw new BusinessException("没有可申报的商品行");
}
return items;
}
private static OrderParcelSenderInput BuildFallbackSender(SalesStoreOrder storeOrder)
{
return new OrderParcelSenderInput
{
SenderName = string.IsNullOrWhiteSpace(storeOrder.PlatformOrderNo)
? "Platform Seller"
: $"Store#{storeOrder.StoreId}",
StreetAddress1 = "TBD",
CountryCode = storeOrder.OrderCountry
};
}
private static List BuildDeclarationLines(
IReadOnlyCollection lines,
IReadOnlyDictionary skuMap)
{
var items = new List();
foreach (var line in lines)
{
if (!line.MappedSkuId.HasValue || !skuMap.TryGetValue(line.MappedSkuId.Value, out var sku))
{
throw new BusinessException($"申报行「{line.PlatformSku}」缺少库内 SKU 映射");
}
var unitWeightKg = sku.WeightG is > 0 ? Math.Max(sku.WeightG.Value / 1000m, MinActualWeightKg) : MinActualWeightKg;
var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? sku.SkuName : line.ProductName.Trim();
var enName = string.IsNullOrWhiteSpace(sku.SkuNameEn)
? (string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName)
: sku.SkuNameEn.Trim();
items.Add(new OrderParcelItemInput
{
CnName = cnName,
EnName = enName,
Sku = sku.SkuCode,
Unit = "PCS",
UnitPrice = unitPrice,
UnitWeight = unitWeightKg,
Quantity = line.Quantity,
Amount = unitPrice * line.Quantity,
Weight = unitWeightKg * line.Quantity,
ExHsCode = sku.ExHsCode,
ImHsCode = sku.ImHsCode,
DeclareElement = sku.DeclareElement,
ProductId = sku.ProductId,
NonStdProductId = sku.NonStdProductId,
StdProductId = sku.StdProductId,
});
}
if (items.Count == 0)
{
throw new BusinessException("没有可申报的商品行");
}
return items;
}
private static (decimal Length, decimal Width, decimal Height, decimal ActualWeightKg) ResolveParcelMetrics(
IReadOnlyCollection items,
IReadOnlyCollection lines,
IReadOnlyDictionary skuMap)
{
decimal maxLength = DefaultLengthCm;
decimal maxWidth = DefaultWidthCm;
decimal maxHeight = DefaultHeightCm;
decimal totalWeightKg = 0m;
foreach (var line in lines.Where(x => x.MappedSkuId.HasValue))
{
if (!skuMap.TryGetValue(line.MappedSkuId!.Value, out var sku))
{
continue;
}
if (sku.LengthCm is > 0)
{
maxLength = Math.Max(maxLength, sku.LengthCm.Value);
}
if (sku.WidthCm is > 0)
{
maxWidth = Math.Max(maxWidth, sku.WidthCm.Value);
}
if (sku.HeightCm is > 0)
{
maxHeight = Math.Max(maxHeight, sku.HeightCm.Value);
}
var unitWeightKg = sku.WeightG is > 0 ? sku.WeightG.Value / 1000m : MinActualWeightKg;
totalWeightKg += unitWeightKg * line.Quantity;
}
if (totalWeightKg <= 0)
{
totalWeightKg = Math.Max(items.Sum(x => x.Weight ?? 0m), MinActualWeightKg);
}
return (maxLength, maxWidth, maxHeight, Math.Max(totalWeightKg, MinActualWeightKg));
}
private static OrderParcelSenderInput BuildSender(WarehouseAddress warehouse)
{
var senderName = string.IsNullOrWhiteSpace(warehouse.Recipient) ? warehouse.WarehouseName : warehouse.Recipient.Trim();
return new OrderParcelSenderInput
{
SenderName = senderName,
Phone = warehouse.ContactPhone,
StreetAddress1 = string.IsNullOrWhiteSpace(warehouse.ShippingAddress) ? warehouse.WarehouseName : warehouse.ShippingAddress.Trim()
};
}
private static OrderParcelReceiverInput? ParseReceiver(SalesStoreOrder storeOrder)
{
if (!string.IsNullOrWhiteSpace(storeOrder.RecipientName)
&& !string.IsNullOrWhiteSpace(storeOrder.RecipientCountry ?? storeOrder.OrderCountry)
&& !string.IsNullOrWhiteSpace(storeOrder.RecipientZipCode)
&& !string.IsNullOrWhiteSpace(storeOrder.RecipientAddress1))
{
return new OrderParcelReceiverInput
{
RecipientsName = storeOrder.RecipientName.Trim(),
CompanyName = storeOrder.RecipientCompany,
Phone = storeOrder.RecipientPhone,
TelePhone = storeOrder.RecipientTelePhone,
Email = storeOrder.RecipientEmail,
CountryCode = (storeOrder.RecipientCountry ?? storeOrder.OrderCountry)!.Trim(),
Province = storeOrder.RecipientProvince,
City = storeOrder.RecipientCity,
StreetAddress1 = storeOrder.RecipientAddress1.Trim(),
StreetAddress2 = storeOrder.RecipientAddress2,
ZipCode = storeOrder.RecipientZipCode.Trim()
};
}
if (!string.IsNullOrWhiteSpace(storeOrder.PlatformExtraJson))
{
try
{
var node = JsonNode.Parse(storeOrder.PlatformExtraJson);
if (node != null)
{
var parsed = ParseReceiverNode(node);
if (parsed != null)
{
return parsed;
}
}
}
catch (JsonException)
{
// 忽略 JSON 解析错误,走兜底
}
}
if (string.IsNullOrWhiteSpace(storeOrder.OrderCountry))
{
return null;
}
return new OrderParcelReceiverInput
{
RecipientsName = "Platform Recipient",
CountryCode = storeOrder.OrderCountry.Trim(),
ZipCode = "00000",
StreetAddress1 = "TBD"
};
}
private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node)
{
var candidates = new List { node };
foreach (var key in new[]
{
"recipient_address", "recipientAddress", "recipient", "receiver", "shippingAddress", "address"
})
{
if (node[key] != null)
{
candidates.Add(node[key]);
}
}
foreach (var candidate in candidates)
{
if (candidate == null)
{
continue;
}
var (province, city, countryFromDistrict) = ParseTiktokDistrictInfo(candidate);
var name = ReadText(candidate, "recipientsName", "recipientName", "receiverName", "name", "fullName", "first_name");
var country = ReadText(candidate, "countryCode", "country", "region_code", "region", "orderCountry")
?? countryFromDistrict
?? ReadText(node, "orderCountry");
var zip = ReadText(candidate, "zipCode", "zip", "postal_code", "postalCode", "postcode");
var address = ReadText(
candidate,
"streetAddress1",
"address_line1",
"address_detail",
"address",
"address1",
"full_address",
"fullAddress",
"detailAddress");
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(country) || string.IsNullOrWhiteSpace(zip) || string.IsNullOrWhiteSpace(address))
{
continue;
}
return new OrderParcelReceiverInput
{
RecipientsName = name,
CompanyName = ReadText(candidate, "companyName", "company"),
Phone = ReadText(candidate, "phone", "mobile", "phone_number", "phoneNumber"),
TelePhone = ReadText(candidate, "telePhone", "tel"),
Email = ReadText(candidate, "email") ?? ReadText(node, "buyer_email"),
CountryCode = country,
Province = ReadText(candidate, "province", "state", "regionState") ?? province,
City = ReadText(candidate, "city") ?? city,
StreetAddress1 = address,
StreetAddress2 = ReadText(candidate, "streetAddress2", "address_line2", "address2"),
ZipCode = zip
};
}
return null;
}
///
/// 解析 TikTok recipient_address.district_info:L0 国家、L1 州省、L3 城市。
///
private static (string? Province, string? City, string? CountryCode) ParseTiktokDistrictInfo(JsonNode candidate)
{
var districts = candidate["district_info"]?.AsArray();
if (districts == null || districts.Count == 0)
{
return (null, null, null);
}
string? province = null;
string? city = null;
string? country = null;
foreach (var district in districts)
{
if (district == null) continue;
var level = district["address_level"]?.ToString();
var name = district["address_name"]?.ToString()?.Trim();
var iso = district["iso_code"]?.ToString()?.Trim();
switch (level)
{
case "L0":
country = !string.IsNullOrWhiteSpace(iso) ? iso : name;
break;
case "L1":
province = name;
break;
case "L3":
city = name;
break;
case "L2" when string.IsNullOrWhiteSpace(city):
city = name;
break;
}
}
return (province, city, country);
}
private static string? ReadText(JsonNode node, params string[] keys)
{
foreach (var key in keys)
{
var value = node[key]?.ToString()?.Trim();
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return null;
}
private static void ValidateReceiver(OrderParcelReceiverInput? receiver)
{
if (receiver == null
|| string.IsNullOrWhiteSpace(receiver.RecipientsName)
|| string.IsNullOrWhiteSpace(receiver.CountryCode)
|| string.IsNullOrWhiteSpace(receiver.ZipCode)
|| string.IsNullOrWhiteSpace(receiver.StreetAddress1))
{
throw new BusinessException("收件人信息不完整,请同步平台订单或在匹配时补充收件人");
}
}
///
/// 从 PlatformExtraJson / ExtraJson 解析收件人并写入结构化字段(已有值不覆盖)。
///
public static void TryFillRecipientFields(SalesStoreOrder order, string? extraJson = null)
{
if (!string.IsNullOrWhiteSpace(order.RecipientName)
&& !string.IsNullOrWhiteSpace(order.RecipientAddress1)
&& !string.IsNullOrWhiteSpace(order.RecipientZipCode)
&& !string.IsNullOrWhiteSpace(order.RecipientCountry ?? order.OrderCountry))
{
return;
}
var json = !string.IsNullOrWhiteSpace(extraJson) ? extraJson : order.PlatformExtraJson;
if (string.IsNullOrWhiteSpace(json))
{
return;
}
try
{
var node = JsonNode.Parse(json);
if (node == null)
{
return;
}
var parsed = ParseReceiverNode(node);
if (parsed == null)
{
return;
}
order.RecipientName ??= parsed.RecipientsName;
order.RecipientCompany ??= parsed.CompanyName;
order.RecipientPhone ??= parsed.Phone;
order.RecipientTelePhone ??= parsed.TelePhone;
order.RecipientEmail ??= parsed.Email;
order.RecipientCountry ??= parsed.CountryCode;
order.RecipientProvince ??= parsed.Province;
order.RecipientCity ??= parsed.City;
order.RecipientZipCode ??= parsed.ZipCode;
order.RecipientAddress1 ??= parsed.StreetAddress1;
order.RecipientAddress2 ??= parsed.StreetAddress2;
if (string.IsNullOrWhiteSpace(order.OrderCountry) && !string.IsNullOrWhiteSpace(parsed.CountryCode))
{
order.OrderCountry = parsed.CountryCode;
}
}
catch (JsonException)
{
// ignore
}
}
}