< Summary

Information
Class: OzonOrdersWeb.TransactionServices.MotexCartService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/MotexCartService.cs
Line coverage
100%
Covered lines: 79
Uncovered lines: 0
Coverable lines: 79
Total lines: 148
Line coverage: 100%
Branch coverage
96%
Covered branches: 58
Total branches: 60
Branch coverage: 96.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
CountActiveAsync(...)100%11100%
GetActiveAsync(...)100%11100%
GetOrdersAsync(...)100%11100%
QueueAsync()100%66100%
CheckoutDirectAsync()100%44100%
CheckoutAsync()100%66100%
RemoveAsync()100%88100%
CreateOrderAsync()87.5%3232100%
IsValid(...)100%66100%
ToCartItem(...)100%44100%
TrackAsync()100%11100%

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
 19public sealed record MotexCartResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 20{
 21    public bool IsSuccess => Failed == 0;
 22    public string ToHtmlBlock()
 23    {
 24        var css = Failed > 0 ? "alert-warning" : "alert-success";
 25        var list = string.Join(string.Empty, Messages.Select(x => $"<li>{WebUtility.HtmlEncode(x)}</li>"));
 26        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;
 3836    public MotexCartService(ApplicationDbContext db, MotexDataManager api, IOrderSupplierStateService supplierState,
 7637        ILogger<MotexCartService>? logger = null) => (_db, _api, _supplierState, _logger) =
 3838        (db, api, supplierState, logger ?? NullLogger<MotexCartService>.Instance);
 39
 140    public Task<int> CountActiveAsync(CancellationToken ct = default) => _db.MotexCartItems.CountAsync(x => MotexCartIte
 141    public Task<List<MotexCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.MotexCartItems.Include(x => x
 142    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    {
 6046        var added = 0; var skipped = 0; var failed = 0; var messages = new List<string>();
 6247        foreach (var item in queue)
 48        {
 4349            if (!IsValid(item)) { failed++; messages.Add("Некорректное предложение МоТехС."); continue; }
 1350            if (await _db.MotexCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; messages
 551            var row = ToCartItem(item, user);
 552            _db.MotexCartItems.Add(row);
 553            await _db.SaveChangesAsync(ct);
 1054            await TrackAsync(() => _supplierState.MarkInCartAsync(row.StudioOrderId, ApiSupplierSettings.MotexProviderKe
 1055            added++; messages.Add($"{item.Code}: добавлен в корзину МоТехС.");
 556        }
 1557        return new(added, skipped, failed, messages);
 1558    }
 59
 60    public async Task<MotexCartResult> CheckoutDirectAsync(IReadOnlyCollection<MotexCartQueueItem> queue, string user, C
 61    {
 2362        if (queue.Count == 0 || queue.GroupBy(x => x.OrderId).Any(x => x.Key <= 0 || x.Count() > 1) || queue.Any(x => !I
 363            return new(0, 0, 1, ["Для прямого оформления МоТехС требуется по одному корректному предложению на заказ."])
 564        var rows = queue.Select(x => ToCartItem(x, user)).ToList();
 265        return await CreateOrderAsync(rows, null, user, ct);
 566    }
 67
 68    public async Task<MotexCartResult> CheckoutAsync(IReadOnlyCollection<long>? ids, string user, CancellationToken ct =
 69    {
 1170        var query = _db.MotexCartItems.Where(x => MotexCartItemStatus.Active.Contains(x.Status));
 1871        if (ids is { Count: > 0 }) query = query.Where(x => ids.Contains(x.Id));
 1172        var rows = await query.ToListAsync(ct);
 1173        return rows.Count == 0 ? new(0, 0, 1, ["Корзина МоТехС пуста."]) : await CreateOrderAsync(rows, rows, user, ct);
 1174    }
 75
 76    public async Task<MotexCartResult> RemoveAsync(IReadOnlyCollection<long>? ids, CancellationToken ct = default)
 77    {
 478        var query = _db.MotexCartItems.Where(x => MotexCartItemStatus.Active.Contains(x.Status));
 779        if (ids is { Count: > 0 }) query = query.Where(x => ids.Contains(x.Id));
 480        var rows = await query.ToListAsync(ct);
 581        if (rows.Count == 0) return new(0, 0, 1, ["Выберите позиции МоТехС."]);
 3482        foreach (var row in rows) { row.Status = MotexCartItemStatus.Removed; row.ActiveStudioOrderId = null; row.Update
 383        await _db.SaveChangesAsync(ct);
 384        return new(rows.Count, 0, 0, ["Позиции удалены из корзины МоТехС."]);
 485    }
 86
 87    private async Task<MotexCartResult> CreateOrderAsync(IReadOnlyList<MotexCartItem> rows, IReadOnlyList<MotexCartItem>
 88    {
 1289        var addresses = (await _api.GetAsync("/api/v1/Users/GetUserDeliveryAddresses", null, ct))["deliveryAddresses"] a
 3890        var codes = addresses?.OfType<JObject>().Select(x => x.Value<string>("code")).Where(x => !string.IsNullOrWhiteSp
 1491        if (codes.Count != 1) return new(0, 0, rows.Count, ["МоТехС: для оформления должен быть ровно один адрес доставк
 92
 5093        foreach (var row in persistedRows ?? []) row.Status = MotexCartItemStatus.Processing;
 1894        if (persistedRows is not null) await _db.SaveChangesAsync(ct);
 1095        var order = new MotexOrder { DeliveryAddressCode = codes[0]!, CreatedBy = user };
 2096        _db.MotexOrders.Add(order); await _db.SaveChangesAsync(ct);
 1097        order.ClientOrderNum = $"ST2-MTX-{order.Id}";
 1098        var request = new JObject
 1099        {
 10100            ["clientOrderNum"] = order.ClientOrderNum,
 10101            ["deliveryAddressCode"] = order.DeliveryAddressCode,
 10102            ["comment"] = "Studio2",
 10103            ["orderForm"] = 1,
 13104            ["orderItems"] = new JArray(rows.Select(x => new JObject { ["code"] = x.Code, ["brand"] = x.Brand, ["quantit
 10105        };
 20106        order.RequestJson = request.ToString(Formatting.None); await _db.SaveChangesAsync(ct);
 107        try
 108        {
 109            JToken response;
 16110            try { response = await _api.CreateOrderAsync(request, ct); }
 4111            catch (Exception ex) when (ex is MotexApiException or HttpRequestException)
 112            {
 113                // Восстанавливаем заказ по clientOrderNum на случай, если CreateOrder создал его, но ответ потерялся
 114                // (таймаут/обрыв сети). Если и это падает — значит заказа не было, и полезна исходная ошибка, а не 404 
 5115                try { response = await _api.GetOrderDetailsAsync(null, order.ClientOrderNum, ct); }
 6116                catch (Exception ex2) when (ex2 is MotexApiException or HttpRequestException) { throw ex; }
 117            }
 7118            var payload = response["order"] as JObject ?? response as JObject ?? new JObject();
 7119            order.ExternalOrderId = payload.Value<string>("id");
 7120            order.Status = payload.Value<string>("status") ?? "Создан";
 7121            order.ResponseJson = payload.ToString(Formatting.None);
 34122            foreach (var row in rows)
 123            {
 20124                var local = persistedRows?.FirstOrDefault(x => x.Id == row.Id);
 42125                if (local is not null) { local.Status = MotexCartItemStatus.Ordered; local.ActiveStudioOrderId = null; l
 10126                order.Items.Add(new MotexOrderItem { MotexCartItemId = local?.Id, StudioOrderId = row.StudioOrderId, Cod
 20127                await TrackAsync(() => _supplierState.MarkOrderedAsync(row.StudioOrderId, ApiSupplierSettings.MotexProvi
 128            }
 7129            await _db.SaveChangesAsync(ct);
 7130            return new(rows.Count, 0, 0, [$"Создан заказ МоТехС №{order.ExternalOrderId ?? order.ClientOrderNum}."]);
 131        }
 3132        catch (Exception ex) when (ex is MotexApiException or HttpRequestException)
 133        {
 6134            order.Status = "Failed"; order.ResponseJson = ex.Message;
 16135            foreach (var row in persistedRows ?? []) { row.Status = MotexCartItemStatus.Failed; row.LastError = ex.Messa
 3136            await _db.SaveChangesAsync(ct);
 3137            return new(0, 0, rows.Count, [$"МоТехС: {ex.Message}"]);
 138        }
 12139    }
 140
 21141    private static bool IsValid(MotexCartQueueItem item) => item.OrderId > 0 && !string.IsNullOrWhiteSpace(item.Code) &&
 8142    private static MotexCartItem ToCartItem(MotexCartQueueItem item, string user) => new() { StudioOrderId = item.OrderI
 143    private async Task TrackAsync(Func<Task> action)
 144    {
 36145        try { await action(); }
 6146        catch (Exception exception) { _logger.LogWarning(exception, "Не удалось обновить состояние заказа у поставщика М
 19147    }
 148}