< Summary

Information
Class: OzonOrdersWeb.TransactionServices.OrderToSupplierOzonShipService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/OrderToSupplierOzonShipService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 110
Coverable lines: 110
Total lines: 271
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 42
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
ShipAsync()0%2040%
ShipSingleOrderAsync()0%1482380%

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
 020    public OrderToSupplierOzonShipService(
 021        ApplicationDbContext context,
 022        OzonApiDataManager ozonApiDataManager,
 023        OzonClientServices ozonClientServices)
 24    {
 025        _context = context;
 026        _ozonApiDataManager = ozonApiDataManager;
 027        _ozonClientServices = ozonClientServices;
 028    }
 29
 30    public async Task<OrderToSupplierOzonShipResult> ShipAsync(
 31        IReadOnlyCollection<int> orderIds,
 32        CancellationToken cancellationToken = default)
 33    {
 034        if (orderIds.Count == 0)
 35        {
 036            return OrderToSupplierOzonShipResult.Empty();
 37        }
 38
 039        var dbOrders = await _context.Orders
 040            .AsNoTracking()
 041            .Include(o => o.ProductInfo)
 042            .Where(o => orderIds.Contains(o.Id) &&
 043                        o.OzonClientId.HasValue)
 044            .ToListAsync(cancellationToken);
 45
 046        var entries = new List<OrderToSupplierOzonShipEntry>();
 47
 048        foreach (var order in dbOrders)
 49        {
 050            entries.Add(await ShipSingleOrderAsync(order).ConfigureAwait(false));
 51        }
 52
 053        return new OrderToSupplierOzonShipResult(entries);
 054    }
 55
 56    private async Task<OrderToSupplierOzonShipEntry> ShipSingleOrderAsync(Order order)
 57    {
 058        var shipment = order.ShipmentNumber ?? $"ID {order.Id}";
 59        try
 60        {
 061            if (!order.OzonClientId.HasValue)
 62            {
 063                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: у заказа не указан Ozon клие
 64            }
 65
 066            var ozonClient = await _ozonClientServices.GetOzonClientAsync(order.OzonClientId.Value).ConfigureAwait(false
 067            if (ozonClient == null || ozonClient.ClientType != ClientType.OZON)
 68            {
 069                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: заказ не относится к Ozon кл
 70            }
 71
 072            if (string.IsNullOrWhiteSpace(ozonClient.DecryptClientId) ||
 073                string.IsNullOrWhiteSpace(ozonClient.DecryptApiKey))
 74            {
 075                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: не найдены ключи Ozon клиент
 76            }
 77
 078            if (order.Quantity.GetValueOrDefault() <= 0)
 79            {
 080                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment, "Пропущено: количество равно 0.");
 81            }
 82
 083            var ozonPostingNumber = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber);
 084            _ozonApiDataManager.SetClient(ozonClient.DecryptClientId, ozonClient.DecryptApiKey);
 85
 086            var orderStatus = await _ozonApiDataManager.GetProductSatatus(ozonPostingNumber).ConfigureAwait(false);
 087            var statusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(orderStatus);
 88
 089            if (OzonLabelStatusHelper.IsOzonStatusPastShipping(orderStatus))
 90            {
 091                return OrderToSupplierOzonShipEntry.Skipped(
 092                    order.Id, shipment,
 093                    $"Пропущено: статус Ozon «{statusName}», повторная отгрузка не требуется.");
 94            }
 95
 096            if (orderStatus == "not_accepted")
 97            {
 098                var toAwaitingDeliver = await _ozonApiDataManager
 099                    .SetAwaitingDelivery([ozonPostingNumber])
 0100                    .ConfigureAwait(false);
 0101                return toAwaitingDeliver.Result
 0102                    ? OrderToSupplierOzonShipEntry.Success(order.Id, shipment, "Спорный заказ передан к отгрузке.")
 0103                    : OrderToSupplierOzonShipEntry.Failed(order.Id, shipment, "Не удалось передать спорный заказ к отгру
 104            }
 105
 106            // Несколько разных SKU в отправлении — отгружаем все товары, иначе Ozon отвечает
 107            // HAS_INCORRECT_PRODUCT_QUANTITY.
 108            List<FbsShipPackage> packages;
 0109            var postingProducts = await _ozonApiDataManager
 0110                .GetPostingShipProductsAsync(ozonPostingNumber)
 0111                .ConfigureAwait(false);
 0112            if (postingProducts.Count > 1)
 113            {
 0114                packages = OzonFbsShipPackageBuilder.Build(postingProducts, packagePerUnit: false);
 115            }
 116            else
 117            {
 0118                var realProductId = await _ozonApiDataManager.GetProductId(ozonPostingNumber).ConfigureAwait(false);
 0119                if (!long.TryParse(realProductId, out var realProductIdValue) || realProductIdValue <= 0)
 120                {
 0121                    return OrderToSupplierOzonShipEntry.Failed(
 0122                        order.Id, shipment,
 0123                        $"Не удалось определить product_id для отправления {ozonPostingNumber}.");
 124                }
 125
 0126                var shipQuantity = Math.Max(1, order.Quantity.GetValueOrDefault());
 0127                var ozonShipQuantity = await _ozonApiDataManager
 0128                    .GetPostingProductQuantityAsync(ozonPostingNumber, realProductIdValue)
 0129                    .ConfigureAwait(false);
 0130                if (ozonShipQuantity > 0 && shipQuantity != ozonShipQuantity)
 131                {
 0132                    shipQuantity = ozonShipQuantity;
 133                }
 134
 0135                packages = new List<FbsShipPackage>
 0136                {
 0137                    new()
 0138                    {
 0139                        Products =
 0140                        [
 0141                            new FbsShipProduct
 0142                            {
 0143                                ProductId = realProductIdValue,
 0144                                Quantity = shipQuantity
 0145                            }
 0146                        ]
 0147                    }
 0148                };
 149            }
 150
 0151            await _ozonApiDataManager
 0152                .ShipFbsPosting(ozonPostingNumber, packages, withAdditionalData: true)
 0153                .ConfigureAwait(false);
 0154            await Task.Delay(2000).ConfigureAwait(false);
 155
 0156            var newStatus = await _ozonApiDataManager.GetProductSubSatatus(ozonPostingNumber).ConfigureAwait(false);
 0157            if (newStatus == "ship_failed")
 158            {
 0159                await _ozonApiDataManager
 0160                    .ShipFbsPosting(ozonPostingNumber, packages, withAdditionalData: true)
 0161                    .ConfigureAwait(false);
 0162                await Task.Delay(2000).ConfigureAwait(false);
 0163                newStatus = await _ozonApiDataManager.GetProductSubSatatus(ozonPostingNumber).ConfigureAwait(false);
 164            }
 165
 0166            if (newStatus == "ship_failed")
 167            {
 0168                return OrderToSupplierOzonShipEntry.Failed(
 0169                    order.Id, shipment,
 0170                    $"Ошибка отгрузки: {OzonStatus.GetSubStatus(newStatus)}.");
 171            }
 172
 0173            return OrderToSupplierOzonShipEntry.Success(
 0174                order.Id, shipment,
 0175                $"{OzonStatus.GetSubStatus(newStatus)}.");
 176        }
 0177        catch (Exception ex)
 178        {
 179            // Ozon иногда успевает провести отправление между проверкой статуса и ShipFbsPosting.
 180            // Это не ошибка оформления: повторная отгрузка не нужна, закупку у поставщика можно продолжать.
 0181            if (ex.Message.Contains("POSTING_ALREADY_SHIPPED", StringComparison.OrdinalIgnoreCase))
 182            {
 0183                return OrderToSupplierOzonShipEntry.Skipped(order.Id, shipment,
 0184                    "Отправление уже отгружено в Ozon, повторная отгрузка не требуется.");
 185            }
 186
 187            // Для HAS_INCORRECT_STATUS пользователю важно увидеть, в каком статусе сейчас отправление.
 0188            string? ozonStatusName = null;
 0189            if (OzonLabelPrintErrorHelper.NeedsCurrentOzonStatus(ex.Message))
 190            {
 191                try
 192                {
 0193                    var current = await _ozonApiDataManager
 0194                        .GetProductSatatus(OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber))
 0195                        .ConfigureAwait(false);
 0196                    ozonStatusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(current);
 0197                }
 0198                catch
 199                {
 200                    // Статус нужен только для текста ошибки.
 0201                }
 202            }
 203
 0204            return OrderToSupplierOzonShipEntry.Failed(order.Id, shipment, ex.Message, ozonStatusName);
 205        }
 0206    }
 207}
 208
 209public sealed class OrderToSupplierOzonShipResult
 210{
 211    public IReadOnlyCollection<OrderToSupplierOzonShipEntry> Entries { get; }
 212
 213    public int SuccessCount => Entries.Count(e => e.IsSuccess);
 214    public int FailedCount => Entries.Count(e => !e.IsSuccess && !e.IsSkipped);
 215    public int SkippedCount => Entries.Count(e => e.IsSkipped);
 216
 217    /// <summary>Заказы, для которых можно продолжать оформление у поставщика: успешно отгруженные
 218    /// либо уже отгруженные в Ozon. Ошибка отгрузки не должна создавать новую закупку.</summary>
 219    public IReadOnlySet<int> EligibleOrderIds => Entries
 220        .Where(e => e.IsSuccess || e.IsSkipped)
 221        .Select(e => e.OrderId)
 222        .ToHashSet();
 223
 224    public OrderToSupplierOzonShipResult(IReadOnlyCollection<OrderToSupplierOzonShipEntry> entries)
 225    {
 226        Entries = entries;
 227    }
 228
 229    public static OrderToSupplierOzonShipResult Empty() => new(Array.Empty<OrderToSupplierOzonShipEntry>());
 230
 231    public string ToHtmlBlock()
 232    {
 233        if (Entries.Count == 0)
 234        {
 235            return string.Empty;
 236        }
 237
 238        var rows = Entries
 239            .Take(20)
 240            .Select(e =>
 241                $"<li><b>{WebUtility.HtmlEncode(e.ShipmentNumber)}</b>: {WebUtility.HtmlEncode(e.Message)}</li>");
 242
 243        var hiddenCount = Math.Max(0, Entries.Count - 20);
 244        var hiddenTail = hiddenCount > 0 ? $"<li>... и еще {hiddenCount}</li>" : string.Empty;
 245
 246        var alertClass = FailedCount > 0 ? "alert-warning border-warning-subtle" : "alert-info border-primary-subtle";
 247        return $"<div class='alert {alertClass} mb-0'>" +
 248               "<div class='fw-semibold mb-1'><i class='fas fa-truck me-1 text-primary'></i>Отгрузка в Ozon</div>" +
 249               $"<div class='small mb-1'>Успешно: <b>{SuccessCount}</b>, с ошибками: <b>{FailedCount}</b>, пропущено: <b
 250               $"<ul class='mb-0 small'>{string.Join(string.Empty, rows)}{hiddenTail}</ul>" +
 251               "</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}