< Summary

Information
Class: OzonOrdersWeb.TransactionServices.MikadoCartService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/MikadoCartService.cs
Line coverage
81%
Covered lines: 73
Uncovered lines: 17
Coverable lines: 90
Total lines: 161
Line coverage: 81.1%
Branch coverage
69%
Covered branches: 36
Total branches: 52
Branch coverage: 69.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
GetActiveAsync(...)100%210%
CountActiveAsync(...)100%210%
QueueAsync()57.14%201468.96%
TestAsync()100%121293.33%
RemoveAsync()100%44100%
TrackPlacementAsync()40%101086.36%
TrackCartStatusAsync(...)100%11100%
TrackAsync()100%1166.66%
ReadString(...)50%66100%
ReadInt(...)50%22100%

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);
 20public sealed record MikadoCartResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages)
 21{ 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;
 535    public MikadoCartService(ApplicationDbContext db, MikadoDataManager api,
 536        IOrderSupplierStateService supplierState, IOrderHistoryDataService orderHistory,
 537        ILogger<MikadoCartService>? logger = null) =>
 538        (_db, _api, _supplierState, _orderHistory, _logger) = (db, api, supplierState, orderHistory,
 539            logger ?? NullLogger<MikadoCartService>.Instance);
 40
 041    public Task<List<MikadoCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.MikadoCartItems.AsNoTracking
 042        .Where(x => MikadoCartItemStatus.Active.Contains(x.Status)).OrderByDescending(x => x.CreatedAtUtc).ToListAsync(c
 043    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    {
 2047        var messages = new List<string>(); var added = 0; var skipped = 0; var failed = 0;
 2048        foreach (var item in items)
 49        {
 550            if (item.Quantity < Math.Max(1, item.MinimumQuantity)) { failed++; messages.Add($"{item.Article}: количество
 551            if (await _db.MikadoCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; message
 52            try
 53            {
 554                var xml = await _api.AddToBasketStrictAsync(item.OrderCode, item.Quantity, item.DeliveryType,
 555                    $"Studio2 ID {item.OrderId}", item.ExpressId, item.StockId, ct);
 2556                var result = XDocument.Parse(xml).Descendants().FirstOrDefault(x => x.Name.LocalName == "Basket_Add_Stri
 1557                var id = ReadInt(result, "ID"); var accepted = ReadInt(result, "OrderedQTY"); var message = ReadString(r
 558                if (id <= 0 || accepted != item.Quantity)
 59                {
 060                    if (id > 0) await _api.DeleteBasketItemAsync(id, ct);
 061                    failed++; messages.Add($"{item.Article}: Микадо не принял точное количество. {message}"); continue;
 62                }
 563                var row = new MikadoCartItem { StudioOrderId = item.OrderId, ActiveStudioOrderId = item.OrderId,
 564                    OrderCode = item.OrderCode, StockId = item.StockId, ApiBasketItemId = id, Article = item.Article, Br
 565                    ProductName = item.ProductName, WarehouseName = item.WarehouseName, Quantity = item.Quantity, Minimu
 566                    SourcePrice = item.SourcePrice, DeliveryDays = item.DeliveryDays, DeliveryType = item.DeliveryType, 
 567                _db.MikadoCartItems.Add(row);
 1068                try { await _db.SaveChangesAsync(ct); }
 069                catch (Exception saveEx)
 70                {
 71                    // Позиция уже лежит в корзине Микадо: без компенсации она осталась бы заказанной у поставщика без с
 072                    _db.Entry(row).State = EntityState.Detached;
 073                    try { await _api.DeleteBasketItemAsync(id, CancellationToken.None); messages.Add($"{item.Article}: п
 074                    catch (Exception rollbackEx) { messages.Add($"{item.Article}: ВНИМАНИЕ, позиция осталась в корзине М
 075                    failed++; messages.Add($"{item.Article}: не сохранено в Studio2. {saveEx.Message}"); continue;
 76                }
 577                await TrackPlacementAsync(item, row, user, ct);
 1078                added++; messages.Add($"{item.Article}: добавлено в корзину Микадо.");
 579            }
 080            catch (Exception ex) when (ex is MikadoApiException or HttpRequestException or System.Xml.XmlException)
 081            { failed++; messages.Add($"{item.Article}: {ex.Message}"); }
 582        }
 583        return new MikadoCartResult(added, skipped, failed, messages);
 584    }
 85
 86    public async Task<MikadoCartResult> TestAsync(IReadOnlyCollection<long> ids, string? user = null, CancellationToken 
 87    {
 288        var rows = await _db.MikadoCartItems.Where(x => ids.Contains(x.Id) && MikadoCartItemStatus.Active.Contains(x.Sta
 689        var messages = new List<string>(); var ok = 0; var failed = 0;
 890        foreach (var row in rows)
 91        {
 92            try
 93            {
 294                var xml = await _api.CodeBrandStockInfoAsync(row.Article, row.Brand, ct);
 1295                var line = XDocument.Parse(xml).Descendants().FirstOrDefault(x => x.Name.LocalName == "CodeBrandLine" &&
 496                var stock = ReadInt(line, "StockQTY"); var min = Math.Max(1, ReadInt(line, "MinZakazQTY"));
 797                if (line is null || stock < row.Quantity || row.Quantity < min) { row.Status = MikadoCartItemStatus.Fail
 298                if (decimal.TryParse(ReadString(line, "PriceRUR"), NumberStyles.Number, CultureInfo.InvariantCulture, ou
 799                row.MinimumQuantity = min; row.Status = MikadoCartItemStatus.Ready; row.LastError = null; row.TestedAtUt
 1100            }
 0101            catch (Exception ex) { row.Status = MikadoCartItemStatus.Failed; row.LastError = ex.Message; failed++; messa
 1102        }
 2103        await _db.SaveChangesAsync(ct);
 104        // Проверка меняет статус позиции — состояние заказа у поставщика должно догнать его тем же действием.
 10105        foreach (var row in rows) await TrackCartStatusAsync(row, row.Status, user, ct);
 2106        return new MikadoCartResult(ok, 0, failed, messages);
 2107    }
 108
 109    public async Task<MikadoCartResult> RemoveAsync(IReadOnlyCollection<long>? ids, bool all, string? user = null, Cance
 110    {
 1111        var rows = await _db.MikadoCartItems.Where(x => MikadoCartItemStatus.Active.Contains(x.Status) && (all || ids!.C
 4112        var removed = 0; var failed = 0; var messages = new List<string>(); var closed = new List<MikadoCartItem>();
 12113        foreach (var row in rows) try { await _api.DeleteBasketItemAsync(row.ApiBasketItemId, ct); row.Status = MikadoCa
 1114        await _db.SaveChangesAsync(ct);
 6115        foreach (var row in closed) await TrackAsync(() => _supplierState.RemoveFromCartAsync(row.StudioOrderId, Provide
 1116        return new MikadoCartResult(removed, 0, failed, messages);
 1117    }
 118
 119    /// <summary>Первое попадание позиции в корзину Микадо: статус поставщика + карточка в таймлайне заказа.</summary>
 120    private async Task TrackPlacementAsync(MikadoQueueItem item, MikadoCartItem row, string user, CancellationToken ct)
 121    {
 5122        var direction = string.IsNullOrWhiteSpace(item.Direction) ? item.WarehouseName : item.Direction;
 5123        var deliveryDaysMin = item.DeliveryDaysMin ?? (item.DeliveryDays > 0 ? item.DeliveryDays : null);
 5124        var deliveryDaysMax = item.DeliveryDaysMax ?? (item.DeliveryDays > 0 ? item.DeliveryDays : null);
 125        try
 126        {
 5127            await _supplierState.MarkInCartAsync(item.OrderId, ProviderKey, ApiSupplierBasketCartStatus.Pending,
 5128                row.Id, row.ApiBasketItemId.ToString(CultureInfo.InvariantCulture), user,
 5129                deliveryDaysMin, deliveryDaysMax, direction, ct);
 5130            var entry = new ApiSupplierBasketHistoryEntry(ProviderKey, DisplayName, LogoUrl, true, null,
 5131                row.ApiBasketItemId.ToString(CultureInfo.InvariantCulture), item.Article, item.ProductName,
 5132                item.SourcePrice, "RUB", item.Quantity, item.ShipmentNumber,
 5133                ClientName: item.ClientName, WarehouseName: direction, CartStatus: ApiSupplierBasketCartStatus.Pending,
 5134                SupplierCartUrl: SupplierCartUrl);
 5135            await _orderHistory.AddOrderHistory(new OrderHistory
 5136            {
 5137                OrderId = item.OrderId, ColumnName = ApiSupplierBasketHistoryEntry.HistoryColumnName,
 5138                ColumnDisplayName = $"Корзина {DisplayName}", NewValue = JsonSerializer.Serialize(entry, ApiSupplierBask
 5139                ChangedAt = DateTime.Now, ChangedBy = user
 5140            });
 5141        }
 0142        catch (Exception exception)
 143        {
 144            // Статус и история вторичны: позиция уже лежит в корзине Микадо и записана в Studio2.
 0145            _logger.LogWarning(exception, "Не удалось записать историю корзины Микадо для заказа {OrderId}", item.OrderI
 0146        }
 5147    }
 148
 149    /// <summary>Синхронизирует состояние заказа у поставщика со статусом строки корзины.</summary>
 150    private Task TrackCartStatusAsync(MikadoCartItem row, string cartStatus, string? user, CancellationToken ct) =>
 4151        TrackAsync(() => _supplierState.MarkInCartAsync(row.StudioOrderId, ProviderKey, cartStatus, row.Id,
 4152            row.ApiBasketItemId.ToString(CultureInfo.InvariantCulture), user, ct: ct));
 153
 154    private async Task TrackAsync(Func<Task> action)
 155    {
 6156        try { await action(); }
 0157        catch (Exception exception) { _logger.LogWarning(exception, "Не удалось обновить состояние заказа у поставщика М
 3158    }
 79159    private static string? ReadString(XElement? x, string name) => x?.Elements().FirstOrDefault(e => e.Name.LocalName ==
 16160    private static int ReadInt(XElement? x, string name) => int.TryParse(ReadString(x, name), out var n) ? n : 0;
 161}