feat: ????????????? A0101/A0301/A0701
Fix UpsertSmallBag ApiCode/TrackNo assembly and MQ-disabled direct fallback; append lifecycle tracks on parcel create, inbound, and outbound. Redact channel fields for customer order list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3d5eaa223a
commit
1545bc385f
@ -4,6 +4,15 @@ public interface ISmallBagTrackOrderClient
|
|||||||
{
|
{
|
||||||
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
|
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert)。
|
||||||
|
/// </summary>
|
||||||
|
Task AppendLifecycleTrackAsync(
|
||||||
|
string orderNo,
|
||||||
|
string trackCode,
|
||||||
|
string? authorization = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同步调用并返回完整请求/响应(仅调试)。
|
/// 同步调用并返回完整请求/响应(仅调试)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -6,4 +6,9 @@ namespace Oms.Application.Integrations.SmallBagTrack;
|
|||||||
public interface ISmallBagTrackOrderNotifier
|
public interface ISmallBagTrackOrderNotifier
|
||||||
{
|
{
|
||||||
void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null);
|
void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步追加生命周期轨迹节点。
|
||||||
|
/// </summary>
|
||||||
|
void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小包订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
|
||||||
|
/// </summary>
|
||||||
|
internal static class SmallBagTrackLifecycleEvents
|
||||||
|
{
|
||||||
|
public const string CreateTrackCode = "A0101";
|
||||||
|
|
||||||
|
public const string InboundTrackCode = "A0301";
|
||||||
|
|
||||||
|
public const string OutboundTrackCode = "A0701";
|
||||||
|
|
||||||
|
public const string TrackPlace = "SZ";
|
||||||
|
|
||||||
|
public const float TrackTimeZone = 8f;
|
||||||
|
|
||||||
|
/// <summary>轨迹订单类型:小包。</summary>
|
||||||
|
public const int OrderType = 1;
|
||||||
|
|
||||||
|
/// <summary>数据来源:内部系统。</summary>
|
||||||
|
public const int Source = 2;
|
||||||
|
}
|
||||||
@ -44,8 +44,24 @@ internal sealed class SmallBagTrackOrderClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo);
|
logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo);
|
||||||
|
|
||||||
|
if (request.AppendInitialTrackOnSuccess)
|
||||||
|
{
|
||||||
|
await AppendLifecycleTrackAsync(
|
||||||
|
request.OrderNo,
|
||||||
|
SmallBagTrackLifecycleEvents.CreateTrackCode,
|
||||||
|
request.Authorization,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task AppendLifecycleTrackAsync(
|
||||||
|
string orderNo,
|
||||||
|
string trackCode,
|
||||||
|
string? authorization = null,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
AppendLifecycleTrackCoreAsync(orderNo, trackCode, authorization, cancellationToken);
|
||||||
|
|
||||||
public async Task<SmallBagTrackUpsertResult> UpsertDebugAsync(
|
public async Task<SmallBagTrackUpsertResult> UpsertDebugAsync(
|
||||||
SmallBagTrackUpsertRequest request,
|
SmallBagTrackUpsertRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@ -67,14 +83,120 @@ internal sealed class SmallBagTrackOrderClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var appKey = settings.AppKey.Trim();
|
var appKey = settings.AppKey.Trim();
|
||||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
||||||
var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim());
|
|
||||||
var body = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request);
|
var body = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request);
|
||||||
var url = SmallBagTrackUpsertApiRequest.BuildEndpointUrl(settings.BaseUrl);
|
var url = SmallBagTrackUpsertApiRequest.BuildEndpointUrl(settings.BaseUrl);
|
||||||
var authorization = ResolveAuthorization(request);
|
var authorization = ResolveAuthorization(request);
|
||||||
var curlCommand = SmallBagTrackCurlBuilder.Build(url, appKey, timestamp, sign, authorization, body, JsonOptions);
|
|
||||||
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
|
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
|
||||||
|
|
||||||
|
var response = await SendSignedPostAsync(
|
||||||
|
settings,
|
||||||
|
appKey,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
authorization,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var curlCommand = SmallBagTrackCurlBuilder.Build(
|
||||||
|
url,
|
||||||
|
appKey,
|
||||||
|
response.Timestamp,
|
||||||
|
response.Sign,
|
||||||
|
authorization,
|
||||||
|
body,
|
||||||
|
JsonOptions);
|
||||||
|
|
||||||
|
return new SmallBagTrackUpsertResult
|
||||||
|
{
|
||||||
|
Skipped = false,
|
||||||
|
Success = response.Response.IsSuccessStatusCode,
|
||||||
|
Url = url,
|
||||||
|
RequestJson = requestJson,
|
||||||
|
CurlCommand = curlCommand,
|
||||||
|
StatusCode = (int)response.Response.StatusCode,
|
||||||
|
ResponseBody = response.ResponseBody
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AppendLifecycleTrackCoreAsync(
|
||||||
|
string orderNo,
|
||||||
|
string trackCode,
|
||||||
|
string? authorization,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(orderNo))
|
||||||
|
{
|
||||||
|
logger.LogWarning("Skip lifecycle track append: orderNo empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(trackCode))
|
||||||
|
{
|
||||||
|
logger.LogWarning("Skip lifecycle track append for order {OrderNo}: trackCode empty", orderNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = await configProvider.GetAsync(cancellationToken);
|
||||||
|
if (!settings.Enabled
|
||||||
|
|| string.IsNullOrWhiteSpace(settings.BaseUrl)
|
||||||
|
|| string.IsNullOrWhiteSpace(settings.AppKey)
|
||||||
|
|| string.IsNullOrWhiteSpace(settings.AppSecret))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appKey = settings.AppKey.Trim();
|
||||||
|
var trackTime = DateTime.Now;
|
||||||
|
var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
|
||||||
|
appKey,
|
||||||
|
orderNo.Trim(),
|
||||||
|
trackCode.Trim(),
|
||||||
|
trackTime);
|
||||||
|
var url = SmallBagTrackUpsertTrackApiRequest.BuildEndpointUrl(settings.BaseUrl);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await SendSignedPostAsync(
|
||||||
|
settings,
|
||||||
|
appKey,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
authorization,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (response.Response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Lifecycle track {TrackCode} appended for order {OrderNo} at {TrackPlace}",
|
||||||
|
trackCode,
|
||||||
|
orderNo,
|
||||||
|
SmallBagTrackLifecycleEvents.TrackPlace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning(
|
||||||
|
"Lifecycle track {TrackCode} append failed for order {OrderNo}: {StatusCode} {Body}",
|
||||||
|
trackCode,
|
||||||
|
orderNo,
|
||||||
|
(int)response.Response.StatusCode,
|
||||||
|
response.ResponseBody);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Lifecycle track {TrackCode} append failed for order {OrderNo}", trackCode, orderNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SignedTrackHttpResponse> SendSignedPostAsync<TBody>(
|
||||||
|
SmallBagTrackOptions settings,
|
||||||
|
string appKey,
|
||||||
|
string url,
|
||||||
|
TBody body,
|
||||||
|
string? authorization,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim());
|
||||||
|
|
||||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
|
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
httpRequest.Headers.TryAddWithoutValidation("appKey", appKey);
|
httpRequest.Headers.TryAddWithoutValidation("appKey", appKey);
|
||||||
httpRequest.Headers.TryAddWithoutValidation("timestamp", timestamp.ToString());
|
httpRequest.Headers.TryAddWithoutValidation("timestamp", timestamp.ToString());
|
||||||
@ -87,16 +209,7 @@ internal sealed class SmallBagTrackOrderClient(
|
|||||||
|
|
||||||
using var response = await client.SendAsync(httpRequest, cancellationToken);
|
using var response = await client.SendAsync(httpRequest, cancellationToken);
|
||||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
return new SmallBagTrackUpsertResult
|
return new SignedTrackHttpResponse(response, responseBody, timestamp, sign);
|
||||||
{
|
|
||||||
Skipped = false,
|
|
||||||
Success = response.IsSuccessStatusCode,
|
|
||||||
Url = url,
|
|
||||||
RequestJson = requestJson,
|
|
||||||
CurlCommand = curlCommand,
|
|
||||||
StatusCode = (int)response.StatusCode,
|
|
||||||
ResponseBody = responseBody
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? ResolveAuthorization(SmallBagTrackUpsertRequest request)
|
private string? ResolveAuthorization(SmallBagTrackUpsertRequest request)
|
||||||
@ -125,4 +238,10 @@ internal sealed class SmallBagTrackOrderClient(
|
|||||||
Skipped = true,
|
Skipped = true,
|
||||||
SkipReason = reason
|
SkipReason = reason
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private sealed record SignedTrackHttpResponse(
|
||||||
|
HttpResponseMessage Response,
|
||||||
|
string ResponseBody,
|
||||||
|
long Timestamp,
|
||||||
|
string Sign);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,11 @@ internal sealed class SmallBagTrackOrderNotifier(
|
|||||||
{
|
{
|
||||||
using var scope = scopeFactory.CreateScope();
|
using var scope = scopeFactory.CreateScope();
|
||||||
var coordinator = scope.ServiceProvider.GetRequiredService<OrderBatchJobCoordinator>();
|
var coordinator = scope.ServiceProvider.GetRequiredService<OrderBatchJobCoordinator>();
|
||||||
await coordinator.PublishTrackUpsertAsync(request, orderId, customerId);
|
var published = await coordinator.PublishTrackUpsertAsync(request, orderId, customerId);
|
||||||
|
if (!published)
|
||||||
|
{
|
||||||
|
await FallbackDirectAsync(request, orderId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -24,12 +28,12 @@ internal sealed class SmallBagTrackOrderNotifier(
|
|||||||
ex,
|
ex,
|
||||||
"RabbitMQ track upsert publish failed, fallback to direct call. orderId={OrderId}",
|
"RabbitMQ track upsert publish failed, fallback to direct call. orderId={OrderId}",
|
||||||
orderId);
|
orderId);
|
||||||
await FallbackDirectAsync(request);
|
await FallbackDirectAsync(request, orderId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FallbackDirectAsync(SmallBagTrackUpsertRequest request)
|
private async Task FallbackDirectAsync(SmallBagTrackUpsertRequest request, int orderId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -39,7 +43,29 @@ internal sealed class SmallBagTrackOrderNotifier(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Fallback UpsertSmallBag failed for order {OrderNo}", request.OrderNo);
|
logger.LogWarning(ex, "Direct UpsertSmallBag failed for orderId={OrderId} orderNo={OrderNo}", orderId, request.OrderNo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<ISmallBagTrackOrderClient>();
|
||||||
|
await client.AppendLifecycleTrackAsync(orderNo, trackCode, authorization);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Async lifecycle track append failed. orderId={OrderId} orderNo={OrderNo} trackCode={TrackCode}",
|
||||||
|
orderId,
|
||||||
|
orderNo,
|
||||||
|
trackCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ public static class SmallBagTrackServiceExtensions
|
|||||||
services.Configure<SmallBagTrackOptions>(configuration.GetSection(SmallBagTrackOptions.SectionName));
|
services.Configure<SmallBagTrackOptions>(configuration.GetSection(SmallBagTrackOptions.SectionName));
|
||||||
services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName);
|
services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName);
|
||||||
services.AddScoped<ISmallBagTrackConfigProvider, SmallBagTrackConfigProvider>();
|
services.AddScoped<ISmallBagTrackConfigProvider, SmallBagTrackConfigProvider>();
|
||||||
|
services.AddScoped<ISmallBagTrackUpsertRequestBuilder, SmallBagTrackUpsertRequestBuilder>();
|
||||||
services.AddScoped<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
|
services.AddScoped<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
|
||||||
services.AddSingleton<ISmallBagTrackOrderNotifier, SmallBagTrackOrderNotifier>();
|
services.AddSingleton<ISmallBagTrackOrderNotifier, SmallBagTrackOrderNotifier>();
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public class SmallBagTrackUpsertRequest
|
|||||||
public string Product { get; set; } = string.Empty;
|
public string Product { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 轨迹服务商 code(对应轨迹 ServiceProviderCode,取订单 DownstreamSupplierCode)。
|
/// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先 ChannelForecastCode)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ApiCode { get; set; } = string.Empty;
|
public string ApiCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
@ -28,6 +28,11 @@ public class SmallBagTrackUpsertRequest
|
|||||||
/// 转发到轨迹网关的 Authorization(异步入队前从当前 HTTP 请求捕获)。
|
/// 转发到轨迹网关的 Authorization(异步入队前从当前 HTTP 请求捕获)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Authorization { get; set; }
|
public string? Authorization { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UpsertSmallBag 成功后是否追加创单默认轨迹(A0101/SZ)。
|
||||||
|
/// </summary>
|
||||||
|
public bool AppendInitialTrackOnSuccess { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SmallBagTrackConsigneeRequest
|
public class SmallBagTrackConsigneeRequest
|
||||||
|
|||||||
@ -0,0 +1,147 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
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 优先 ChannelForecastCode,TrackNo 缺省用 ServiceNo。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class SmallBagTrackUpsertRequestBuilder(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
ICustomerRepository customerRepository,
|
||||||
|
IServiceChannelRepository serviceChannelRepository,
|
||||||
|
ISalesProductRepository salesProductRepository) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = order.ChannelForecastCode?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromOrder))
|
||||||
|
{
|
||||||
|
return fromOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channelCode = order.ChannelCode?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(channelCode))
|
||||||
|
{
|
||||||
|
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
|
||||||
|
var forecast = channel?.ForecastCode?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(forecast))
|
||||||
|
{
|
||||||
|
return forecast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var provisional = await ResolveProvisionalApiCodeFromProductAsync(order, cancellationToken);
|
||||||
|
return provisional ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创单尚未排货时,用销售产品排货规则里首个渠道的 ForecastCode 作为轨迹 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);
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id);
|
||||||
|
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 forecast = channel.ForecastCode?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(forecast))
|
||||||
|
{
|
||||||
|
return forecast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||||
|
|
||||||
|
internal sealed class SmallBagTrackUpsertTrackApiRequest
|
||||||
|
{
|
||||||
|
public string AppKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int OrderType { get; set; }
|
||||||
|
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool IsAdd { get; set; } = true;
|
||||||
|
|
||||||
|
public string TrackCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string TrackPlace { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime TrackTime { get; set; }
|
||||||
|
|
||||||
|
public float TrackTimeZone { get; set; }
|
||||||
|
|
||||||
|
public int Source { get; set; }
|
||||||
|
|
||||||
|
public static SmallBagTrackUpsertTrackApiRequest CreateLifecycleEvent(
|
||||||
|
string appKey,
|
||||||
|
string orderNo,
|
||||||
|
string trackCode,
|
||||||
|
DateTime trackTime) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AppKey = appKey,
|
||||||
|
OrderType = SmallBagTrackLifecycleEvents.OrderType,
|
||||||
|
OrderNo = orderNo,
|
||||||
|
IsAdd = true,
|
||||||
|
TrackCode = trackCode,
|
||||||
|
TrackPlace = SmallBagTrackLifecycleEvents.TrackPlace,
|
||||||
|
TrackTime = trackTime,
|
||||||
|
TrackTimeZone = SmallBagTrackLifecycleEvents.TrackTimeZone,
|
||||||
|
Source = SmallBagTrackLifecycleEvents.Source
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string BuildEndpointUrl(string baseUrl) =>
|
||||||
|
$"{baseUrl.Trim().TrimEnd('/')}/ProviderService/Track/Upsert";
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
IOrderRepository orderRepository,
|
IOrderRepository orderRepository,
|
||||||
ICountryRepository countryRepository,
|
ICountryRepository countryRepository,
|
||||||
ISmallBagTrackOrderClient smallBagTrackOrderClient,
|
ISmallBagTrackOrderClient smallBagTrackOrderClient,
|
||||||
|
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
|
||||||
OrderDownstreamCredentialResolver downstreamCredentialResolver,
|
OrderDownstreamCredentialResolver downstreamCredentialResolver,
|
||||||
IItfxForwardClient itfxForwardClient,
|
IItfxForwardClient itfxForwardClient,
|
||||||
OrderBatchCreateActionHandler createActionHandler,
|
OrderBatchCreateActionHandler createActionHandler,
|
||||||
@ -79,7 +80,7 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法同步轨迹");
|
return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法同步轨迹");
|
||||||
}
|
}
|
||||||
|
|
||||||
request = await BuildTrackRequestAsync(order, cancellationToken);
|
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await smallBagTrackOrderClient.UpsertAsync(request, cancellationToken);
|
await smallBagTrackOrderClient.UpsertAsync(request, cancellationToken);
|
||||||
@ -147,30 +148,4 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
|
|
||||||
return OrderBatchActionExecuteResult.Ok();
|
return OrderBatchActionExecuteResult.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SmallBagTrackUpsertRequest> BuildTrackRequestAsync(
|
|
||||||
Domain.OrdersAggregate.Entities.Order order,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var recipients = await orderRepository.GetRecipientsByOrderIdsAsync([order.Id], cancellationToken);
|
|
||||||
var recipient = recipients.FirstOrDefault();
|
|
||||||
|
|
||||||
return new SmallBagTrackUpsertRequest
|
|
||||||
{
|
|
||||||
OrderNo = order.ServiceNo ?? string.Empty,
|
|
||||||
TrackNo = order.TrackNo?.Trim() ?? string.Empty,
|
|
||||||
OtherNo = order.ClientNo?.Trim() ?? string.Empty,
|
|
||||||
Channel = order.ChannelCode?.Trim() ?? string.Empty,
|
|
||||||
Product = order.ProductCode?.Trim() ?? string.Empty,
|
|
||||||
ApiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty,
|
|
||||||
Consignee = new SmallBagTrackConsigneeRequest
|
|
||||||
{
|
|
||||||
CountryCode = order.ConsigneeCountry?.Trim() ?? string.Empty,
|
|
||||||
Province = recipient?.Province?.Trim() ?? string.Empty,
|
|
||||||
Postcode = recipient?.ZipCode?.Trim() ?? string.Empty,
|
|
||||||
City = recipient?.City?.Trim() ?? string.Empty,
|
|
||||||
Email = recipient?.Email?.Trim() ?? string.Empty,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -366,12 +366,12 @@ public sealed class OrderBatchJobCoordinator(
|
|||||||
output.TaskName = string.IsNullOrWhiteSpace(meta.TaskName) ? null : meta.TaskName.Trim();
|
output.TaskName = string.IsNullOrWhiteSpace(meta.TaskName) ? null : meta.TaskName.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId)
|
public async Task<bool> PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId)
|
||||||
{
|
{
|
||||||
if (!publisher.IsEnabled)
|
if (!publisher.IsEnabled)
|
||||||
{
|
{
|
||||||
logger.LogDebug("RabbitMQ disabled, skip track upsert publish for order {OrderId}", orderId);
|
logger.LogDebug("RabbitMQ disabled, track upsert will use direct call. orderId={OrderId}", orderId);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = new OrderBatchJobMessage
|
var message = new OrderBatchJobMessage
|
||||||
@ -387,6 +387,7 @@ public sealed class OrderBatchJobCoordinator(
|
|||||||
};
|
};
|
||||||
|
|
||||||
await publisher.PublishAsync(message);
|
await publisher.PublishAsync(message);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrderBatchJobOutput?> GetJobAsync(int jobId, CancellationToken cancellationToken = default)
|
public async Task<OrderBatchJobOutput?> GetJobAsync(int jobId, CancellationToken cancellationToken = default)
|
||||||
|
|||||||
@ -89,7 +89,7 @@ public partial class OrderService
|
|||||||
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
||||||
var customerMap = customers.ToDictionary(x => x.Id);
|
var customerMap = customers.ToDictionary(x => x.Id);
|
||||||
|
|
||||||
return orders.Select(order =>
|
var outputs = orders.Select(order =>
|
||||||
{
|
{
|
||||||
senderMap.TryGetValue(order.Id, out var sender);
|
senderMap.TryGetValue(order.Id, out var sender);
|
||||||
recipientMap.TryGetValue(order.Id, out var recipient);
|
recipientMap.TryGetValue(order.Id, out var recipient);
|
||||||
@ -198,6 +198,26 @@ public partial class OrderService
|
|||||||
ReceivableAmount = feeOutputs.Count == 0 ? null : receivableAmount
|
ReceivableAmount = feeOutputs.Count == 0 ? null : receivableAmount
|
||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
if (OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
||||||
|
{
|
||||||
|
RedactChannelFieldsForCustomer(outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RedactChannelFieldsForCustomer(IEnumerable<OrderOutput> outputs)
|
||||||
|
{
|
||||||
|
foreach (var output in outputs)
|
||||||
|
{
|
||||||
|
output.ChannelCode = null;
|
||||||
|
output.ChannelName = null;
|
||||||
|
output.DownstreamSupplierCode = null;
|
||||||
|
output.DownstreamAppKey = null;
|
||||||
|
output.DownstreamAppId = null;
|
||||||
|
output.ChannelForecastCode = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<object>> BuildOrderExportRowsAsync(IReadOnlyCollection<Order> orders)
|
private async Task<List<object>> BuildOrderExportRowsAsync(IReadOnlyCollection<Order> orders)
|
||||||
|
|||||||
@ -155,7 +155,7 @@ public partial class OrderService
|
|||||||
order.UpdateTime = DateTime.Now;
|
order.UpdateTime = DateTime.Now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
|
||||||
await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver);
|
await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver, appendInitialTrack: true);
|
||||||
await EnqueueGenerateLabelPdfAsync(order);
|
await EnqueueGenerateLabelPdfAsync(order);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -1625,6 +1625,16 @@ public partial class OrderService
|
|||||||
order.ReceiveTime = DateTime.Now;
|
order.ReceiveTime = DateTime.Now;
|
||||||
order.UpdateTime = DateTime.Now;
|
order.UpdateTime = DateTime.Now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
|
||||||
|
EnqueueLifecycleTrackAppend(order, SmallBagTrackLifecycleEvents.InboundTrackCode);
|
||||||
|
|
||||||
|
if (allocated)
|
||||||
|
{
|
||||||
|
var customer = order.CustomerId.HasValue
|
||||||
|
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
||||||
|
: null;
|
||||||
|
await EnqueueSmallBagTrackUpsertAsync(order, customer, receiver: null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1706,6 +1716,8 @@ public partial class OrderService
|
|||||||
order.ShipTime = now;
|
order.ShipTime = now;
|
||||||
order.UpdateTime = now;
|
order.UpdateTime = now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
|
||||||
|
EnqueueLifecycleTrackAppend(order, SmallBagTrackLifecycleEvents.OutboundTrackCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
|
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
|
||||||
@ -1764,9 +1776,11 @@ public partial class OrderService
|
|||||||
private async Task EnqueueSmallBagTrackUpsertAsync(
|
private async Task EnqueueSmallBagTrackUpsertAsync(
|
||||||
Order order,
|
Order order,
|
||||||
Customer? customer,
|
Customer? customer,
|
||||||
OrderParcelReceiverInput? receiver)
|
OrderParcelReceiverInput? receiver,
|
||||||
|
bool appendInitialTrack = false)
|
||||||
{
|
{
|
||||||
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
|
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
|
||||||
|
request.AppendInitialTrackOnSuccess = appendInitialTrack;
|
||||||
var authorization = LazyServiceProvider
|
var authorization = LazyServiceProvider
|
||||||
.GetRequiredService<IHttpContextAccessor>()
|
.GetRequiredService<IHttpContextAccessor>()
|
||||||
.HttpContext?
|
.HttpContext?
|
||||||
@ -1782,6 +1796,28 @@ public partial class OrderService
|
|||||||
SmallBagTrackOrderNotifier.Enqueue(request, order.Id, customer?.Id);
|
SmallBagTrackOrderNotifier.Enqueue(request, order.Id, customer?.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnqueueLifecycleTrackAppend(Order order, string trackCode)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(order.ServiceNo))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorization = LazyServiceProvider
|
||||||
|
.GetRequiredService<IHttpContextAccessor>()
|
||||||
|
.HttpContext?
|
||||||
|
.Request
|
||||||
|
.Headers
|
||||||
|
.Authorization
|
||||||
|
.ToString();
|
||||||
|
|
||||||
|
SmallBagTrackOrderNotifier.EnqueueLifecycleTrackAppend(
|
||||||
|
order.ServiceNo,
|
||||||
|
trackCode,
|
||||||
|
order.Id,
|
||||||
|
string.IsNullOrWhiteSpace(authorization) ? null : authorization);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。
|
/// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1802,37 +1838,12 @@ public partial class OrderService
|
|||||||
Order order,
|
Order order,
|
||||||
OrderParcelReceiverInput? receiverInput = null)
|
OrderParcelReceiverInput? receiverInput = null)
|
||||||
{
|
{
|
||||||
Customer? customer = null;
|
return await SmallBagTrackUpsertRequestBuilder.BuildAsync(order, receiverInput);
|
||||||
if (order.CustomerId.HasValue)
|
|
||||||
{
|
|
||||||
customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
|
||||||
var recipient = recipients.FirstOrDefault();
|
|
||||||
|
|
||||||
var apiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty;
|
|
||||||
|
|
||||||
return new SmallBagTrackUpsertRequest
|
|
||||||
{
|
|
||||||
Client = customer?.CustomerCode?.Trim() ?? string.Empty,
|
|
||||||
OrderNo = order.ServiceNo,
|
|
||||||
TrackNo = order.TrackNo?.Trim() ?? string.Empty,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ISmallBagTrackUpsertRequestBuilder SmallBagTrackUpsertRequestBuilder =>
|
||||||
|
LazyServiceProvider.GetRequiredService<ISmallBagTrackUpsertRequestBuilder>();
|
||||||
|
|
||||||
private ISmallBagTrackOrderClient SmallBagTrackOrderClient =>
|
private ISmallBagTrackOrderClient SmallBagTrackOrderClient =>
|
||||||
LazyServiceProvider.GetRequiredService<ISmallBagTrackOrderClient>();
|
LazyServiceProvider.GetRequiredService<ISmallBagTrackOrderClient>();
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,10 @@ public sealed class OrderE2eFixture : IAsyncLifetime
|
|||||||
public void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null)
|
public void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
Normal file
67
src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using Oms.Application.Integrations.SmallBagTrack;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Oms.Tests;
|
||||||
|
|
||||||
|
public class SmallBagTrackUpsertRequestBuilderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ResolveTrackNo_UsesTrackNoWhenPresent()
|
||||||
|
{
|
||||||
|
var order = new Order
|
||||||
|
{
|
||||||
|
ServiceNo = "IDE2607221859123456",
|
||||||
|
TrackNo = "LP123456789CN"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("LP123456789CN", SmallBagTrackUpsertRequestBuilder.ResolveTrackNo(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveTrackNo_FallsBackToServiceNoWhenTrackNoEmpty()
|
||||||
|
{
|
||||||
|
var order = new Order
|
||||||
|
{
|
||||||
|
ServiceNo = "IDE2607221859123456",
|
||||||
|
TrackNo = null
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("IDE2607221859123456", SmallBagTrackUpsertRequestBuilder.ResolveTrackNo(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateLifecycleEvent_UsesConfiguredDefaults()
|
||||||
|
{
|
||||||
|
var trackTime = new DateTime(2026, 7, 22, 20, 7, 0);
|
||||||
|
var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
|
||||||
|
"APP001",
|
||||||
|
"IDE2607221859123456",
|
||||||
|
SmallBagTrackLifecycleEvents.CreateTrackCode,
|
||||||
|
trackTime);
|
||||||
|
|
||||||
|
Assert.Equal("APP001", body.AppKey);
|
||||||
|
Assert.Equal("IDE2607221859123456", body.OrderNo);
|
||||||
|
Assert.True(body.IsAdd);
|
||||||
|
Assert.Equal("A0101", body.TrackCode);
|
||||||
|
Assert.Equal("SZ", body.TrackPlace);
|
||||||
|
Assert.Equal(trackTime, body.TrackTime);
|
||||||
|
Assert.Equal(8f, body.TrackTimeZone);
|
||||||
|
Assert.Equal(1, body.OrderType);
|
||||||
|
Assert.Equal(2, body.Source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("A0301")]
|
||||||
|
[InlineData("A0701")]
|
||||||
|
public void CreateLifecycleEvent_SupportsInboundAndOutboundCodes(string trackCode)
|
||||||
|
{
|
||||||
|
var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
|
||||||
|
"APP001",
|
||||||
|
"IDE2607221859123456",
|
||||||
|
trackCode,
|
||||||
|
DateTime.Now);
|
||||||
|
|
||||||
|
Assert.Equal(trackCode, body.TrackCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user