< Summary

Information
Class: OzonOrdersWeb.TransactionServices.NikeiCartService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/NikeiCartService.cs
Line coverage
93%
Covered lines: 82
Uncovered lines: 6
Coverable lines: 88
Total lines: 157
Line coverage: 93.1%
Branch coverage
96%
Covered branches: 60
Total branches: 62
Branch coverage: 96.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetActiveAsync(...)100%11100%
CountActiveAsync(...)100%11100%
GetApiCartAsync(...)100%11100%
GetApiOrdersAsync(...)100%11100%
GetOfficesAsync(...)100%11100%
GetClientAddressesAsync(...)100%11100%
QueueAsync()100%202096.15%
CheckoutDirectAsync()90%252076.19%
CheckoutAsync()95%2020100%
RemoveAsync()100%88100%
SelectedAsync(...)100%11100%
TrackAsync()100%11100%

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
 15public sealed record NikeiCartOperationResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 16{
 17    public bool IsSuccess => Failed == 0;
 18    public string ToHtmlBlock() => $"<div class='alert alert-{(Failed == 0 ? "success" : "warning")} mb-0'><strong>Корзи
 19                                  (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
 5830    public NikeiCartService(ApplicationDbContext db, NikeiDataManager nikei, IOrderSupplierStateService states) =>
 5831        (_db, _nikei, _states) = (db, nikei, states);
 32
 333    public Task<List<NikeiCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.NikeiCartItems.AsNoTracking()
 334        .Include(x => x.StudioOrder).Where(x => NikeiCartItemStatus.Active.Contains(x.Status)).OrderByDescending(x => x.
 35
 36    public Task<int> CountActiveAsync(CancellationToken ct = default) =>
 237        _db.NikeiCartItems.CountAsync(x => NikeiCartItemStatus.Active.Contains(x.Status), ct);
 38
 139    public Task<Newtonsoft.Json.Linq.JToken> GetApiCartAsync(CancellationToken ct = default) => _nikei.GetCartAsync(ct);
 140    public Task<Newtonsoft.Json.Linq.JToken> GetApiOrdersAsync(CancellationToken ct = default) => _nikei.GetOrdersAsync(
 141    public Task<Newtonsoft.Json.Linq.JToken> GetOfficesAsync(CancellationToken ct = default) => _nikei.GetOfficesAsync(c
 142    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    {
 8447        var added = 0; var skipped = 0; var failed = 0; var messages = new List<string>();
 9548        foreach (var item in queue)
 49        {
 2750            var label = string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber;
 3951            if (string.IsNullOrWhiteSpace(item.ToCart)) { failed++; messages.Add($"{label}: у предложения Nikei отсутств
 3252            if (item.Quantity < item.Multiplicity || item.Quantity % Math.Max(1, item.Multiplicity) != 0) { failed++; me
 2653            if (item.Stock.HasValue && item.Quantity > item.Stock.Value) { failed++; messages.Add($"{label}: запрошено {
 2454            if (await _db.NikeiCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; messages
 55
 1556            var comment = $"Studio2 {label} (ID {item.OrderId})";
 57            NikeiCartAddResult api;
 2958            try { api = await _nikei.AddToCartAsync(item.ToCart, item.Quantity, comment, ct); }
 459            catch (NikeiApiException ex) { failed++; messages.Add($"{label}: {ex.Message}"); continue; }
 2060            if (!api.IsSuccess || string.IsNullOrWhiteSpace(api.ToOrder)) { failed++; messages.Add($"{label}: {api.Messa
 61
 1262            var row = new NikeiCartItem
 1263            {
 1264                StudioOrderId = item.OrderId, ActiveStudioOrderId = item.OrderId, ToCart = item.ToCart, ToOrder = api.To
 1265                ProductKey = item.ProductKey, Article = item.Article, Brand = item.Brand, ProductName = item.ProductName
 1266                WarehouseName = item.WarehouseName, Quantity = item.Quantity, Multiplicity = Math.Max(1, item.Multiplici
 1267                Stock = item.Stock, SourcePrice = item.SourcePrice, DeliveryDaysMin = item.DeliveryDaysMin, DeliveryDays
 1268                Comment = comment, CreatedBy = user
 1269            };
 1270            _db.NikeiCartItems.Add(row);
 2471            try { await _db.SaveChangesAsync(ct); }
 072            catch (DbUpdateException) { _db.Entry(row).State = EntityState.Detached; skipped++; messages.Add($"{label}: 
 1273            added++;
 74            // to_order у Nikei — SHA-256 (64 символа), а legacy-поле ExternalBasketItemId в общей
 75            // таблице статусов ограничено 50 символами. Полный токен уже хранится в NikeiCartItems.
 2476            await TrackAsync(() => _states.MarkInCartAsync(item.OrderId, ProviderKey, SupplierCartStatusCode.Pending, ro
 2477                user, item.DeliveryDaysMin, item.DeliveryDaysMax, item.WarehouseName, ct));
 1278        }
 2079        return new NikeiCartOperationResult(added, skipped, failed, messages);
 2080    }
 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    {
 890        if (queue.Count == 0) return new NikeiCartOperationResult(0, 0, 1, ["Не выбраны позиции Nikei для оформления."])
 2491        if (queue.GroupBy(x => x.OrderId).Any(x => x.Count() > 1))
 092            return new NikeiCartOperationResult(0, 0, 1, ["Для прямого оформления Nikei требуется ровно одно предложение
 93
 1594        var deliveries = queue.Select(x => (Type: x.ShippingType ?? 4, Address: x.ShippingAddress ?? 0)).Distinct().ToLi
 695        if (deliveries.Count > 1)
 196            return new NikeiCartOperationResult(0, 0, 1, ["Позиции Nikei относятся к разным способам или адресам доставк
 597        var (shippingType, shippingAddress) = deliveries[0];
 598        if (shippingType is not (0 or 4) || shippingAddress <= 0)
 399            return new NikeiCartOperationResult(0, 0, 1, ["Выберите корректный адрес доставки Nikei."]);
 100
 2101        var queued = await QueueAsync(queue, user, ct);
 2102        if (queued.Failed > 0)
 1103            return new NikeiCartOperationResult(queued.Added, queued.Skipped, queued.Failed,
 1104                [.. queued.Messages, "Заказ Nikei не создан: добавленные позиции остались в корзине Nikei."]);
 105
 3106        var orderIds = queue.Select(x => (int?)x.OrderId).ToList();
 1107        var ids = await _db.NikeiCartItems
 1108            .Where(x => orderIds.Contains(x.ActiveStudioOrderId) && NikeiCartItemStatus.Active.Contains(x.Status))
 1109            .Select(x => x.Id).ToListAsync(ct);
 1110        if (ids.Count != queue.Count)
 0111            return new NikeiCartOperationResult(queued.Added, queued.Skipped, 1,
 0112                ["Не все позиции Nikei найдены в корзине. Заказ Nikei не создан."]);
 113
 1114        try { return await CheckoutAsync(ids, shippingType, shippingAddress, "Studio2", user, ct); }
 0115        catch (NikeiApiException ex)
 116        {
 0117            return new NikeiCartOperationResult(0, 0, ids.Count, [$"Nikei: {ex.Message} Позиции остались в корзине Nikei
 118        }
 7119    }
 120
 121    public async Task<NikeiCartOperationResult> CheckoutAsync(IReadOnlyCollection<long> ids, int shippingType, int shipp
 122        string orderComment, string user, CancellationToken ct = default)
 123    {
 16124        var rows = await SelectedAsync(ids, ct);
 17125        if (rows.Count == 0) return new NikeiCartOperationResult(0, 0, 1, ["Выберите позиции Nikei для оформления."]);
 18126        if (shippingType is not (0 or 4) || shippingAddress <= 0) return new NikeiCartOperationResult(0, 0, 1, ["Выберит
 19127        var response = await _nikei.CreateOrderAsync(rows.Select(x => x.ToOrder!).ToList(), shippingType, shippingAddres
 8128        NikeiHttpClient.ThrowIfError(response, "/account/order/create");
 7129        if (!string.Equals(response.Value<string>("status"), "ok", StringComparison.OrdinalIgnoreCase))
 1130            return new NikeiCartOperationResult(0, 0, rows.Count, [response.Value<string>("message") ?? "Nikei не создал
 6131        var invoice = response.Value<string>("message") ?? "Nikei";
 30132        foreach (var row in rows)
 133        {
 45134            row.Status = NikeiCartItemStatus.Ordered; row.ActiveStudioOrderId = null; row.ExternalOrderNumber = invoice;
 135        }
 6136        await _db.SaveChangesAsync(ct);
 48137        foreach (var row in rows) await TrackAsync(() => _states.MarkOrderedAsync(row.StudioOrderId, ProviderKey, invoic
 18138            deliveryDaysMin: row.DeliveryDaysMin, deliveryDaysMax: row.DeliveryDaysMax, direction: row.WarehouseName, ct
 6139        return new NikeiCartOperationResult(rows.Count, 0, 0, [$"Nikei создал заказ {invoice}."]);
 14140    }
 141
 142    public async Task<NikeiCartOperationResult> RemoveAsync(IReadOnlyCollection<long> ids, bool all, string user, Cancel
 143    {
 8144        var rows = all ? await _db.NikeiCartItems.Where(x => NikeiCartItemStatus.Active.Contains(x.Status)).ToListAsync(
 8145        if (rows.Count == 0) return new NikeiCartOperationResult(0, 0, 0, []);
 26146        try { await _nikei.RemoveFromCartAsync(rows.Where(x => !string.IsNullOrWhiteSpace(x.ToOrder)).Select(x => x.ToOr
 2147        catch (NikeiApiException ex) { return new NikeiCartOperationResult(0, 0, rows.Count, [ex.Message]); }
 45148        foreach (var row in rows) { row.Status = NikeiCartItemStatus.Removed; row.ActiveStudioOrderId = null; row.Update
 5149        await _db.SaveChangesAsync(ct);
 38150        foreach (var row in rows) await TrackAsync(() => _states.RemoveFromCartAsync(row.StudioOrderId, ProviderKey, use
 5151        return new NikeiCartOperationResult(rows.Count, 0, 0, []);
 7152    }
 153
 23154    private Task<List<NikeiCartItem>> SelectedAsync(IReadOnlyCollection<long> ids, CancellationToken ct) => _db.NikeiCar
 23155        .Where(x => ids.Contains(x.Id) && NikeiCartItemStatus.Active.Contains(x.Status)).ToListAsync(ct);
 87156    private static async Task TrackAsync(Func<Task> action) { try { await action(); } catch { /* Статус не отменяет успе
 157}