< Summary

Information
Class: OzonOrdersWeb.TransactionServices.MotexCartResult
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/MotexCartService.cs
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 148
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
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%22100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using System.Net;
 3using Microsoft.EntityFrameworkCore;
 4using Microsoft.Extensions.Logging;
 5using Microsoft.Extensions.Logging.Abstractions;
 6using Newtonsoft.Json;
 7using Newtonsoft.Json.Linq;
 8using OzonDomains.Models;
 9using OzonRepositories.Context;
 10using Services.ApiServices.MotexApi;
 11using Services.SupplierStatusServices;
 12
 13namespace OzonOrdersWeb.TransactionServices;
 14
 15public sealed record MotexCartQueueItem(int OrderId, string Code, string Brand, string? ProductName, string? StoreCode,
 16    string? StoreInfo, string? ShipmentNumber, decimal Quantity, decimal? SourcePrice, string? CurrencyCode,
 17    string? DeliveryAtUtc, int? DeliveryDaysMin, int? DeliveryDaysMax, string? Direction);
 18
 17219public sealed record MotexCartResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 20{
 3621    public bool IsSuccess => Failed == 0;
 22    public string ToHtmlBlock()
 23    {
 224        var css = Failed > 0 ? "alert-warning" : "alert-success";
 525        var list = string.Join(string.Empty, Messages.Select(x => $"<li>{WebUtility.HtmlEncode(x)}</li>"));
 226        return $"<div class='alert {css} mb-0'><strong>МоТехС: добавлено {Added}, уже было {Skipped}, ошибок {Failed}.</
 27    }
 28}
 29
 30public sealed class MotexCartService
 31{
 32    private readonly ApplicationDbContext _db;
 33    private readonly MotexDataManager _api;
 34    private readonly IOrderSupplierStateService _supplierState;
 35    private readonly ILogger<MotexCartService> _logger;
 36    public MotexCartService(ApplicationDbContext db, MotexDataManager api, IOrderSupplierStateService supplierState,
 37        ILogger<MotexCartService>? logger = null) => (_db, _api, _supplierState, _logger) =
 38        (db, api, supplierState, logger ?? NullLogger<MotexCartService>.Instance);
 39
 40    public Task<int> CountActiveAsync(CancellationToken ct = default) => _db.MotexCartItems.CountAsync(x => MotexCartIte
 41    public Task<List<MotexCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.MotexCartItems.Include(x => x
 42    public Task<List<MotexOrder>> GetOrdersAsync(CancellationToken ct = default) => _db.MotexOrders.Include(x => x.Items
 43
 44    public async Task<MotexCartResult> QueueAsync(IReadOnlyCollection<MotexCartQueueItem> queue, string user, Cancellati
 45    {
 46        var added = 0; var skipped = 0; var failed = 0; var messages = new List<string>();
 47        foreach (var item in queue)
 48        {
 49            if (!IsValid(item)) { failed++; messages.Add("Некорректное предложение МоТехС."); continue; }
 50            if (await _db.MotexCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; messages
 51            var row = ToCartItem(item, user);
 52            _db.MotexCartItems.Add(row);
 53            await _db.SaveChangesAsync(ct);
 54            await TrackAsync(() => _supplierState.MarkInCartAsync(row.StudioOrderId, ApiSupplierSettings.MotexProviderKe
 55            added++; messages.Add($"{item.Code}: добавлен в корзину МоТехС.");
 56        }
 57        return new(added, skipped, failed, messages);
 58    }
 59
 60    public async Task<MotexCartResult> CheckoutDirectAsync(IReadOnlyCollection<MotexCartQueueItem> queue, string user, C
 61    {
 62        if (queue.Count == 0 || queue.GroupBy(x => x.OrderId).Any(x => x.Key <= 0 || x.Count() > 1) || queue.Any(x => !I
 63            return new(0, 0, 1, ["Для прямого оформления МоТехС требуется по одному корректному предложению на заказ."])
 64        var rows = queue.Select(x => ToCartItem(x, user)).ToList();
 65        return await CreateOrderAsync(rows, null, user, ct);
 66    }
 67
 68    public async Task<MotexCartResult> CheckoutAsync(IReadOnlyCollection<long>? ids, string user, CancellationToken ct =
 69    {
 70        var query = _db.MotexCartItems.Where(x => MotexCartItemStatus.Active.Contains(x.Status));
 71        if (ids is { Count: > 0 }) query = query.Where(x => ids.Contains(x.Id));
 72        var rows = await query.ToListAsync(ct);
 73        return rows.Count == 0 ? new(0, 0, 1, ["Корзина МоТехС пуста."]) : await CreateOrderAsync(rows, rows, user, ct);
 74    }
 75
 76    public async Task<MotexCartResult> RemoveAsync(IReadOnlyCollection<long>? ids, CancellationToken ct = default)
 77    {
 78        var query = _db.MotexCartItems.Where(x => MotexCartItemStatus.Active.Contains(x.Status));
 79        if (ids is { Count: > 0 }) query = query.Where(x => ids.Contains(x.Id));
 80        var rows = await query.ToListAsync(ct);
 81        if (rows.Count == 0) return new(0, 0, 1, ["Выберите позиции МоТехС."]);
 82        foreach (var row in rows) { row.Status = MotexCartItemStatus.Removed; row.ActiveStudioOrderId = null; row.Update
 83        await _db.SaveChangesAsync(ct);
 84        return new(rows.Count, 0, 0, ["Позиции удалены из корзины МоТехС."]);
 85    }
 86
 87    private async Task<MotexCartResult> CreateOrderAsync(IReadOnlyList<MotexCartItem> rows, IReadOnlyList<MotexCartItem>
 88    {
 89        var addresses = (await _api.GetAsync("/api/v1/Users/GetUserDeliveryAddresses", null, ct))["deliveryAddresses"] a
 90        var codes = addresses?.OfType<JObject>().Select(x => x.Value<string>("code")).Where(x => !string.IsNullOrWhiteSp
 91        if (codes.Count != 1) return new(0, 0, rows.Count, ["МоТехС: для оформления должен быть ровно один адрес доставк
 92
 93        foreach (var row in persistedRows ?? []) row.Status = MotexCartItemStatus.Processing;
 94        if (persistedRows is not null) await _db.SaveChangesAsync(ct);
 95        var order = new MotexOrder { DeliveryAddressCode = codes[0]!, CreatedBy = user };
 96        _db.MotexOrders.Add(order); await _db.SaveChangesAsync(ct);
 97        order.ClientOrderNum = $"ST2-MTX-{order.Id}";
 98        var request = new JObject
 99        {
 100            ["clientOrderNum"] = order.ClientOrderNum,
 101            ["deliveryAddressCode"] = order.DeliveryAddressCode,
 102            ["comment"] = "Studio2",
 103            ["orderForm"] = 1,
 104            ["orderItems"] = new JArray(rows.Select(x => new JObject { ["code"] = x.Code, ["brand"] = x.Brand, ["quantit
 105        };
 106        order.RequestJson = request.ToString(Formatting.None); await _db.SaveChangesAsync(ct);
 107        try
 108        {
 109            JToken response;
 110            try { response = await _api.CreateOrderAsync(request, ct); }
 111            catch (Exception ex) when (ex is MotexApiException or HttpRequestException)
 112            {
 113                // Восстанавливаем заказ по clientOrderNum на случай, если CreateOrder создал его, но ответ потерялся
 114                // (таймаут/обрыв сети). Если и это падает — значит заказа не было, и полезна исходная ошибка, а не 404 
 115                try { response = await _api.GetOrderDetailsAsync(null, order.ClientOrderNum, ct); }
 116                catch (Exception ex2) when (ex2 is MotexApiException or HttpRequestException) { throw ex; }
 117            }
 118            var payload = response["order"] as JObject ?? response as JObject ?? new JObject();
 119            order.ExternalOrderId = payload.Value<string>("id");
 120            order.Status = payload.Value<string>("status") ?? "Создан";
 121            order.ResponseJson = payload.ToString(Formatting.None);
 122            foreach (var row in rows)
 123            {
 124                var local = persistedRows?.FirstOrDefault(x => x.Id == row.Id);
 125                if (local is not null) { local.Status = MotexCartItemStatus.Ordered; local.ActiveStudioOrderId = null; l
 126                order.Items.Add(new MotexOrderItem { MotexCartItemId = local?.Id, StudioOrderId = row.StudioOrderId, Cod
 127                await TrackAsync(() => _supplierState.MarkOrderedAsync(row.StudioOrderId, ApiSupplierSettings.MotexProvi
 128            }
 129            await _db.SaveChangesAsync(ct);
 130            return new(rows.Count, 0, 0, [$"Создан заказ МоТехС №{order.ExternalOrderId ?? order.ClientOrderNum}."]);
 131        }
 132        catch (Exception ex) when (ex is MotexApiException or HttpRequestException)
 133        {
 134            order.Status = "Failed"; order.ResponseJson = ex.Message;
 135            foreach (var row in persistedRows ?? []) { row.Status = MotexCartItemStatus.Failed; row.LastError = ex.Messa
 136            await _db.SaveChangesAsync(ct);
 137            return new(0, 0, rows.Count, [$"МоТехС: {ex.Message}"]);
 138        }
 139    }
 140
 141    private static bool IsValid(MotexCartQueueItem item) => item.OrderId > 0 && !string.IsNullOrWhiteSpace(item.Code) &&
 142    private static MotexCartItem ToCartItem(MotexCartQueueItem item, string user) => new() { StudioOrderId = item.OrderI
 143    private async Task TrackAsync(Func<Task> action)
 144    {
 145        try { await action(); }
 146        catch (Exception exception) { _logger.LogWarning(exception, "Не удалось обновить состояние заказа у поставщика М
 147    }
 148}