< Summary

Information
Class: OzonOrdersWeb.TransactionServices.OrderToSupplierOzonShipResult
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/OrderToSupplierOzonShipService.cs
Line coverage
15%
Covered lines: 4
Uncovered lines: 22
Coverable lines: 26
Total lines: 271
Line coverage: 15.3%
Branch coverage
0%
Covered branches: 0
Total branches: 10
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Entries()100%11100%
get_SuccessCount()100%210%
get_FailedCount()0%620%
get_SkippedCount()100%210%
get_EligibleOrderIds()0%620%
.ctor(...)100%11100%
Empty()100%210%
ToHtmlBlock()0%4260%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/OrderToSupplierOzonShipService.cs

#LineLine coverage
 1using System.Net;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains;
 4using OzonDomains.Models;
 5using OzonRepositories.Context;
 6using Services.ApiServices.OzonApi;
 7using Services.ApiServices.OzonApi.Filters;
 8using Services.DataServices;
 9using Services.LabelServices;
 10using Services.ParserServices.HelpDictEnum;
 11
 12namespace OzonOrdersWeb.TransactionServices;
 13
 14public sealed class OrderToSupplierOzonShipService
 15{
 16    private readonly ApplicationDbContext _context;
 17    private readonly OzonApiDataManager _ozonApiDataManager;
 18    private readonly OzonClientServices _ozonClientServices;
 19
 20    public OrderToSupplierOzonShipService(
 21        ApplicationDbContext context,
 22        OzonApiDataManager ozonApiDataManager,
 23        OzonClientServices ozonClientServices)
 24    {
 25        _context = context;
 26        _ozonApiDataManager = ozonApiDataManager;
 27        _ozonClientServices = ozonClientServices;
 28    }
 29
 30    public async Task<OrderToSupplierOzonShipResult> ShipAsync(
 31        IReadOnlyCollection<int> orderIds,
 32        CancellationToken cancellationToken = default)
 33    {
 34        if (orderIds.Count == 0)
 35        {
 36            return OrderToSupplierOzonShipResult.Empty();
 37        }
 38
 39        var dbOrders = await _context.Orders
 40            .AsNoTracking()
 41            .Include(o => o.ProductInfo)
 42            .Where(o => orderIds.Contains(o.Id) &&
 43                        o.OzonClientId.HasValue)
 44            .ToListAsync(cancellationToken);
 45
 46        var entries = new List<OrderToSupplierOzonShipEntry>();
 47
 48        foreach (var order in dbOrders)
 49        {
 50            entries.Add(await ShipSingleOrderAsync(order).ConfigureAwait(false));
 51        }
 52
 53        return new OrderToSupplierOzonShipResult(entries);
 54    }
 55
 56    private async Task<OrderToSupplierOzonShipEntry> ShipSingleOrderAsync(Order order)
 57    {
 58        var shipment = order.ShipmentNumber ?? $"ID {order.Id}";
 59        try
 60        {
 61            if (!order.OzonClientId.HasValue)
 62            {
 63                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: у заказа не указан Ozon клие
 64            }
 65
 66            var ozonClient = await _ozonClientServices.GetOzonClientAsync(order.OzonClientId.Value).ConfigureAwait(false
 67            if (ozonClient == null || ozonClient.ClientType != ClientType.OZON)
 68            {
 69                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: заказ не относится к Ozon кл
 70            }
 71
 72            if (string.IsNullOrWhiteSpace(ozonClient.DecryptClientId) ||
 73                string.IsNullOrWhiteSpace(ozonClient.DecryptApiKey))
 74            {
 75                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: не найдены ключи Ozon клиент
 76            }
 77
 78            if (order.Quantity.GetValueOrDefault() <= 0)
 79            {
 80                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: количество равно 0.");
 81            }
 82
 83            var ozonPostingNumber = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 84            _ozonApiDataManager.SetClient(ozonClient.DecryptClientId, ozonClient.DecryptApiKey);
 85
 86            var orderStatus = await _ozonApiDataManager.GetProductSatatus(ozonPostingNumber).ConfigureAwait(false);
 87            var statusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(orderStatus);
 88
 89            if (OzonLabelStatusHelper.IsOzonStatusPastShipping(orderStatus))
 90            {
 91                return OrderToSupplierOzonShipEntry.Skipped(
 92                    order.Id, shipment,
 93                    $"Пропущено: статус Ozon «{statusName}», повторная отгрузка не требуется.");
 94            }
 95
 96            if (orderStatus == "not_accepted")
 97            {
 98                var toAwaitingDeliver = await _ozonApiDataManager
 99                    .SetAwaitingDelivery([ozonPostingNumber])
 100                    .ConfigureAwait(false);
 101                return toAwaitingDeliver.Result
 102                    ? OrderToSupplierOzonShipEntry.Success(order.Id, shipment, "Спорный заказ передан к отгрузке.")
 103                    : OrderToSupplierOzonShipEntry.Failed(order.Id, shipment, "Не удалось передать спорный заказ к отгру
 104            }
 105
 106            // Несколько разных SKU в отправлении — отгружаем все товары, иначе Ozon отвечает
 107            // HAS_INCORRECT_PRODUCT_QUANTITY.
 108            List<FbsShipPackage> packages;
 109            var postingProducts = await _ozonApiDataManager
 110                .GetPostingShipProductsAsync(ozonPostingNumber)
 111                .ConfigureAwait(false);
 112            if (postingProducts.Count > 1)
 113            {
 114                packages = OzonFbsShipPackageBuilder.Build(postingProducts, packagePerUnit: false);
 115            }
 116            else
 117            {
 118                var realProductId = await _ozonApiDataManager.GetProductId(ozonPostingNumber).ConfigureAwait(false);
 119                if (!long.TryParse(realProductId, out var realProductIdValue) || realProductIdValue <= 0)
 120                {
 121                    return OrderToSupplierOzonShipEntry.Failed(
 122                        order.Id, shipment,
 123                        $"Не удалось определить product_id для отправления {ozonPostingNumber}.");
 124                }
 125
 126                var shipQuantity = Math.Max(1, order.Quantity.GetValueOrDefault());
 127                var ozonShipQuantity = await _ozonApiDataManager
 128                    .GetPostingProductQuantityAsync(ozonPostingNumber, realProductIdValue)
 129                    .ConfigureAwait(false);
 130                if (ozonShipQuantity > 0 && shipQuantity != ozonShipQuantity)
 131                {
 132                    shipQuantity = ozonShipQuantity;
 133                }
 134
 135                packages = new List<FbsShipPackage>
 136                {
 137                    new()
 138                    {
 139                        Products =
 140                        [
 141                            new FbsShipProduct
 142                            {
 143                                ProductId = realProductIdValue,
 144                                Quantity = shipQuantity
 145                            }
 146                        ]
 147                    }
 148                };
 149            }
 150
 151            await _ozonApiDataManager
 152                .ShipFbsPosting(ozonPostingNumber, packages, withAdditionalData: true)
 153                .ConfigureAwait(false);
 154            await Task.Delay(2000).ConfigureAwait(false);
 155
 156            var newStatus = await _ozonApiDataManager.GetProductSubSatatus(ozonPostingNumber).ConfigureAwait(false);
 157            if (newStatus == "ship_failed")
 158            {
 159                await _ozonApiDataManager
 160                    .ShipFbsPosting(ozonPostingNumber, packages, withAdditionalData: true)
 161                    .ConfigureAwait(false);
 162                await Task.Delay(2000).ConfigureAwait(false);
 163                newStatus = await _ozonApiDataManager.GetProductSubSatatus(ozonPostingNumber).ConfigureAwait(false);
 164            }
 165
 166            if (newStatus == "ship_failed")
 167            {
 168                return OrderToSupplierOzonShipEntry.Failed(
 169                    order.Id, shipment,
 170                    $"Ошибка отгрузки: {OzonStatus.GetSubStatus(newStatus)}.");
 171            }
 172
 173            return OrderToSupplierOzonShipEntry.Success(
 174                order.Id, shipment,
 175                $"{OzonStatus.GetSubStatus(newStatus)}.");
 176        }
 177        catch (Exception ex)
 178        {
 179            // Ozon иногда успевает провести отправление между проверкой статуса и ShipFbsPosting.
 180            // Это не ошибка оформления: повторная отгрузка не нужна, закупку у поставщика можно продолжать.
 181            if (ex.Message.Contains("POSTING_ALREADY_SHIPPED", StringComparison.OrdinalIgnoreCase))
 182            {
 183                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment,
 184                    "Отправление уже отгружено в Ozon, повторная отгрузка не требуется.");
 185            }
 186
 187            // Для HAS_INCORRECT_STATUS пользователю важно увидеть, в каком статусе сейчас отправление.
 188            string? ozonStatusName = null;
 189            if (OzonLabelPrintErrorHelper.NeedsCurrentOzonStatus(ex.Message))
 190            {
 191                try
 192                {
 193                    var current = await _ozonApiDataManager
 194                        .GetProductSatatus(OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber))
 195                        .ConfigureAwait(false);
 196                    ozonStatusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(current);
 197                }
 198                catch
 199                {
 200                    // Статус нужен только для текста ошибки.
 201                }
 202            }
 203
 204            return OrderToSupplierOzonShipEntry.Failed(order.Id, shipment, ex.Message, ozonStatusName);
 205        }
 206    }
 207}
 208
 209public sealed class OrderToSupplierOzonShipResult
 210{
 2211    public IReadOnlyCollection<OrderToSupplierOzonShipEntry> Entries { get; }
 212
 0213    public int SuccessCount => Entries.Count(e => e.IsSuccess);
 0214    public int FailedCount => Entries.Count(e => !e.IsSuccess && !e.IsSkipped);
 0215    public int SkippedCount => Entries.Count(e => e.IsSkipped);
 216
 217    /// <summary>Заказы, для которых можно продолжать оформление у поставщика: успешно отгруженные
 218    /// либо уже отгруженные в Ozon. Ошибка отгрузки не должна создавать новую закупку.</summary>
 0219    public IReadOnlySet<int> EligibleOrderIds => Entries
 0220        .Where(e => e.IsSuccess || e.IsSkipped)
 0221        .Select(e => e.OrderId)
 0222        .ToHashSet();
 223
 2224    public OrderToSupplierOzonShipResult(IReadOnlyCollection<OrderToSupplierOzonShipEntry> entries)
 225    {
 2226        Entries = entries;
 2227    }
 228
 0229    public static OrderToSupplierOzonShipResult Empty() => new(Array.Empty<OrderToSupplierOzonShipEntry>());
 230
 231    public string ToHtmlBlock()
 232    {
 0233        if (Entries.Count == 0)
 234        {
 0235            return string.Empty;
 236        }
 237
 0238        var rows = Entries
 0239            .Take(20)
 0240            .Select(e =>
 0241                $"<li><b>{WebUtility.HtmlEncode(e.ShipmentNumber)}</b>: {WebUtility.HtmlEncode(e.Message)}</li>");
 242
 0243        var hiddenCount = Math.Max(0, Entries.Count - 20);
 0244        var hiddenTail = hiddenCount > 0 ? $"<li>... и еще {hiddenCount}</li>" : string.Empty;
 245
 0246        var alertClass = FailedCount > 0 ? "alert-warning border-warning-subtle" : "alert-info border-primary-subtle";
 0247        return $"<div class='alert {alertClass} mb-0'>" +
 0248               "<div class='fw-semibold mb-1'><i class='fas fa-truck me-1 text-primary'></i>Отгрузка в Ozon</div>" +
 0249               $"<div class='small mb-1'>Успешно: <b>{SuccessCount}</b>, с ошибками: <b>{FailedCount}</b>, пропущено: <b
 0250               $"<ul class='mb-0 small'>{string.Join(string.Empty, rows)}{hiddenTail}</ul>" +
 0251               "</div>";
 252    }
 253}
 254
 255public sealed record OrderToSupplierOzonShipEntry(
 256    int OrderId,
 257    string ShipmentNumber,
 258    bool IsSuccess,
 259    bool IsSkipped,
 260    string Message,
 261    string? OzonStatusName = null)
 262{
 263    public static OrderToSupplierOzonShipEntry Success(int orderId, string shipmentNumber, string message) =>
 264        new(orderId, shipmentNumber, true, false, message);
 265
 266    public static OrderToSupplierOzonShipEntry Failed(int orderId, string shipmentNumber, string message, string? ozonSt
 267        new(orderId, shipmentNumber, false, false, message, ozonStatusName);
 268
 269    public static OrderToSupplierOzonShipEntry Skipped(int orderId, string shipmentNumber, string message) =>
 270        new(orderId, shipmentNumber, false, true, message);
 271}