using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
namespace Oms.Application.Integrations.SmallBagTrack;
///
/// 揽收大包轨迹同步结果(UpsertBigBag → Link → Track A0201)。
///
public class PickupBigBagTrackSyncResult
{
public string PickupNo { get; set; } = string.Empty;
public bool Success { get; set; }
public string? FailStep { get; set; }
public string? Message { get; set; }
public SmallBagTrackUpsertResult? UpsertBigBag { get; set; }
public SmallBagTrackUpsertResult? Link { get; set; }
public SmallBagTrackUpsertResult? TrackUpsert { get; set; }
}
///
/// 揽收单预报下游成功后 / 手动:按序 UpsertBigBag → Link → Track/Upsert(A0201)。
///
public interface IPickupBigBagTrackSyncer
{
Task SyncAfterDownstreamForwardAsync(PickupOrder pickup, CancellationToken cancellationToken = default);
///
/// 同步执行三步并返回逐步请求/响应(手动「创建轨迹订单」联调)。
///
Task SyncDebugAsync(PickupOrder pickup, CancellationToken cancellationToken = default);
}
internal sealed class PickupBigBagTrackSyncer(
ISmallBagTrackOrderClient trackClient,
IPickupOrderLineRepository pickupOrderLineRepository,
ISalesProductRepository salesProductRepository,
IServiceChannelRepository serviceChannelRepository,
IHttpContextAccessor httpContextAccessor,
ILogger logger) : IPickupBigBagTrackSyncer
{
public async Task SyncAfterDownstreamForwardAsync(
PickupOrder pickup,
CancellationToken cancellationToken = default)
{
await SyncDebugAsync(pickup, cancellationToken);
}
public async Task SyncDebugAsync(
PickupOrder pickup,
CancellationToken cancellationToken = default)
{
var result = new PickupBigBagTrackSyncResult
{
PickupNo = pickup?.PickupNo?.Trim() ?? string.Empty
};
if (pickup == null || string.IsNullOrWhiteSpace(pickup.PickupNo))
{
result.Success = false;
result.FailStep = "validate";
result.Message = "pickup or PickupNo empty";
logger.LogWarning("Skip big-bag track sync: pickup or PickupNo empty");
return result;
}
var bagNo = pickup.PickupNo.Trim();
result.PickupNo = bagNo;
var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString();
if (string.IsNullOrWhiteSpace(authorization))
{
authorization = null;
}
try
{
var (channel, product) = await ResolveChannelProductAsync(pickup.CarrierService, cancellationToken);
var country = ResolveCountry(pickup);
// 1) UpsertBigBag:client 取 product(销售产品编码)
var upsertResult = await trackClient.UpsertBigBagAsync(
new SmallBagTrackUpsertBigBagRequest
{
Client = product ?? string.Empty,
BagNo = bagNo,
OtherNo = pickup.CarrierWaybillNo,
Channel = channel,
Product = product,
Country = country,
Authorization = authorization
},
cancellationToken);
result.UpsertBigBag = upsertResult;
if (upsertResult.Skipped)
{
result.Success = false;
result.FailStep = "UpsertBigBag";
result.Message = upsertResult.SkipReason ?? "skipped";
logger.LogDebug(
"UpsertBigBag skipped for pickup {PickupNo}: {Reason}",
bagNo,
upsertResult.SkipReason);
return result;
}
if (!upsertResult.Success)
{
result.Success = false;
result.FailStep = "UpsertBigBag";
result.Message = $"HTTP {upsertResult.StatusCode}";
logger.LogWarning(
"Stop big-bag track sync after UpsertBigBag failure for pickup {PickupNo}",
bagNo);
return result;
}
// 2) Link(无明细单号则跳过,继续写轨迹)
var lines = await pickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id, cancellationToken);
var serviceNos = lines
.Select(x => x.ServiceNo)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (serviceNos.Count > 0)
{
var linkResult = await trackClient.LinkOrdersAsync(
bagNo,
SmallBagTrackLifecycleEvents.BigBagOrderType,
serviceNos,
SmallBagTrackLifecycleEvents.OrderType,
authorization,
cancellationToken);
result.Link = linkResult;
if (linkResult.Skipped)
{
logger.LogDebug(
"Link skipped for pickup {PickupNo}: {Reason}",
bagNo,
linkResult.SkipReason);
}
else if (!linkResult.Success)
{
result.Success = false;
result.FailStep = "Link";
result.Message = $"HTTP {linkResult.StatusCode}";
logger.LogWarning(
"Stop big-bag track sync after Link failure for pickup {PickupNo}",
bagNo);
return result;
}
}
else
{
logger.LogDebug("Skip Link for pickup {PickupNo}: no bound ServiceNo", bagNo);
}
// 3) Track Upsert A0201
var trackResult = await trackClient.AppendLifecycleTrackResultAsync(
bagNo,
SmallBagTrackLifecycleEvents.PickupCreateTrackCode,
SmallBagTrackLifecycleEvents.BigBagOrderType,
authorization,
cancellationToken);
result.TrackUpsert = trackResult;
if (trackResult.Skipped)
{
result.Success = false;
result.FailStep = "TrackUpsert";
result.Message = trackResult.SkipReason ?? "skipped";
logger.LogDebug(
"Track A0201 skipped for pickup {PickupNo}: {Reason}",
bagNo,
trackResult.SkipReason);
return result;
}
if (!trackResult.Success)
{
result.Success = false;
result.FailStep = "TrackUpsert";
result.Message = $"HTTP {trackResult.StatusCode}";
logger.LogWarning(
"Track A0201 failed for pickup {PickupNo}: {StatusCode} {Body}",
bagNo,
trackResult.StatusCode,
trackResult.ResponseBody);
return result;
}
result.Success = true;
result.Message = "ok";
return result;
}
catch (Exception ex)
{
result.Success = false;
result.FailStep ??= "exception";
result.Message = ex.Message;
logger.LogWarning(ex, "Big-bag track sync failed for pickup {PickupNo}", bagNo);
return result;
}
}
private async Task<(string? Channel, string? Product)> ResolveChannelProductAsync(
string? carrierService,
CancellationToken cancellationToken)
{
var salesProductCode = carrierService?.Trim();
if (string.IsNullOrWhiteSpace(salesProductCode))
{
return (null, null);
}
var product = await salesProductRepository.GetByProductCodeAsync(salesProductCode, cancellationToken);
if (product == null)
{
return (null, salesProductCode);
}
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id, cancellationToken);
var channelId = providers
.Where(x => x.ServiceChannelId is > 0)
.OrderBy(x => x.SerialNumber)
.Select(x => x.ServiceChannelId!.Value)
.FirstOrDefault();
if (channelId <= 0)
{
return (null, product.ProductCode);
}
var channel = await serviceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId, cancellationToken);
var channelCode = channel?.ChannelCode?.Trim();
return (
string.IsNullOrWhiteSpace(channelCode) ? null : channelCode,
product.ProductCode);
}
private static string ResolveCountry(PickupOrder pickup)
{
var isReverse = string.Equals(
PickupDirectionCodes.NormalizeOrDefault(pickup.Direction),
PickupDirectionCodes.Reverse,
StringComparison.OrdinalIgnoreCase);
if (isReverse)
{
return string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
? "CN"
: pickup.SenderCountryCode.Trim().ToUpperInvariant();
}
// 正向:目的仓,仓库无国别字段,默认 CN
return "CN";
}
}