< Summary

Information
Class: OzonOrdersWeb.TransactionServices.NikeiCartOperationResult
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/NikeiCartService.cs
Line coverage
100%
Covered lines: 4
Uncovered lines: 0
Coverable lines: 4
Total lines: 157
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Added()100%11100%
get_IsSuccess()100%11100%
ToHtmlBlock()100%44100%

File(s)

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

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using OzonDomains.Models;
 3using OzonDomains.Models.SupplierStatuses;
 4using OzonRepositories.Context;
 5using Services.ApiServices.NikeiApi;
 6using Services.SupplierStatusServices;
 7
 8namespace OzonOrdersWeb.TransactionServices;
 9
 10public sealed record NikeiCartQueueItem(int OrderId, string ToCart, string? ProductKey, string Article, string Brand,
 11    string? ProductName, string? WarehouseName, int Quantity, int Multiplicity, int? Stock, decimal? SourcePrice,
 12    int? DeliveryDaysMin, int? DeliveryDaysMax, string? ShipmentNumber, string? ClientName,
 13    int? ShippingType = null, int? ShippingAddress = null);
 14
 20215public sealed record NikeiCartOperationResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 16{
 1617    public bool IsSuccess => Failed == 0;
 418    public string ToHtmlBlock() => $"<div class='alert alert-{(Failed == 0 ? "success" : "warning")} mb-0'><strong>Корзи
 419                                  (Messages.Count == 0 ? "</div>" : "<br/>" + string.Join("<br/>", Messages.Select(Syste
 20}
 21
 22/// <summary>Гибридная корзина Nikei: позиция сначала добавляется в реальную корзину API, затем сохраняется её связь с з
 23public sealed class NikeiCartService
 24{
 25    public const string ProviderKey = ApiSupplierSettings.NikeiProviderKey;
 26    private readonly ApplicationDbContext _db;
 27    private readonly NikeiDataManager _nikei;
 28    private readonly IOrderSupplierStateService _states;
 29
 30    public NikeiCartService(ApplicationDbContext db, NikeiDataManager nikei, IOrderSupplierStateService states) =>
 31        (_db, _nikei, _states) = (db, nikei, states);
 32
 33    public Task<List<NikeiCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.NikeiCartItems.AsNoTracking()
 34        .Include(x => x.StudioOrder).Where(x => NikeiCartItemStatus.Active.Contains(x.Status)).OrderByDescending(x => x.
 35
 36    public Task<int> CountActiveAsync(CancellationToken ct = default) =>
 37        _db.NikeiCartItems.CountAsync(x => NikeiCartItemStatus.Active.Contains(x.Status), ct);
 38
 39    public Task<Newtonsoft.Json.Linq.JToken> GetApiCartAsync(CancellationToken ct = default) => _nikei.GetCartAsync(ct);
 40    public Task<Newtonsoft.Json.Linq.JToken> GetApiOrdersAsync(CancellationToken ct = default) => _nikei.GetOrdersAsync(
 41    public Task<Newtonsoft.Json.Linq.JToken> GetOfficesAsync(CancellationToken ct = default) => _nikei.GetOfficesAsync(c
 42    public Task<Newtonsoft.Json.Linq.JToken> GetClientAddressesAsync(CancellationToken ct = default) => _nikei.GetClient
 43
 44    public async Task<NikeiCartOperationResult> QueueAsync(IReadOnlyCollection<NikeiCartQueueItem> queue, string user,
 45        CancellationToken ct = default)
 46    {
 47        var added = 0; var skipped = 0; var failed = 0; var messages = new List<string>();
 48        foreach (var item in queue)
 49        {
 50            var label = string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber;
 51            if (string.IsNullOrWhiteSpace(item.ToCart)) { failed++; messages.Add($"{label}: у предложения Nikei отсутств
 52            if (item.Quantity < item.Multiplicity || item.Quantity % Math.Max(1, item.Multiplicity) != 0) { failed++; me
 53            if (item.Stock.HasValue && item.Quantity > item.Stock.Value) { failed++; messages.Add($"{label}: запрошено {
 54            if (await _db.NikeiCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; messages
 55
 56            var comment = $"Studio2 {label} (ID {item.OrderId})";
 57            NikeiCartAddResult api;
 58            try { api = await _nikei.AddToCartAsync(item.ToCart, item.Quantity, comment, ct); }
 59            catch (NikeiApiException ex) { failed++; messages.Add($"{label}: {ex.Message}"); continue; }
 60            if (!api.IsSuccess || string.IsNullOrWhiteSpace(api.ToOrder)) { failed++; messages.Add($"{label}: {api.Messa
 61
 62            var row = new NikeiCartItem
 63            {
 64                StudioOrderId = item.OrderId, ActiveStudioOrderId = item.OrderId, ToCart = item.ToCart, ToOrder = api.To
 65                ProductKey = item.ProductKey, Article = item.Article, Brand = item.Brand, ProductName = item.ProductName
 66                WarehouseName = item.WarehouseName, Quantity = item.Quantity, Multiplicity = Math.Max(1, item.Multiplici
 67                Stock = item.Stock, SourcePrice = item.SourcePrice, DeliveryDaysMin = item.DeliveryDaysMin, DeliveryDays
 68                Comment = comment, CreatedBy = user
 69            };
 70            _db.NikeiCartItems.Add(row);
 71            try { await _db.SaveChangesAsync(ct); }
 72            catch (DbUpdateException) { _db.Entry(row).State = EntityState.Detached; skipped++; messages.Add($"{label}: 
 73            added++;
 74            // to_order у Nikei — SHA-256 (64 символа), а legacy-поле ExternalBasketItemId в общей
 75            // таблице статусов ограничено 50 символами. Полный токен уже хранится в NikeiCartItems.
 76            await TrackAsync(() => _states.MarkInCartAsync(item.OrderId, ProviderKey, SupplierCartStatusCode.Pending, ro
 77                user, item.DeliveryDaysMin, item.DeliveryDaysMax, item.WarehouseName, ct));
 78        }
 79        return new NikeiCartOperationResult(added, skipped, failed, messages);
 80    }
 81
 82    /// <summary>
 83    /// Прямое оформление: позиции кладутся в корзину Nikei и сразу уходят одним заказом. Nikei создаёт один заказ
 84    /// на выбранные строки с единым способом и адресом доставки, поэтому разные адреса в одном проведении запрещены.
 85    /// Заказ создаётся только целиком: при ошибке добавления заказ не оформляется, а добавленные позиции остаются в кор
 86    /// </summary>
 87    public async Task<NikeiCartOperationResult> CheckoutDirectAsync(IReadOnlyCollection<NikeiCartQueueItem> queue, strin
 88        CancellationToken ct = default)
 89    {
 90        if (queue.Count == 0) return new NikeiCartOperationResult(0, 0, 1, ["Не выбраны позиции Nikei для оформления."])
 91        if (queue.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 92            return new NikeiCartOperationResult(0, 0, 1, ["Для прямого оформления Nikei требуется ровно одно предложение
 93
 94        var deliveries = queue.Select(x => (Type: x.ShippingType ?? 4, Address: x.ShippingAddress ?? 0)).Distinct().ToLi
 95        if (deliveries.Count > 1)
 96            return new NikeiCartOperationResult(0, 0, 1, ["Позиции Nikei относятся к разным способам или адресам доставк
 97        var (shippingType, shippingAddress) = deliveries[0];
 98        if (shippingType is not (0 or 4) || shippingAddress <= 0)
 99            return new NikeiCartOperationResult(0, 0, 1, ["Выберите корректный адрес доставки Nikei."]);
 100
 101        var queued = await QueueAsync(queue, user, ct);
 102        if (queued.Failed > 0)
 103            return new NikeiCartOperationResult(queued.Added, queued.Skipped, queued.Failed,
 104                [.. queued.Messages, "Заказ Nikei не создан: добавленные позиции остались в корзине Nikei."]);
 105
 106        var orderIds = queue.Select(x => (int?)x.OrderId).ToList();
 107        var ids = await _db.NikeiCartItems
 108            .Where(x => orderIds.Contains(x.ActiveStudioOrderId) && NikeiCartItemStatus.Active.Contains(x.Status))
 109            .Select(x => x.Id).ToListAsync(ct);
 110        if (ids.Count != queue.Count)
 111            return new NikeiCartOperationResult(queued.Added, queued.Skipped, 1,
 112                ["Не все позиции Nikei найдены в корзине. Заказ Nikei не создан."]);
 113
 114        try { return await CheckoutAsync(ids, shippingType, shippingAddress, "Studio2", user, ct); }
 115        catch (NikeiApiException ex)
 116        {
 117            return new NikeiCartOperationResult(0, 0, ids.Count, [$"Nikei: {ex.Message} Позиции остались в корзине Nikei
 118        }
 119    }
 120
 121    public async Task<NikeiCartOperationResult> CheckoutAsync(IReadOnlyCollection<long> ids, int shippingType, int shipp
 122        string orderComment, string user, CancellationToken ct = default)
 123    {
 124        var rows = await SelectedAsync(ids, ct);
 125        if (rows.Count == 0) return new NikeiCartOperationResult(0, 0, 1, ["Выберите позиции Nikei для оформления."]);
 126        if (shippingType is not (0 or 4) || shippingAddress <= 0) return new NikeiCartOperationResult(0, 0, 1, ["Выберит
 127        var response = await _nikei.CreateOrderAsync(rows.Select(x => x.ToOrder!).ToList(), shippingType, shippingAddres
 128        NikeiHttpClient.ThrowIfError(response, "/account/order/create");
 129        if (!string.Equals(response.Value<string>("status"), "ok", StringComparison.OrdinalIgnoreCase))
 130            return new NikeiCartOperationResult(0, 0, rows.Count, [response.Value<string>("message") ?? "Nikei не создал
 131        var invoice = response.Value<string>("message") ?? "Nikei";
 132        foreach (var row in rows)
 133        {
 134            row.Status = NikeiCartItemStatus.Ordered; row.ActiveStudioOrderId = null; row.ExternalOrderNumber = invoice;
 135        }
 136        await _db.SaveChangesAsync(ct);
 137        foreach (var row in rows) await TrackAsync(() => _states.MarkOrderedAsync(row.StudioOrderId, ProviderKey, invoic
 138            deliveryDaysMin: row.DeliveryDaysMin, deliveryDaysMax: row.DeliveryDaysMax, direction: row.WarehouseName, ct
 139        return new NikeiCartOperationResult(rows.Count, 0, 0, [$"Nikei создал заказ {invoice}."]);
 140    }
 141
 142    public async Task<NikeiCartOperationResult> RemoveAsync(IReadOnlyCollection<long> ids, bool all, string user, Cancel
 143    {
 144        var rows = all ? await _db.NikeiCartItems.Where(x => NikeiCartItemStatus.Active.Contains(x.Status)).ToListAsync(
 145        if (rows.Count == 0) return new NikeiCartOperationResult(0, 0, 0, []);
 146        try { await _nikei.RemoveFromCartAsync(rows.Where(x => !string.IsNullOrWhiteSpace(x.ToOrder)).Select(x => x.ToOr
 147        catch (NikeiApiException ex) { return new NikeiCartOperationResult(0, 0, rows.Count, [ex.Message]); }
 148        foreach (var row in rows) { row.Status = NikeiCartItemStatus.Removed; row.ActiveStudioOrderId = null; row.Update
 149        await _db.SaveChangesAsync(ct);
 150        foreach (var row in rows) await TrackAsync(() => _states.RemoveFromCartAsync(row.StudioOrderId, ProviderKey, use
 151        return new NikeiCartOperationResult(rows.Count, 0, 0, []);
 152    }
 153
 154    private Task<List<NikeiCartItem>> SelectedAsync(IReadOnlyCollection<long> ids, CancellationToken ct) => _db.NikeiCar
 155        .Where(x => ids.Contains(x.Id) && NikeiCartItemStatus.Active.Contains(x.Status)).ToListAsync(ct);
 156    private static async Task TrackAsync(Func<Task> action) { try { await action(); } catch { /* Статус не отменяет успе
 157}