< Summary

Information
Class: Services.SupplierStatusServices.Providers.MlAutoSupplierStatusProviderBase
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/MlAutoSupplierStatusProvider.cs
Line coverage
92%
Covered lines: 73
Uncovered lines: 6
Coverable lines: 79
Total lines: 203
Line coverage: 92.4%
Branch coverage
96%
Covered branches: 31
Total branches: 32
Branch coverage: 96.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
FetchCartAsync(...)100%11100%
FetchOrderAsync()100%2020100%
<FetchOrderAsync()50%2264.7%
ResolveStatusName(...)100%88100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/MlAutoSupplierStatusProvider.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Logging;
 3using OzonDomains.Models;
 4using OzonDomains.Models.SupplierStatuses;
 5using OzonRepositories.Context;
 6using Services.ApiServices.MlAutoApi;
 7
 8namespace Services.SupplierStatusServices.Providers;
 9
 10/// <summary>
 11/// Статусы ML-Auto. Корзина локальная — читается из БД; заказ опрашивается через
 12/// GetOrderByNumber, по одному HTTP-вызову на номер заказа, поэтому параллелизм ограничен.
 13/// </summary>
 14public abstract class MlAutoSupplierStatusProviderBase : ISupplierStatusProvider
 15{
 16    /// <summary>Состав каждого заказа — отдельный запрос; держим ту же границу, что и страница корзин.</summary>
 17    private const int DetailsConcurrency = 4;
 18
 19    private readonly ApplicationDbContext _db;
 20    private readonly LocalCartStatusReader _cartReader;
 21    private readonly ILogger _logger;
 22
 3023    protected MlAutoSupplierStatusProviderBase(
 3024        ApplicationDbContext db, LocalCartStatusReader cartReader, ILogger logger)
 25    {
 3026        _db = db;
 3027        _cartReader = cartReader;
 3028        _logger = logger;
 3029    }
 30
 31    public abstract string ProviderKey { get; }
 32
 33    /// <summary>
 34    /// Позиции заказа у поставщика. BY и RU — разные клиенты без общего базового класса,
 35    /// поэтому абстрагируем сам вызов, а не тип менеджера.
 36    /// </summary>
 37    protected abstract Task<IReadOnlyList<MlAutoOrderItemInfo>> GetOrderItemsAsync(
 38        string orderNumber, CancellationToken ct);
 39
 40    /// <summary>Связки «номер заказа ML-Auto → заказы Studio2» для нужного региона.</summary>
 41    protected abstract Task<IReadOnlyList<(string OrderNumber, int StudioOrderId, string Pin, string Brand)>>
 42        LoadOrderLinksAsync(ApplicationDbContext db, List<int> orderIds, CancellationToken ct);
 43
 44    public Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(
 45        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct) =>
 546        _cartReader.ReadAsync(ProviderKey, states, ct);
 47
 48    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(
 49        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 50    {
 2451        if (states.Count == 0) return [];
 52
 5253        var orderIds = states.Select(x => x.OrderId).Distinct().ToList();
 2254        var links = await LoadOrderLinksAsync(_db, orderIds, ct);
 2555        if (links.Count == 0) return [];
 56
 1957        var byOrderNumber = links
 2658            .GroupBy(x => x.OrderNumber, StringComparer.Ordinal)
 1959            .ToList();
 60
 1961        var result = new List<SupplierStatusSnapshot>();
 1962        var gate = new SemaphoreSlim(DetailsConcurrency);
 63        // Лимит запросов в минуту временный: после первого отказа остальные заказы не опрашиваем, а не бьёмся в тот же 
 1964        var rateLimited = false;
 65        // Порядок случайный: при упоре в лимит пропущенные заказы помечаются опрошенными вместе с остальными,
 66        // и фиксированный порядок оставил бы одни и те же заказы без обновления навсегда.
 4467        var tasks = byOrderNumber.OrderBy(_ => Random.Shared.Next()).Select(async group =>
 1968        {
 2569            await gate.WaitAsync(ct);
 1970            try
 1971            {
 2572                if (Volatile.Read(ref rateLimited))
 073                    return (Group: group, Items: (IReadOnlyList<MlAutoOrderItemInfo>)[], Error: (string?)null);
 2574                return (Group: group, Items: await GetOrderItemsAsync(group.Key, ct), Error: (string?)null);
 1975            }
 276            catch (MlAutoApiException ex) when (ex.IsRateLimited)
 1977            {
 078                Volatile.Write(ref rateLimited, true);
 079                _logger.LogWarning(
 080                    "{Provider}: лимит запросов исчерпан на заказе {Number}, остальные заказы пропущены до следующего пр
 081                    ProviderKey, group.Key);
 082                return (Group: group, Items: (IReadOnlyList<MlAutoOrderItemInfo>)[], Error: (string?)null);
 1983            }
 584            catch (Exception ex) when (ex is MlAutoApiException or HttpRequestException or TaskCanceledException)
 1985            {
 586                _logger.LogInformation(
 587                    "{Provider}: не удалось прочитать заказ {Number}: {Error}",
 588                    ProviderKey, group.Key, ex.Message);
 589                return (Group: group, Items: (IReadOnlyList<MlAutoOrderItemInfo>)[], Error: $"{ProviderKey}: {ex.Message
 1990            }
 1991            finally
 1992            {
 2593                gate.Release();
 1994            }
 4495        });
 96
 8897        foreach (var (group, items, error) in await Task.WhenAll(tasks))
 98        {
 10299            foreach (var link in group)
 100            {
 26101                if (error is not null)
 102                {
 5103                    result.Add(new SupplierStatusSnapshot
 5104                    {
 5105                        OrderId = link.StudioOrderId,
 5106                        Kind = SupplierStatusKind.Order,
 5107                        StatusName = string.Empty,
 5108                        Error = error
 5109                    });
 5110                    continue;
 111                }
 112
 113                // Позиция ищется по паре артикул+бренд: другого ключа ML-Auto в ответе не даёт.
 21114                var item = items.FirstOrDefault(x =>
 43115                    string.Equals(x.Pin, link.Pin, StringComparison.OrdinalIgnoreCase) &&
 43116                    string.Equals(x.Brand, link.Brand, StringComparison.OrdinalIgnoreCase));
 21117                if (item is null) continue;
 118
 20119                result.Add(new SupplierStatusSnapshot
 20120                {
 20121                    OrderId = link.StudioOrderId,
 20122                    Kind = SupplierStatusKind.Order,
 20123                    ExternalCode = string.IsNullOrWhiteSpace(item.StatusCode) ? null : item.StatusCode,
 20124                    StatusName = ResolveStatusName(item),
 20125                    ExternalOrderNumber = group.Key,
 20126                    QuantityConfirmed = item.Confirmed ? item.Quantity : null,
 20127                    QuantityShipped = item.Shipped ? item.Quantity : null,
 20128                    QuantityRejected = item.Rejected ? item.Quantity : null,
 20129                    ExpectedDeliveryAt = item.EstimatedDelivery
 20130                });
 131            }
 132        }
 133
 19134        return result;
 23135    }
 136
 137    /// <summary>
 138    /// ML-Auto отдаёт статус числовым кодом без справочника. Флаги CONFIRMED/SHIPPED/REJECTED
 139    /// приходят отдельно и читаются однозначно, поэтому имя берём из них; если флагов нет —
 140    /// заводим статус как "Код {n}", админ переименует его в справочнике.
 141    /// </summary>
 142    private static string ResolveStatusName(MlAutoOrderItemInfo item)
 143    {
 22144        if (item.Rejected) return "Отказ";
 22145        if (item.Shipped) return "Отгружен";
 26146        if (item.Confirmed) return "Подтверждён";
 2147        return string.IsNullOrWhiteSpace(item.StatusCode) ? "Без статуса" : $"Код {item.StatusCode}";
 148    }
 149}
 150
 151public sealed class MlAutoSupplierStatusProvider : MlAutoSupplierStatusProviderBase
 152{
 153    private readonly MlAutoDataManager _mlAuto;
 154
 155    public MlAutoSupplierStatusProvider(
 156        ApplicationDbContext db, MlAutoDataManager mlAuto, LocalCartStatusReader cartReader,
 157        ILogger<MlAutoSupplierStatusProvider> logger)
 158        : base(db, cartReader, logger)
 159    {
 160        _mlAuto = mlAuto;
 161    }
 162
 163    public override string ProviderKey => ApiSupplierSettings.MlAutoProviderKey;
 164
 165    protected override Task<IReadOnlyList<MlAutoOrderItemInfo>> GetOrderItemsAsync(
 166        string orderNumber, CancellationToken ct) => _mlAuto.GetOrderItemsAsync(orderNumber, ct);
 167
 168    protected override async Task<IReadOnlyList<(string OrderNumber, int StudioOrderId, string Pin, string Brand)>>
 169        LoadOrderLinksAsync(ApplicationDbContext db, List<int> orderIds, CancellationToken ct) =>
 170        (await db.MlAutoOrderItems
 171            .Where(x => orderIds.Contains(x.StudioOrderId) && x.MlAutoOrder != null)
 172            .Select(x => new { x.MlAutoOrder!.OrderNumber, x.StudioOrderId, x.Pin, x.Brand })
 173            .ToListAsync(ct))
 174        .Select(x => (x.OrderNumber, x.StudioOrderId, x.Pin, x.Brand))
 175        .ToList();
 176}
 177
 178public sealed class MlAutoRuSupplierStatusProvider : MlAutoSupplierStatusProviderBase
 179{
 180    private readonly MlAutoRuDataManager _mlAuto;
 181
 182    public MlAutoRuSupplierStatusProvider(
 183        ApplicationDbContext db, MlAutoRuDataManager mlAuto, LocalCartStatusReader cartReader,
 184        ILogger<MlAutoRuSupplierStatusProvider> logger)
 185        : base(db, cartReader, logger)
 186    {
 187        _mlAuto = mlAuto;
 188    }
 189
 190    public override string ProviderKey => ApiSupplierSettings.MlAutoRuProviderKey;
 191
 192    protected override Task<IReadOnlyList<MlAutoOrderItemInfo>> GetOrderItemsAsync(
 193        string orderNumber, CancellationToken ct) => _mlAuto.GetOrderItemsAsync(orderNumber, ct);
 194
 195    protected override async Task<IReadOnlyList<(string OrderNumber, int StudioOrderId, string Pin, string Brand)>>
 196        LoadOrderLinksAsync(ApplicationDbContext db, List<int> orderIds, CancellationToken ct) =>
 197        (await db.Set<MlAutoRuOrderItem>()
 198            .Where(x => orderIds.Contains(x.StudioOrderId) && x.MlAutoRuOrder != null)
 199            .Select(x => new { x.MlAutoRuOrder.OrderNumber, x.StudioOrderId, x.Pin, x.Brand })
 200            .ToListAsync(ct))
 201        .Select(x => (x.OrderNumber, x.StudioOrderId, x.Pin, x.Brand))
 202        .ToList();
 203}