< Summary

Information
Class: Services.SupplierStatusServices.Providers.MlAutoSupplierStatusProvider
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/MlAutoSupplierStatusProvider.cs
Line coverage
100%
Covered lines: 11
Uncovered lines: 0
Coverable lines: 11
Total lines: 203
Line coverage: 100%
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
.ctor(...)100%11100%
get_ProviderKey()100%11100%
GetOrderItemsAsync(...)100%11100%
LoadOrderLinksAsync()100%11100%

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
 23    protected MlAutoSupplierStatusProviderBase(
 24        ApplicationDbContext db, LocalCartStatusReader cartReader, ILogger logger)
 25    {
 26        _db = db;
 27        _cartReader = cartReader;
 28        _logger = logger;
 29    }
 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) =>
 46        _cartReader.ReadAsync(ProviderKey, states, ct);
 47
 48    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(
 49        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 50    {
 51        if (states.Count == 0) return [];
 52
 53        var orderIds = states.Select(x => x.OrderId).Distinct().ToList();
 54        var links = await LoadOrderLinksAsync(_db, orderIds, ct);
 55        if (links.Count == 0) return [];
 56
 57        var byOrderNumber = links
 58            .GroupBy(x => x.OrderNumber, StringComparer.Ordinal)
 59            .ToList();
 60
 61        var result = new List<SupplierStatusSnapshot>();
 62        var gate = new SemaphoreSlim(DetailsConcurrency);
 63        // Лимит запросов в минуту временный: после первого отказа остальные заказы не опрашиваем, а не бьёмся в тот же 
 64        var rateLimited = false;
 65        // Порядок случайный: при упоре в лимит пропущенные заказы помечаются опрошенными вместе с остальными,
 66        // и фиксированный порядок оставил бы одни и те же заказы без обновления навсегда.
 67        var tasks = byOrderNumber.OrderBy(_ => Random.Shared.Next()).Select(async group =>
 68        {
 69            await gate.WaitAsync(ct);
 70            try
 71            {
 72                if (Volatile.Read(ref rateLimited))
 73                    return (Group: group, Items: (IReadOnlyList<MlAutoOrderItemInfo>)[], Error: (string?)null);
 74                return (Group: group, Items: await GetOrderItemsAsync(group.Key, ct), Error: (string?)null);
 75            }
 76            catch (MlAutoApiException ex) when (ex.IsRateLimited)
 77            {
 78                Volatile.Write(ref rateLimited, true);
 79                _logger.LogWarning(
 80                    "{Provider}: лимит запросов исчерпан на заказе {Number}, остальные заказы пропущены до следующего пр
 81                    ProviderKey, group.Key);
 82                return (Group: group, Items: (IReadOnlyList<MlAutoOrderItemInfo>)[], Error: (string?)null);
 83            }
 84            catch (Exception ex) when (ex is MlAutoApiException or HttpRequestException or TaskCanceledException)
 85            {
 86                _logger.LogInformation(
 87                    "{Provider}: не удалось прочитать заказ {Number}: {Error}",
 88                    ProviderKey, group.Key, ex.Message);
 89                return (Group: group, Items: (IReadOnlyList<MlAutoOrderItemInfo>)[], Error: $"{ProviderKey}: {ex.Message
 90            }
 91            finally
 92            {
 93                gate.Release();
 94            }
 95        });
 96
 97        foreach (var (group, items, error) in await Task.WhenAll(tasks))
 98        {
 99            foreach (var link in group)
 100            {
 101                if (error is not null)
 102                {
 103                    result.Add(new SupplierStatusSnapshot
 104                    {
 105                        OrderId = link.StudioOrderId,
 106                        Kind = SupplierStatusKind.Order,
 107                        StatusName = string.Empty,
 108                        Error = error
 109                    });
 110                    continue;
 111                }
 112
 113                // Позиция ищется по паре артикул+бренд: другого ключа ML-Auto в ответе не даёт.
 114                var item = items.FirstOrDefault(x =>
 115                    string.Equals(x.Pin, link.Pin, StringComparison.OrdinalIgnoreCase) &&
 116                    string.Equals(x.Brand, link.Brand, StringComparison.OrdinalIgnoreCase));
 117                if (item is null) continue;
 118
 119                result.Add(new SupplierStatusSnapshot
 120                {
 121                    OrderId = link.StudioOrderId,
 122                    Kind = SupplierStatusKind.Order,
 123                    ExternalCode = string.IsNullOrWhiteSpace(item.StatusCode) ? null : item.StatusCode,
 124                    StatusName = ResolveStatusName(item),
 125                    ExternalOrderNumber = group.Key,
 126                    QuantityConfirmed = item.Confirmed ? item.Quantity : null,
 127                    QuantityShipped = item.Shipped ? item.Quantity : null,
 128                    QuantityRejected = item.Rejected ? item.Quantity : null,
 129                    ExpectedDeliveryAt = item.EstimatedDelivery
 130                });
 131            }
 132        }
 133
 134        return result;
 135    }
 136
 137    /// <summary>
 138    /// ML-Auto отдаёт статус числовым кодом без справочника. Флаги CONFIRMED/SHIPPED/REJECTED
 139    /// приходят отдельно и читаются однозначно, поэтому имя берём из них; если флагов нет —
 140    /// заводим статус как "Код {n}", админ переименует его в справочнике.
 141    /// </summary>
 142    private static string ResolveStatusName(MlAutoOrderItemInfo item)
 143    {
 144        if (item.Rejected) return "Отказ";
 145        if (item.Shipped) return "Отгружен";
 146        if (item.Confirmed) return "Подтверждён";
 147        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)
 25158        : base(db, cartReader, logger)
 159    {
 25160        _mlAuto = mlAuto;
 25161    }
 162
 14163    public override string ProviderKey => ApiSupplierSettings.MlAutoProviderKey;
 164
 165    protected override Task<IReadOnlyList<MlAutoOrderItemInfo>> GetOrderItemsAsync(
 23166        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) =>
 19170        (await db.MlAutoOrderItems
 19171            .Where(x => orderIds.Contains(x.StudioOrderId) && x.MlAutoOrder != null)
 19172            .Select(x => new { x.MlAutoOrder!.OrderNumber, x.StudioOrderId, x.Pin, x.Brand })
 19173            .ToListAsync(ct))
 24174        .Select(x => (x.OrderNumber, x.StudioOrderId, x.Pin, x.Brand))
 19175        .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}