< Summary

Information
Class: Services.LabelServices.OrdersOzonShipService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/LabelServices/OrdersOzonShipService.cs
Line coverage
18%
Covered lines: 22
Uncovered lines: 100
Coverable lines: 122
Total lines: 291
Line coverage: 18%
Branch coverage
24%
Covered branches: 18
Total branches: 74
Branch coverage: 24.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SetClient(...)100%210%
ProcessOrderStatusAsync()0%620%
ProcessOrderStatusWithKnownStatusAsync()69.23%962652.94%
BuildShipPackagesAsync()0%1190340%
.cctor()100%210%
WaitForShipSubStatusAsync()0%7280%
RecordFailedShipmentAsync()0%2040%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/LabelServices/OrdersOzonShipService.cs

#LineLine coverage
 1using OzonDomains.Models;
 2using Services.ApiServices.OzonApi;
 3using Services.ApiServices.OzonApi.Filters;
 4using Services.LabelServices;
 5using Services.ParserServices;
 6using Services.ParserServices.HelpDictEnum;
 7
 8namespace Services.LabelServices;
 9
 10public interface IOrdersOzonShipService
 11{
 12    /// <summary>
 13    /// Устанавливает Client-Id/Api-Key Ozon для последующих вызовов этого сервиса. Обязательно
 14    /// вызывать перед <see cref="ProcessOrderStatusAsync"/>/<see cref="RecordFailedShipmentAsync"/> —
 15    /// у сервиса свой собственный (DI, transient) <c>OzonJsonDataBuilder</c>, не связанный с
 16    /// экземпляром вызывающего кода, поэтому его клиент нужно выставлять явно здесь же.
 17    /// </summary>
 18    void SetClient(string clientId, string apiKey);
 19
 20    Task<string> ProcessOrderStatusAsync(
 21        Order order,
 22        string? splitOption,
 23        string? ozonPostingOverride = null,
 24        int? forcedPackageCount = null);
 25
 26    /// <summary>
 27    /// То же, что <see cref="ProcessOrderStatusAsync"/>, но со статусом Ozon, уже полученным
 28    /// вызывающим кодом (например батч-запросом на всю пачку заказов) — экономит по одному
 29    /// последовательному запросу статуса на заказ.
 30    /// </summary>
 31    Task<string> ProcessOrderStatusWithKnownStatusAsync(
 32        Order order,
 33        string? splitOption,
 34        string orderStatus,
 35        string? ozonPostingOverride = null,
 36        int? forcedPackageCount = null);
 37
 38    Task RecordFailedShipmentAsync(
 39        Order order,
 40        Exception ex,
 41        IList<string> failedShipments,
 42        CancellationToken cancellationToken = default);
 43}
 44
 45/// <summary>
 46/// Отгрузка FBS-постингов Ozon и обновление статуса (логика бывшего ProcessAndUpdateOrderStatus).
 47/// </summary>
 48public sealed class OrdersOzonShipService : IOrdersOzonShipService
 49{
 50    private readonly OzonJsonDataBuilder _jsonDataBuilder;
 51    private OzonApiDataManager _ozonApiDataManager;
 52
 453    public OrdersOzonShipService(OzonJsonDataBuilder jsonDataBuilder, OzonApiDataManager ozonApiDataManager)
 54    {
 455        _jsonDataBuilder = jsonDataBuilder;
 456        _ozonApiDataManager = ozonApiDataManager;
 457    }
 58
 59    public void SetClient(string clientId, string apiKey)
 60    {
 061        _jsonDataBuilder.SetClient(clientId, apiKey);
 062        _ozonApiDataManager = _ozonApiDataManager.SetClient(clientId, apiKey);
 063    }
 64
 65    public async Task<string> ProcessOrderStatusAsync(
 66        Order order,
 67        string? splitOption,
 68        string? ozonPostingOverride = null,
 69        int? forcedPackageCount = null)
 70    {
 071        var ozonPostingNumber = ozonPostingOverride
 072                                ?? OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 73
 074        string orderStatus = await _jsonDataBuilder.GetProductSatatus(ozonPostingNumber);
 75
 076        return await ProcessOrderStatusWithKnownStatusAsync(
 077            order, splitOption, orderStatus, ozonPostingOverride, forcedPackageCount);
 078    }
 79
 80    public async Task<string> ProcessOrderStatusWithKnownStatusAsync(
 81        Order order,
 82        string? splitOption,
 83        string orderStatus,
 84        string? ozonPostingOverride = null,
 85        int? forcedPackageCount = null)
 86    {
 487        var ozonPostingNumber = ozonPostingOverride
 488                                ?? OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 89
 490        if (orderStatus != "awaiting_deliver" &&
 491            !string.IsNullOrEmpty(order.ShipmentNumber)
 492            && order.ProductInfo != null
 493            && !string.IsNullOrEmpty(order.ProductInfo.OzonProductId)
 494            && order.Quantity.HasValue
 495            && order.Quantity.Value > 0)
 96        {
 397            if (orderStatus == "not_accepted")
 98            {
 099                var toAwaitingDeliver = await _jsonDataBuilder.SetAwaitingDelivery([ozonPostingNumber]);
 0100                return toAwaitingDeliver.Result
 0101                    ? "Спорный заказ передан к отгрузке."
 0102                    : "Не удалось передать спорный заказ к отгркзке.";
 103            }
 104
 105            // Проверяем ДО ветки "past shipping": cancelled/delivered блокируют печать/отгрузку,
 106            // но cancelled не входит в IsOzonStatusPastShipping (delivering/driver_pickup/sent_by_seller/delivered) —
 107            // без этой проверки код лез бы дальше пытаться отгрузить уже отменённый на Ozon заказ
 108            // и получал в ответ невнятную ошибку от Ozon вместо понятного статуса.
 3109            if (OzonLabelStatusHelper.IsOzonStatusLabelGenerationBlocked(orderStatus))
 110            {
 2111                var blockedStatusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(orderStatus);
 2112                return
 2113                    $"Статус Ozon: {blockedStatusName} — печать этикетки и отгрузка через Ozon API недоступны.";
 114            }
 115
 1116            if (OzonLabelStatusHelper.IsOzonStatusPastShipping(orderStatus))
 117            {
 1118                var statusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(orderStatus);
 1119                return $"Статус Ozon: {statusName} — повторная отгрузка не требуется.";
 120            }
 121
 0122            var packages = await BuildShipPackagesAsync(order, splitOption, ozonPostingNumber, forcedPackageCount);
 123
 0124            await _jsonDataBuilder.ShipFbsPosting(ozonPostingNumber, packages, withAdditionalData: true);
 125
 0126            string newStatus = await WaitForShipSubStatusAsync(ozonPostingNumber);
 0127            if (newStatus == "ship_failed")
 128            {
 0129                await _jsonDataBuilder.ShipFbsPosting(ozonPostingNumber, packages, withAdditionalData: true);
 0130                newStatus = await WaitForShipSubStatusAsync(ozonPostingNumber);
 131            }
 132
 0133            string msg = OzonStatus.GetSubStatus(newStatus) + '.';
 0134            if (newStatus == "posting_registered")
 135            {
 0136                msg +=
 0137                    " Мы регистрируем ваше отправление в сервисе доставки партнера Озон, это может занять до 30 минут. "
 0138                    "Как только регистрация закончится, вы увидите трек-номер и сможете передать отправление в отгрузку"
 139            }
 140
 0141            return msg;
 142        }
 143
 1144        return "Статус заказа не требует обновления";
 4145    }
 146
 147    /// <summary>
 148    /// Отправление с несколькими разными товарами отгружается целиком: в packages должны быть
 149    /// все SKU отправления, иначе Ozon отвечает HAS_INCORRECT_PRODUCT_QUANTITY. Отправление
 150    /// с одним SKU — прежняя логика (количество из Ozon, разбиение по коробкам по splitOption).
 151    /// </summary>
 152    private async Task<List<FbsShipPackage>> BuildShipPackagesAsync(
 153        Order order,
 154        string? splitOption,
 155        string ozonPostingNumber,
 156        int? forcedPackageCount)
 157    {
 0158        var postingProducts = await _ozonApiDataManager.GetPostingShipProductsAsync(ozonPostingNumber);
 0159        if (postingProducts.Count > 1)
 160        {
 0161            var packagePerUnit = forcedPackageCount is > 0 || !string.IsNullOrEmpty(splitOption);
 0162            return OzonFbsShipPackageBuilder.Build(postingProducts, packagePerUnit);
 163        }
 164
 0165        string realProductId = await _jsonDataBuilder.GetProductId(ozonPostingNumber);
 0166        if (!long.TryParse(realProductId, out var realProductIdValue) || realProductIdValue <= 0)
 167        {
 0168            throw new InvalidOperationException(
 0169                $"Не удалось определить product_id для отправления {ozonPostingNumber}.");
 170        }
 171
 0172        var effectiveSplitOption = splitOption;
 0173        var shipQuantity = order.Quantity.GetValueOrDefault();
 0174        if (forcedPackageCount is > 0)
 175        {
 0176            effectiveSplitOption = null;
 0177            shipQuantity = forcedPackageCount.Value;
 178        }
 0179        else if (OzonPostingNumberHelper.IsSplitChildPosting(order.ShipmentNumber) &&
 0180                 OzonPostingNumberHelper.IsStudioLocalSplitPosting(order.ShipmentNumber))
 181        {
 0182            effectiveSplitOption = null;
 0183            shipQuantity = 1;
 184        }
 0185        else if (OzonPostingNumberHelper.IsSplitChildPosting(order.ShipmentNumber))
 186        {
 0187            effectiveSplitOption = null;
 0188            shipQuantity = 1;
 189        }
 190
 0191        var ozonShipQuantity =
 0192            await _ozonApiDataManager.GetPostingProductQuantityAsync(ozonPostingNumber, realProductIdValue);
 0193        if (ozonShipQuantity > 0 && shipQuantity != ozonShipQuantity)
 194        {
 0195            shipQuantity = ozonShipQuantity;
 196        }
 197
 0198        List<FbsShipPackage> packages = new();
 199
 0200        if (forcedPackageCount is > 0 || (!string.IsNullOrEmpty(effectiveSplitOption) && shipQuantity > 1))
 201        {
 0202            for (int i = 0; i < shipQuantity; i++)
 203            {
 0204                packages.Add(new FbsShipPackage
 0205                {
 0206                    Products = new List<FbsShipProduct>
 0207                    {
 0208                        new()
 0209                        {
 0210                            ProductId = realProductIdValue,
 0211                            Quantity = 1
 0212                        }
 0213                    }
 0214                });
 215            }
 216        }
 217        else
 218        {
 0219            packages.Add(new FbsShipPackage
 0220            {
 0221                Products = new List<FbsShipProduct>
 0222                {
 0223                    new()
 0224                    {
 0225                        ProductId = realProductIdValue,
 0226                        Quantity = shipQuantity
 0227                    }
 0228                }
 0229            });
 230        }
 231
 0232        return packages;
 0233    }
 234
 235    private const int ShipSubStatusPollAttempts = 8;
 0236    private static readonly TimeSpan ShipSubStatusPollDelay = TimeSpan.FromMilliseconds(250);
 237
 238    /// <summary>
 239    /// Ozon применяет результат отгрузки не мгновенно: сразу после /v4/posting/fbs/ship подстатус
 240    /// какое-то время остаётся ship_pending (или ответ пустой). Вместо фиксированной паузы в 2 с
 241    /// опрашиваем подстатус короткими интервалами и выходим сразу, как только он определился.
 242    /// </summary>
 243    private async Task<string> WaitForShipSubStatusAsync(string ozonPostingNumber)
 244    {
 0245        var status = string.Empty;
 0246        for (var attempt = 0; attempt < ShipSubStatusPollAttempts; attempt++)
 247        {
 0248            if (attempt > 0)
 249            {
 0250                await Task.Delay(ShipSubStatusPollDelay);
 251            }
 252
 0253            status = await _jsonDataBuilder.GetProductSubSatatus(ozonPostingNumber);
 0254            if (!string.IsNullOrEmpty(status) && status != "ship_pending")
 255            {
 0256                return status;
 257            }
 258        }
 259
 0260        return status;
 0261    }
 262
 263    public async Task RecordFailedShipmentAsync(
 264        Order order,
 265        Exception ex,
 266        IList<string> failedShipments,
 267        CancellationToken cancellationToken = default)
 268    {
 269        _ = cancellationToken;
 0270        string? ozonStatusName = null;
 0271        if (OzonLabelPrintErrorHelper.NeedsCurrentOzonStatus(ex.Message))
 272        {
 273            try
 274            {
 0275                var currentStatus = await _jsonDataBuilder.GetProductSatatus(
 0276                    OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber));
 0277                ozonStatusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(currentStatus);
 0278            }
 0279            catch
 280            {
 281                // статус для подсказки не критичен
 0282            }
 283        }
 284
 0285        failedShipments.Add(
 0286            OzonLabelPrintErrorHelper.FormatShipmentError(
 0287                order.ShipmentNumber ?? string.Empty,
 0288                ex.Message,
 0289                ozonStatusName));
 0290    }
 291}