< Summary

Information
Class: OzonOrdersWeb.TransactionServices.MikadoCartResult
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/MikadoCartService.cs
Line coverage
50%
Covered lines: 1
Uncovered lines: 1
Coverable lines: 2
Total lines: 161
Line coverage: 50%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
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%210%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using System.Text.Json;
 3using System.Xml.Linq;
 4using Microsoft.EntityFrameworkCore;
 5using Microsoft.Extensions.Logging;
 6using Microsoft.Extensions.Logging.Abstractions;
 7using OzonDomains.Models;
 8using OzonRepositories.Context;
 9using Services.ApiServices.MikadoApi;
 10using Services.DataServices;
 11using Services.SupplierStatusServices;
 12
 13namespace OzonOrdersWeb.TransactionServices;
 14
 15public sealed record MikadoQueueItem(int OrderId, string OrderCode, int StockId, string Article, string Brand,
 16    string? ProductName, string? WarehouseName, int Quantity, int MinimumQuantity, decimal? SourcePrice,
 17    int DeliveryDays, int DeliveryType, int ExpressId, string? Direction = null,
 18    int? DeliveryDaysMin = null, int? DeliveryDaysMax = null,
 19    string? ShipmentNumber = null, string? ClientName = null);
 1120public sealed record MikadoCartResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 021{ public bool IsSuccess => Failed == 0; }
 22
 23public sealed class MikadoCartService
 24{
 25    private const string ProviderKey = ApiSupplierSettings.MikadoMskHod20ProviderKey;
 26    private const string SupplierCartUrl = "/SupplierCart?provider=mikadomskhod20&supplierTab=basket";
 27    private const string DisplayName = "Микадо МскХод20";
 28    private const string LogoUrl = "https://mikado-parts.ru/img/MikadoLogo.svg";
 29
 30    private readonly ApplicationDbContext _db;
 31    private readonly MikadoDataManager _api;
 32    private readonly IOrderSupplierStateService _supplierState;
 33    private readonly IOrderHistoryDataService _orderHistory;
 34    private readonly ILogger<MikadoCartService> _logger;
 35    public MikadoCartService(ApplicationDbContext db, MikadoDataManager api,
 36        IOrderSupplierStateService supplierState, IOrderHistoryDataService orderHistory,
 37        ILogger<MikadoCartService>? logger = null) =>
 38        (_db, _api, _supplierState, _orderHistory, _logger) = (db, api, supplierState, orderHistory,
 39            logger ?? NullLogger<MikadoCartService>.Instance);
 40
 41    public Task<List<MikadoCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.MikadoCartItems.AsNoTracking
 42        .Where(x => MikadoCartItemStatus.Active.Contains(x.Status)).OrderByDescending(x => x.CreatedAtUtc).ToListAsync(c
 43    public Task<int> CountActiveAsync(CancellationToken ct = default) => _db.MikadoCartItems.CountAsync(x => MikadoCartI
 44
 45    public async Task<MikadoCartResult> QueueAsync(IReadOnlyCollection<MikadoQueueItem> items, string user, Cancellation
 46    {
 47        var messages = new List<string>(); var added = 0; var skipped = 0; var failed = 0;
 48        foreach (var item in items)
 49        {
 50            if (item.Quantity < Math.Max(1, item.MinimumQuantity)) { failed++; messages.Add($"{item.Article}: количество
 51            if (await _db.MikadoCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; message
 52            try
 53            {
 54                var xml = await _api.AddToBasketStrictAsync(item.OrderCode, item.Quantity, item.DeliveryType,
 55                    $"Studio2 ID {item.OrderId}", item.ExpressId, item.StockId, ct);
 56                var result = XDocument.Parse(xml).Descendants().FirstOrDefault(x => x.Name.LocalName == "Basket_Add_Stri
 57                var id = ReadInt(result, "ID"); var accepted = ReadInt(result, "OrderedQTY"); var message = ReadString(r
 58                if (id <= 0 || accepted != item.Quantity)
 59                {
 60                    if (id > 0) await _api.DeleteBasketItemAsync(id, ct);
 61                    failed++; messages.Add($"{item.Article}: Микадо не принял точное количество. {message}"); continue;
 62                }
 63                var row = new MikadoCartItem { StudioOrderId = item.OrderId, ActiveStudioOrderId = item.OrderId,
 64                    OrderCode = item.OrderCode, StockId = item.StockId, ApiBasketItemId = id, Article = item.Article, Br
 65                    ProductName = item.ProductName, WarehouseName = item.WarehouseName, Quantity = item.Quantity, Minimu
 66                    SourcePrice = item.SourcePrice, DeliveryDays = item.DeliveryDays, DeliveryType = item.DeliveryType, 
 67                _db.MikadoCartItems.Add(row);
 68                try { await _db.SaveChangesAsync(ct); }
 69                catch (Exception saveEx)
 70                {
 71                    // Позиция уже лежит в корзине Микадо: без компенсации она осталась бы заказанной у поставщика без с
 72                    _db.Entry(row).State = EntityState.Detached;
 73                    try { await _api.DeleteBasketItemAsync(id, CancellationToken.None); messages.Add($"{item.Article}: п
 74                    catch (Exception rollbackEx) { messages.Add($"{item.Article}: ВНИМАНИЕ, позиция осталась в корзине М
 75                    failed++; messages.Add($"{item.Article}: не сохранено в Studio2. {saveEx.Message}"); continue;
 76                }
 77                await TrackPlacementAsync(item, row, user, ct);
 78                added++; messages.Add($"{item.Article}: добавлено в корзину Микадо.");
 79            }
 80            catch (Exception ex) when (ex is MikadoApiException or HttpRequestException or System.Xml.XmlException)
 81            { failed++; messages.Add($"{item.Article}: {ex.Message}"); }
 82        }
 83        return new MikadoCartResult(added, skipped, failed, messages);
 84    }
 85
 86    public async Task<MikadoCartResult> TestAsync(IReadOnlyCollection<long> ids, string? user = null, CancellationToken 
 87    {
 88        var rows = await _db.MikadoCartItems.Where(x => ids.Contains(x.Id) && MikadoCartItemStatus.Active.Contains(x.Sta
 89        var messages = new List<string>(); var ok = 0; var failed = 0;
 90        foreach (var row in rows)
 91        {
 92            try
 93            {
 94                var xml = await _api.CodeBrandStockInfoAsync(row.Article, row.Brand, ct);
 95                var line = XDocument.Parse(xml).Descendants().FirstOrDefault(x => x.Name.LocalName == "CodeBrandLine" &&
 96                var stock = ReadInt(line, "StockQTY"); var min = Math.Max(1, ReadInt(line, "MinZakazQTY"));
 97                if (line is null || stock < row.Quantity || row.Quantity < min) { row.Status = MikadoCartItemStatus.Fail
 98                if (decimal.TryParse(ReadString(line, "PriceRUR"), NumberStyles.Number, CultureInfo.InvariantCulture, ou
 99                row.MinimumQuantity = min; row.Status = MikadoCartItemStatus.Ready; row.LastError = null; row.TestedAtUt
 100            }
 101            catch (Exception ex) { row.Status = MikadoCartItemStatus.Failed; row.LastError = ex.Message; failed++; messa
 102        }
 103        await _db.SaveChangesAsync(ct);
 104        // Проверка меняет статус позиции — состояние заказа у поставщика должно догнать его тем же действием.
 105        foreach (var row in rows) await TrackCartStatusAsync(row, row.Status, user, ct);
 106        return new MikadoCartResult(ok, 0, failed, messages);
 107    }
 108
 109    public async Task<MikadoCartResult> RemoveAsync(IReadOnlyCollection<long>? ids, bool all, string? user = null, Cance
 110    {
 111        var rows = await _db.MikadoCartItems.Where(x => MikadoCartItemStatus.Active.Contains(x.Status) && (all || ids!.C
 112        var removed = 0; var failed = 0; var messages = new List<string>(); var closed = new List<MikadoCartItem>();
 113        foreach (var row in rows) try { await _api.DeleteBasketItemAsync(row.ApiBasketItemId, ct); row.Status = MikadoCa
 114        await _db.SaveChangesAsync(ct);
 115        foreach (var row in closed) await TrackAsync(() => _supplierState.RemoveFromCartAsync(row.StudioOrderId, Provide
 116        return new MikadoCartResult(removed, 0, failed, messages);
 117    }
 118
 119    /// <summary>Первое попадание позиции в корзину Микадо: статус поставщика + карточка в таймлайне заказа.</summary>
 120    private async Task TrackPlacementAsync(MikadoQueueItem item, MikadoCartItem row, string user, CancellationToken ct)
 121    {
 122        var direction = string.IsNullOrWhiteSpace(item.Direction) ? item.WarehouseName : item.Direction;
 123        var deliveryDaysMin = item.DeliveryDaysMin ?? (item.DeliveryDays > 0 ? item.DeliveryDays : null);
 124        var deliveryDaysMax = item.DeliveryDaysMax ?? (item.DeliveryDays > 0 ? item.DeliveryDays : null);
 125        try
 126        {
 127            await _supplierState.MarkInCartAsync(item.OrderId, ProviderKey, ApiSupplierBasketCartStatus.Pending,
 128                row.Id, row.ApiBasketItemId.ToString(CultureInfo.InvariantCulture), user,
 129                deliveryDaysMin, deliveryDaysMax, direction, ct);
 130            var entry = new ApiSupplierBasketHistoryEntry(ProviderKey, DisplayName, LogoUrl, true, null,
 131                row.ApiBasketItemId.ToString(CultureInfo.InvariantCulture), item.Article, item.ProductName,
 132                item.SourcePrice, "RUB", item.Quantity, item.ShipmentNumber,
 133                ClientName: item.ClientName, WarehouseName: direction, CartStatus: ApiSupplierBasketCartStatus.Pending,
 134                SupplierCartUrl: SupplierCartUrl);
 135            await _orderHistory.AddOrderHistory(new OrderHistory
 136            {
 137                OrderId = item.OrderId, ColumnName = ApiSupplierBasketHistoryEntry.HistoryColumnName,
 138                ColumnDisplayName = $"Корзина {DisplayName}", NewValue = JsonSerializer.Serialize(entry, ApiSupplierBask
 139                ChangedAt = DateTime.Now, ChangedBy = user
 140            });
 141        }
 142        catch (Exception exception)
 143        {
 144            // Статус и история вторичны: позиция уже лежит в корзине Микадо и записана в Studio2.
 145            _logger.LogWarning(exception, "Не удалось записать историю корзины Микадо для заказа {OrderId}", item.OrderI
 146        }
 147    }
 148
 149    /// <summary>Синхронизирует состояние заказа у поставщика со статусом строки корзины.</summary>
 150    private Task TrackCartStatusAsync(MikadoCartItem row, string cartStatus, string? user, CancellationToken ct) =>
 151        TrackAsync(() => _supplierState.MarkInCartAsync(row.StudioOrderId, ProviderKey, cartStatus, row.Id,
 152            row.ApiBasketItemId.ToString(CultureInfo.InvariantCulture), user, ct: ct));
 153
 154    private async Task TrackAsync(Func<Task> action)
 155    {
 156        try { await action(); }
 157        catch (Exception exception) { _logger.LogWarning(exception, "Не удалось обновить состояние заказа у поставщика М
 158    }
 159    private static string? ReadString(XElement? x, string name) => x?.Elements().FirstOrDefault(e => e.Name.LocalName ==
 160    private static int ReadInt(XElement? x, string name) => int.TryParse(ReadString(x, name), out var n) ? n : 0;
 161}

Methods/Properties

get_Added()
get_IsSuccess()