< Summary

Information
Class: OzonOrdersWeb.TransactionServices.JapartsCartQueueItem
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/JapartsCartService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 12
Coverable lines: 12
Total lines: 227
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_OrderId()100%210%
get_PriceId()100%210%
get_Quantity()100%210%
get_Price()100%210%
get_Article()100%210%
get_ProductName()100%210%
get_ShipmentNumber()100%210%
get_ClientName()100%210%
get_DeliveryDaysMin()100%210%
get_DeliveryDaysMax()100%210%
get_Direction()100%210%

File(s)

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

#LineLine coverage
 1using System.Text.Json;
 2using OzonDomains.Models;
 3using OzonDomains.Models.SupplierStatuses;
 4using Services.ApiServices.JapartsApi;
 5using Services.DataServices;
 6using Services.SupplierStatusServices;
 7
 8namespace OzonOrdersWeb.TransactionServices;
 9
 010public sealed record JapartsCartQueueItem(
 011    int OrderId,
 012    string PriceId,
 013    decimal Quantity,
 014    decimal? Price,
 015    string? Article,
 016    string? ProductName,
 017    string? ShipmentNumber,
 018    string? ClientName,
 019    int? DeliveryDaysMin = null,
 020    int? DeliveryDaysMax = null,
 021    string? Direction = null);
 22
 23public sealed record JapartsCartResult(bool IsSuccess, IReadOnlyList<string> Messages);
 24
 25/// <summary>
 26/// Корзина Japarts со страницы проведения: раскладка позиций по серверной корзине поставщика и
 27/// прямое оформление (корзина + baskettoorder одним действием пользователя).
 28/// </summary>
 29public sealed class JapartsCartService
 30{
 31    private const string ProviderKey = ApiSupplierSettings.JapartsProviderKey;
 32    private const string SupplierDisplayName = "Japarts";
 33    private const string LogoUrl = "/images/japarts-logo.jpg";
 34    private const string CurrencyCode = "RUB";
 35    private const string SupplierCartUrl = "/SupplierCart?provider=japarts&supplierTab=basket";
 36
 37    private readonly JapartsDataManager _japarts;
 38    private readonly IOrderHistoryDataService _orderHistoryService;
 39    private readonly IOrderSupplierStateService _supplierStateService;
 40
 41    public JapartsCartService(JapartsDataManager japarts, IOrderHistoryDataService orderHistoryService,
 42        IOrderSupplierStateService supplierStateService)
 43    {
 44        _japarts = japarts;
 45        _orderHistoryService = orderHistoryService;
 46        _supplierStateService = supplierStateService;
 47    }
 48
 49    /// <summary>Позиции уходят только в корзину Japarts: заказ у поставщика оформляется отдельно.</summary>
 50    public async Task<JapartsCartResult> PlaceQueuedItemsAsync(IReadOnlyCollection<JapartsCartQueueItem> items,
 51        string changedBy, CancellationToken ct = default)
 52    {
 53        if (items.Count == 0) return new(true, []);
 54        if (Invalid(items)) return new(false, ["Для корзины Japarts требуется ровно одно корректное предложение на заказ
 55
 56        var (added, messages) = await AddItemsAsync(items, changedBy, ct);
 57        foreach (var (item, basketId) in added)
 58        {
 59            await TryTrackCartAsync(item, basketId, changedBy);
 60            await TryRecordHistoryAsync(item, true, basketId, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 61            messages.Add($"{Label(item)}: добавлено в корзину Japarts.");
 62        }
 63
 64        return new(added.Count == items.Count, messages);
 65    }
 66
 67    /// <summary>Прямое оформление: позиции кладутся в корзину Japarts и сразу превращаются в заказ поставщика.</summary
 68    public async Task<JapartsCartResult> CheckoutDirectAsync(IReadOnlyCollection<JapartsCartQueueItem> items,
 69        string changedBy, CancellationToken ct = default)
 70    {
 71        if (items.Count == 0) return new(false, ["Не выбраны позиции Japarts для оформления."]);
 72        if (Invalid(items)) return new(false, ["Для прямого оформления Japarts требуется ровно одно корректное предложен
 73
 74        var (added, messages) = await AddItemsAsync(items, changedBy, ct);
 75        // Заказ создаём только целиком: частичное оформление разъехалось бы с составом проведения.
 76        if (added.Count != items.Count)
 77        {
 78            foreach (var (item, basketId) in added)
 79            {
 80                await TryTrackCartAsync(item, basketId, changedBy);
 81                await TryRecordHistoryAsync(item, true, basketId, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 82            }
 83
 84            messages.Add("Корзина Japarts могла получить часть позиций; заказ не создан.");
 85            return new(false, messages);
 86        }
 87
 88        try
 89        {
 90            var orderNumbers = await _japarts.CreateOrderAsync(added.Select(x => x.BasketId), ct);
 91            var orderNumber = orderNumbers.Count > 0 ? string.Join(", ", orderNumbers) : null;
 92            foreach (var (item, basketId) in added)
 93            {
 94                await TryTrackOrderedAsync(item, orderNumber, basketId, changedBy);
 95                await TryRecordHistoryAsync(item, true, basketId, orderNumber, changedBy, ApiSupplierBasketCartStatus.Or
 96            }
 97
 98            messages.Add(orderNumber is null
 99                ? "Заказ Japarts создан, но поставщик не вернул его номер."
 100                : $"Создан заказ Japarts №{orderNumber}.");
 101            return new(true, messages);
 102        }
 103        catch (JapartsApiException ex)
 104        {
 105            foreach (var (item, basketId) in added)
 106            {
 107                await TryTrackCartAsync(item, basketId, changedBy);
 108                await TryRecordHistoryAsync(item, true, basketId, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 109            }
 110
 111            messages.Add($"Japarts не оформил заказ: {ex.Message}. Добавленные позиции остались в корзине Japarts.");
 112            return new(false, messages);
 113        }
 114    }
 115
 116    private static bool Invalid(IReadOnlyCollection<JapartsCartQueueItem> items) =>
 117        items.Any(x => x.OrderId <= 0 || string.IsNullOrWhiteSpace(x.PriceId) || x.Quantity <= 0) ||
 118        items.GroupBy(x => x.OrderId).Any(x => x.Count() > 1);
 119
 120    private static string Label(JapartsCartQueueItem item) =>
 121        item.Article ?? (string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber!);
 122
 123    private static string Comment(JapartsCartQueueItem item) =>
 124        $"Studio2 {(string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber)} (ID {i
 125
 126    /// <summary>
 127    /// addtobasket2 не возвращает идентификатор позиции, поэтому корзину перечитываем один раз после всех
 128    /// добавлений и сопоставляем по комментарию Studio2. Если такой комментарий уже был в корзине с прошлого
 129    /// раза, берём последнюю строку — она и есть только что добавленная.
 130    /// </summary>
 131    private async Task<(List<(JapartsCartQueueItem Item, string BasketId)> Added, List<string> Messages)> AddItemsAsync(
 132        IReadOnlyCollection<JapartsCartQueueItem> items, string changedBy, CancellationToken ct)
 133    {
 134        var messages = new List<string>();
 135        var accepted = new List<JapartsCartQueueItem>();
 136        foreach (var item in items)
 137        {
 138            try
 139            {
 140                await _japarts.AddToBasketAsync(item.PriceId, item.Quantity, item.Price, Comment(item), ct);
 141                accepted.Add(item);
 142            }
 143            catch (JapartsApiException ex)
 144            {
 145                messages.Add($"{Label(item)}: {ex.Message}");
 146                await TryRecordHistoryAsync(item, false, null, null, changedBy, ApiSupplierBasketCartStatus.Failed, ex.M
 147            }
 148        }
 149
 150        if (accepted.Count == 0) return ([], messages);
 151
 152        var basket = await _japarts.GetBasketAsync(ct);
 153        var added = new List<(JapartsCartQueueItem Item, string BasketId)>();
 154        foreach (var item in accepted)
 155        {
 156            var basketItem = basket.LastOrDefault(x => x.Comment == Comment(item));
 157            if (basketItem is null || string.IsNullOrWhiteSpace(basketItem.BasketId))
 158            {
 159                var message = "Japarts принял позицию, но не показал её в корзине.";
 160                messages.Add($"{Label(item)}: {message}");
 161                await TryRecordHistoryAsync(item, false, null, null, changedBy, ApiSupplierBasketCartStatus.Failed, mess
 162                continue;
 163            }
 164
 165            added.Add((item, basketItem.BasketId));
 166        }
 167
 168        return (added, messages);
 169    }
 170
 171    /// <summary>Стадия корзины — вспомогательный журнал: её сбой не должен ломать уже выполненное размещение.</summary>
 172    private async Task TryTrackCartAsync(JapartsCartQueueItem item, string basketId, string changedBy)
 173    {
 174        try
 175        {
 176            await _supplierStateService.MarkInCartAsync(item.OrderId, ProviderKey, SupplierCartStatusCode.Pending,
 177                cartItemId: null, externalBasketItemId: basketId, user: changedBy,
 178                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction)
 179        }
 180        catch
 181        {
 182            // См. комментарий к методу.
 183        }
 184    }
 185
 186    private async Task TryTrackOrderedAsync(JapartsCartQueueItem item, string? orderNumber, string basketId, string chan
 187    {
 188        try
 189        {
 190            await _supplierStateService.MarkOrderedAsync(item.OrderId, ProviderKey, externalOrderNumber: orderNumber,
 191                externalOrderId: orderNumber, externalLineId: basketId, user: changedBy,
 192                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction)
 193        }
 194        catch
 195        {
 196            // См. TryTrackCartAsync.
 197        }
 198    }
 199
 200    private async Task TryRecordHistoryAsync(JapartsCartQueueItem item, bool success, string? apiItemId,
 201        string? supplierOrderNumber, string changedBy, string cartStatus, string? errorMessage = null)
 202    {
 203        try
 204        {
 205            var entry = new ApiSupplierBasketHistoryEntry(ProviderKey, SupplierDisplayName, LogoUrl, success,
 206                errorMessage, apiItemId, item.Article, item.ProductName, item.Price, CurrencyCode, item.Quantity,
 207                string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber,
 208                ClientName: item.ClientName, CartStatus: cartStatus, SupplierOrderNumber: supplierOrderNumber,
 209                SupplierCartUrl: SupplierCartUrl);
 210
 211            await _orderHistoryService.AddOrderHistory(new OrderHistory
 212            {
 213                OrderId = item.OrderId,
 214                ColumnName = ApiSupplierBasketHistoryEntry.HistoryColumnName,
 215                ColumnDisplayName = $"Корзина {SupplierDisplayName}",
 216                OldValue = null,
 217                NewValue = JsonSerializer.Serialize(entry, ApiSupplierBasketHistoryEntry.JsonOptions),
 218                ChangedAt = DateTime.Now,
 219                ChangedBy = changedBy
 220            });
 221        }
 222        catch
 223        {
 224            // Не ломаем размещение в корзине из-за журнала истории.
 225        }
 226    }
 227}