< Summary

Information
Class: OzonOrdersWeb.TransactionServices.JapartsCartService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/JapartsCartService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 103
Coverable lines: 103
Total lines: 227
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 48
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%
PlaceQueuedItemsAsync()0%7280%
CheckoutDirectAsync()0%342180%
Invalid(...)0%2040%
Label(...)0%2040%
Comment(...)0%620%
AddItemsAsync()0%110100%
TryTrackCartAsync()100%210%
TryTrackOrderedAsync()100%210%
TryRecordHistoryAsync()0%620%

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
 10public sealed record JapartsCartQueueItem(
 11    int OrderId,
 12    string PriceId,
 13    decimal Quantity,
 14    decimal? Price,
 15    string? Article,
 16    string? ProductName,
 17    string? ShipmentNumber,
 18    string? ClientName,
 19    int? DeliveryDaysMin = null,
 20    int? DeliveryDaysMax = null,
 21    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
 041    public JapartsCartService(JapartsDataManager japarts, IOrderHistoryDataService orderHistoryService,
 042        IOrderSupplierStateService supplierStateService)
 43    {
 044        _japarts = japarts;
 045        _orderHistoryService = orderHistoryService;
 046        _supplierStateService = supplierStateService;
 047    }
 48
 49    /// <summary>Позиции уходят только в корзину Japarts: заказ у поставщика оформляется отдельно.</summary>
 50    public async Task<JapartsCartResult> PlaceQueuedItemsAsync(IReadOnlyCollection<JapartsCartQueueItem> items,
 51        string changedBy, CancellationToken ct = default)
 52    {
 053        if (items.Count == 0) return new(true, []);
 054        if (Invalid(items)) return new(false, ["Для корзины Japarts требуется ровно одно корректное предложение на заказ
 55
 056        var (added, messages) = await AddItemsAsync(items, changedBy, ct);
 057        foreach (var (item, basketId) in added)
 58        {
 059            await TryTrackCartAsync(item, basketId, changedBy);
 060            await TryRecordHistoryAsync(item, true, basketId, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 061            messages.Add($"{Label(item)}: добавлено в корзину Japarts.");
 062        }
 63
 064        return new(added.Count == items.Count, messages);
 065    }
 66
 67    /// <summary>Прямое оформление: позиции кладутся в корзину Japarts и сразу превращаются в заказ поставщика.</summary
 68    public async Task<JapartsCartResult> CheckoutDirectAsync(IReadOnlyCollection<JapartsCartQueueItem> items,
 69        string changedBy, CancellationToken ct = default)
 70    {
 071        if (items.Count == 0) return new(false, ["Не выбраны позиции Japarts для оформления."]);
 072        if (Invalid(items)) return new(false, ["Для прямого оформления Japarts требуется ровно одно корректное предложен
 73
 074        var (added, messages) = await AddItemsAsync(items, changedBy, ct);
 75        // Заказ создаём только целиком: частичное оформление разъехалось бы с составом проведения.
 076        if (added.Count != items.Count)
 77        {
 078            foreach (var (item, basketId) in added)
 79            {
 080                await TryTrackCartAsync(item, basketId, changedBy);
 081                await TryRecordHistoryAsync(item, true, basketId, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 082            }
 83
 084            messages.Add("Корзина Japarts могла получить часть позиций; заказ не создан.");
 085            return new(false, messages);
 86        }
 87
 88        try
 89        {
 90            var orderNumbers = await _japarts.CreateOrderAsync(added.Select(x => x.BasketId), ct);
 091            var orderNumber = orderNumbers.Count > 0 ? string.Join(", ", orderNumbers) : null;
 092            foreach (var (item, basketId) in added)
 93            {
 094                await TryTrackOrderedAsync(item, orderNumber, basketId, changedBy);
 095                await TryRecordHistoryAsync(item, true, basketId, orderNumber, changedBy, ApiSupplierBasketCartStatus.Or
 096            }
 97
 098            messages.Add(orderNumber is null
 099                ? "Заказ Japarts создан, но поставщик не вернул его номер."
 0100                : $"Создан заказ Japarts №{orderNumber}.");
 0101            return new(true, messages);
 102        }
 0103        catch (JapartsApiException ex)
 104        {
 0105            foreach (var (item, basketId) in added)
 106            {
 0107                await TryTrackCartAsync(item, basketId, changedBy);
 0108                await TryRecordHistoryAsync(item, true, basketId, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 0109            }
 110
 0111            messages.Add($"Japarts не оформил заказ: {ex.Message}. Добавленные позиции остались в корзине Japarts.");
 0112            return new(false, messages);
 113        }
 0114    }
 115
 116    private static bool Invalid(IReadOnlyCollection<JapartsCartQueueItem> items) =>
 0117        items.Any(x => x.OrderId <= 0 || string.IsNullOrWhiteSpace(x.PriceId) || x.Quantity <= 0) ||
 0118        items.GroupBy(x => x.OrderId).Any(x => x.Count() > 1);
 119
 120    private static string Label(JapartsCartQueueItem item) =>
 0121        item.Article ?? (string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber!);
 122
 123    private static string Comment(JapartsCartQueueItem item) =>
 0124        $"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    {
 0134        var messages = new List<string>();
 0135        var accepted = new List<JapartsCartQueueItem>();
 0136        foreach (var item in items)
 137        {
 138            try
 139            {
 0140                await _japarts.AddToBasketAsync(item.PriceId, item.Quantity, item.Price, Comment(item), ct);
 0141                accepted.Add(item);
 0142            }
 0143            catch (JapartsApiException ex)
 144            {
 0145                messages.Add($"{Label(item)}: {ex.Message}");
 0146                await TryRecordHistoryAsync(item, false, null, null, changedBy, ApiSupplierBasketCartStatus.Failed, ex.M
 147            }
 0148        }
 149
 0150        if (accepted.Count == 0) return ([], messages);
 151
 0152        var basket = await _japarts.GetBasketAsync(ct);
 0153        var added = new List<(JapartsCartQueueItem Item, string BasketId)>();
 0154        foreach (var item in accepted)
 155        {
 0156            var basketItem = basket.LastOrDefault(x => x.Comment == Comment(item));
 0157            if (basketItem is null || string.IsNullOrWhiteSpace(basketItem.BasketId))
 158            {
 0159                var message = "Japarts принял позицию, но не показал её в корзине.";
 0160                messages.Add($"{Label(item)}: {message}");
 0161                await TryRecordHistoryAsync(item, false, null, null, changedBy, ApiSupplierBasketCartStatus.Failed, mess
 0162                continue;
 163            }
 164
 0165            added.Add((item, basketItem.BasketId));
 166        }
 167
 0168        return (added, messages);
 0169    }
 170
 171    /// <summary>Стадия корзины — вспомогательный журнал: её сбой не должен ломать уже выполненное размещение.</summary>
 172    private async Task TryTrackCartAsync(JapartsCartQueueItem item, string basketId, string changedBy)
 173    {
 174        try
 175        {
 0176            await _supplierStateService.MarkInCartAsync(item.OrderId, ProviderKey, SupplierCartStatusCode.Pending,
 0177                cartItemId: null, externalBasketItemId: basketId, user: changedBy,
 0178                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction)
 0179        }
 0180        catch
 181        {
 182            // См. комментарий к методу.
 0183        }
 0184    }
 185
 186    private async Task TryTrackOrderedAsync(JapartsCartQueueItem item, string? orderNumber, string basketId, string chan
 187    {
 188        try
 189        {
 0190            await _supplierStateService.MarkOrderedAsync(item.OrderId, ProviderKey, externalOrderNumber: orderNumber,
 0191                externalOrderId: orderNumber, externalLineId: basketId, user: changedBy,
 0192                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction)
 0193        }
 0194        catch
 195        {
 196            // См. TryTrackCartAsync.
 0197        }
 0198    }
 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        {
 0205            var entry = new ApiSupplierBasketHistoryEntry(ProviderKey, SupplierDisplayName, LogoUrl, success,
 0206                errorMessage, apiItemId, item.Article, item.ProductName, item.Price, CurrencyCode, item.Quantity,
 0207                string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber,
 0208                ClientName: item.ClientName, CartStatus: cartStatus, SupplierOrderNumber: supplierOrderNumber,
 0209                SupplierCartUrl: SupplierCartUrl);
 210
 0211            await _orderHistoryService.AddOrderHistory(new OrderHistory
 0212            {
 0213                OrderId = item.OrderId,
 0214                ColumnName = ApiSupplierBasketHistoryEntry.HistoryColumnName,
 0215                ColumnDisplayName = $"Корзина {SupplierDisplayName}",
 0216                OldValue = null,
 0217                NewValue = JsonSerializer.Serialize(entry, ApiSupplierBasketHistoryEntry.JsonOptions),
 0218                ChangedAt = DateTime.Now,
 0219                ChangedBy = changedBy
 0220            });
 0221        }
 0222        catch
 223        {
 224            // Не ломаем размещение в корзине из-за журнала истории.
 0225        }
 0226    }
 227}