< Summary

Information
Class: Services.SupplierStatusServices.Providers.ArmtekBySupplierStatusProvider
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/ArmtekSupplierStatusProvider.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 3
Coverable lines: 3
Total lines: 204
Line coverage: 0%
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%210%
get_ProviderKey()100%210%

File(s)

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

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Logging;
 3using OzonDomains.Models;
 4using OzonDomains.Models.SupplierStatuses;
 5using OzonRepositories.Context;
 6using Services.ApiServices.ArmtekApi;
 7using Services.ApiServices.ArmtekApi.Models.Response;
 8
 9namespace Services.SupplierStatusServices.Providers;
 10
 11/// <summary>
 12/// Статусы Armtek. Корзина локальная — читается из БД; заказ опрашивается через
 13/// ws_order/getOrder2 со STATUS=1. Базовый класс общий для RU и BY, ключ и клиент задаёт наследник.
 14/// </summary>
 15public abstract class ArmtekSupplierStatusProviderBase : ISupplierStatusProvider
 16{
 17    private readonly ApplicationDbContext _db;
 18    private readonly ArmtekDataManager _armtek;
 19    private readonly LocalCartStatusReader _cartReader;
 20    private readonly ILogger _logger;
 21
 22    protected ArmtekSupplierStatusProviderBase(
 23        ApplicationDbContext db, ArmtekDataManager armtek, LocalCartStatusReader cartReader, ILogger logger)
 24    {
 25        _db = db;
 26        _armtek = armtek;
 27        _cartReader = cartReader;
 28        _logger = logger;
 29    }
 30
 31    public abstract string ProviderKey { get; }
 32
 33    public Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(
 34        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct) =>
 35        _cartReader.ReadAsync(ProviderKey, states, ct);
 36
 37    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(
 38        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 39    {
 40        if (states.Count == 0) return [];
 41
 42        var orderIds = states.Select(x => x.OrderId).Distinct().ToList();
 43
 44        // Один заказ Studio2 может разложиться на несколько Vbeln (аллокации по складам),
 45        // поэтому берём все связки и группируем по заказу Armtek — один HTTP-вызов на Vbeln.
 46        var links = await _db.ArmtekOrderItems
 47            .Where(x => orderIds.Contains(x.StudioOrderId)
 48                        && x.ArmtekOrder != null && x.ArmtekOrder.ProviderKey == ProviderKey)
 49            .Select(x => new
 50            {
 51                x.StudioOrderId,
 52                x.Posnr,
 53                x.ArmtekOrder!.Vbeln,
 54                x.ArmtekOrder.Kunrg,
 55                x.ArmtekOrder.Vkorg
 56            })
 57            .ToListAsync(ct);
 58
 59        if (links.Count == 0) return [];
 60
 61        var result = new List<SupplierStatusSnapshot>();
 62        foreach (var group in links.GroupBy(x => new { x.Vbeln, x.Kunrg, x.Vkorg }))
 63        {
 64            ArmtekGetOrder2Response? resp;
 65            bool hasErrors;
 66            string? errorText = null;
 67            try
 68            {
 69                var response = await _armtek.GetOrder2Async(
 70                    group.Key.Kunrg, group.Key.Vbeln, group.Key.Vkorg, detailedStatus: true, ct);
 71                resp = response.Resp;
 72                hasErrors = response.HasErrors;
 73                if (hasErrors)
 74                    errorText = string.Join("; ", response.Messages.Where(m => m.Type is "E" or "A").Select(m => m.Text)
 75            }
 76            catch (Exception ex) when (ex is ArmtekApiException or HttpRequestException or TaskCanceledException)
 77            {
 78                _logger.LogInformation("{Provider}: не удалось прочитать заказ {Vbeln}: {Error}", ProviderKey, group.Key
 79                foreach (var link in group)
 80                {
 81                    result.Add(ErrorSnapshot(link.StudioOrderId, $"{ProviderKey}: {ex.Message}"));
 82                }
 83                continue;
 84            }
 85
 86            // Таймаут getOrder2?STATUS=1 отдаёт MESSAGES[E] вместе с частичным HEADER/ITEMS.
 87            // Если статуса выжать не из чего — пишем ошибку и не трогаем известный статус.
 88            if (resp is null)
 89            {
 90                foreach (var link in group)
 91                {
 92                    result.Add(ErrorSnapshot(link.StudioOrderId, errorText ?? $"{ProviderKey}: пустой ответ getOrder2"))
 93                }
 94                continue;
 95            }
 96
 97            var (statusName, expectedAt) = ResolveStatus(resp);
 98            if (string.IsNullOrWhiteSpace(statusName))
 99            {
 100                foreach (var link in group)
 101                {
 102                    result.Add(ErrorSnapshot(link.StudioOrderId, errorText ?? $"{ProviderKey}: статус не определён"));
 103                }
 104                continue;
 105            }
 106
 107            foreach (var link in group)
 108            {
 109                result.Add(new SupplierStatusSnapshot
 110                {
 111                    OrderId = link.StudioOrderId,
 112                    Kind = SupplierStatusKind.Order,
 113                    ExternalCode = statusName,
 114                    StatusName = statusName,
 115                    ExternalOrderNumber = group.Key.Vbeln,
 116                    ExternalLineId = link.Posnr,
 117                    ExpectedDeliveryAt = expectedAt
 118                });
 119            }
 120        }
 121
 122        return result;
 123    }
 124
 125    /// <summary>
 126    /// Приоритет: Delivered (отгружено) > Ready (готово к отгрузке) > Processing.SubStatus.
 127    /// Имена полей внутри STATUSES не подтверждены живым ответом API (см. комментарий
 128    /// в ArmtekOrderStatuses) — Newtonsoft при несовпадении молча оставит списки пустыми,
 129    /// поэтому при пустой детализации откатываемся на HEADER.ORDER_STATUS.
 130    /// </summary>
 131    private (string? StatusName, DateTime? ExpectedAt) ResolveStatus(ArmtekGetOrder2Response resp)
 132    {
 133        var statuses = resp.Statuses;
 134        if (statuses is not null)
 135        {
 136            if (statuses.Delivered.Count > 0)
 137                return ("Отгружен", null);
 138
 139            if (statuses.Ready.Count > 0)
 140            {
 141                var expected = statuses.Ready
 142                    .Select(x => ParseArmtekDate(x.ExpectedDate) ?? ParseArmtekDate(x.DeliveryDate))
 143                    .OfType<DateTime>()
 144                    .OrderBy(x => x)
 145                    .FirstOrDefault();
 146                return ("Готов к отгрузке", expected == default ? null : expected);
 147            }
 148
 149            if (statuses.Processing.Count > 0)
 150            {
 151                var first = statuses.Processing[0];
 152                var subStatus = string.IsNullOrWhiteSpace(first.SubStatus) ? "В обработке" : first.SubStatus;
 153                var expected = statuses.Processing
 154                    .Select(x => ParseArmtekDate(x.ExpectedDate) ?? ParseArmtekDate(x.Date))
 155                    .OfType<DateTime>()
 156                    .OrderBy(x => x)
 157                    .FirstOrDefault();
 158                return (subStatus, expected == default ? null : expected);
 159            }
 160
 161            _logger.LogDebug(
 162                "{Provider}: STATUSES пуст для заказа {Order} — используем HEADER.ORDER_STATUS",
 163                ProviderKey, resp.Header?.Order);
 164        }
 165
 166        var headerStatus = resp.Header?.OrderStatus;
 167        return (string.IsNullOrWhiteSpace(headerStatus) ? null : headerStatus, null);
 168    }
 169
 170    private static DateTime? ParseArmtekDate(string? value) =>
 171        DateTime.TryParse(value, out var parsed) ? parsed : null;
 172
 173    private SupplierStatusSnapshot ErrorSnapshot(int orderId, string error) => new()
 174    {
 175        OrderId = orderId,
 176        Kind = SupplierStatusKind.Order,
 177        StatusName = string.Empty,
 178        Error = error
 179    };
 180}
 181
 182public sealed class ArmtekSupplierStatusProvider : ArmtekSupplierStatusProviderBase
 183{
 184    public ArmtekSupplierStatusProvider(
 185        ApplicationDbContext db, ArmtekDataManager armtek, LocalCartStatusReader cartReader,
 186        ILogger<ArmtekSupplierStatusProvider> logger)
 187        : base(db, armtek, cartReader, logger)
 188    {
 189    }
 190
 191    public override string ProviderKey => ApiSupplierSettings.ArmtekProviderKey;
 192}
 193
 194public sealed class ArmtekBySupplierStatusProvider : ArmtekSupplierStatusProviderBase
 195{
 196    public ArmtekBySupplierStatusProvider(
 197        ApplicationDbContext db, ArmtekByDataManager armtek, LocalCartStatusReader cartReader,
 198        ILogger<ArmtekBySupplierStatusProvider> logger)
 0199        : base(db, armtek, cartReader, logger)
 200    {
 0201    }
 202
 0203    public override string ProviderKey => ApiSupplierSettings.ArmtekByProviderKey;
 204}