< Summary

Information
Class: Services.SupplierStatusServices.Providers.ArmtekSupplierStatusProviderBase
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/ArmtekSupplierStatusProvider.cs
Line coverage
43%
Covered lines: 39
Uncovered lines: 50
Coverable lines: 89
Total lines: 204
Line coverage: 43.8%
Branch coverage
8%
Covered branches: 4
Total branches: 48
Branch coverage: 8.3%
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%210%
FetchOrderAsync()25%872452.08%
ResolveStatus(...)0%600240%
ParseArmtekDate(...)0%620%
ErrorSnapshot(...)100%11100%

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
 122    protected ArmtekSupplierStatusProviderBase(
 123        ApplicationDbContext db, ArmtekDataManager armtek, LocalCartStatusReader cartReader, ILogger logger)
 24    {
 125        _db = db;
 126        _armtek = armtek;
 127        _cartReader = cartReader;
 128        _logger = logger;
 129    }
 30
 31    public abstract string ProviderKey { get; }
 32
 33    public Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(
 34        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct) =>
 035        _cartReader.ReadAsync(ProviderKey, states, ct);
 36
 37    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(
 38        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 39    {
 140        if (states.Count == 0) return [];
 41
 42        var orderIds = states.Select(x => x.OrderId).Distinct().ToList();
 43
 44        // Один заказ Studio2 может разложиться на несколько Vbeln (аллокации по складам),
 45        // поэтому берём все связки и группируем по заказу Armtek — один HTTP-вызов на Vbeln.
 146        var links = await _db.ArmtekOrderItems
 147            .Where(x => orderIds.Contains(x.StudioOrderId)
 148                        && x.ArmtekOrder != null && x.ArmtekOrder.ProviderKey == ProviderKey)
 149            .Select(x => new
 150            {
 151                x.StudioOrderId,
 152                x.Posnr,
 153                x.ArmtekOrder!.Vbeln,
 154                x.ArmtekOrder.Kunrg,
 155                x.ArmtekOrder.Vkorg
 156            })
 157            .ToListAsync(ct);
 58
 159        if (links.Count == 0) return [];
 60
 161        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;
 166            string? errorText = null;
 67            try
 68            {
 169                var response = await _armtek.GetOrder2Async(
 170                    group.Key.Kunrg, group.Key.Vbeln, group.Key.Vkorg, detailedStatus: true, ct);
 071                resp = response.Resp;
 072                hasErrors = response.HasErrors;
 073                if (hasErrors)
 74                    errorText = string.Join("; ", response.Messages.Where(m => m.Type is "E" or "A").Select(m => m.Text)
 075            }
 176            catch (Exception ex) when (ex is ArmtekApiException or HttpRequestException or TaskCanceledException)
 77            {
 178                _logger.LogInformation("{Provider}: не удалось прочитать заказ {Vbeln}: {Error}", ProviderKey, group.Key
 479                foreach (var link in group)
 80                {
 181                    result.Add(ErrorSnapshot(link.StudioOrderId, $"{ProviderKey}: {ex.Message}"));
 82                }
 183                continue;
 84            }
 85
 86            // Таймаут getOrder2?STATUS=1 отдаёт MESSAGES[E] вместе с частичным HEADER/ITEMS.
 87            // Если статуса выжать не из чего — пишем ошибку и не трогаем известный статус.
 088            if (resp is null)
 89            {
 090                foreach (var link in group)
 91                {
 092                    result.Add(ErrorSnapshot(link.StudioOrderId, errorText ?? $"{ProviderKey}: пустой ответ getOrder2"))
 93                }
 94                continue;
 95            }
 96
 097            var (statusName, expectedAt) = ResolveStatus(resp);
 098            if (string.IsNullOrWhiteSpace(statusName))
 99            {
 0100                foreach (var link in group)
 101                {
 0102                    result.Add(ErrorSnapshot(link.StudioOrderId, errorText ?? $"{ProviderKey}: статус не определён"));
 103                }
 104                continue;
 105            }
 106
 0107            foreach (var link in group)
 108            {
 0109                result.Add(new SupplierStatusSnapshot
 0110                {
 0111                    OrderId = link.StudioOrderId,
 0112                    Kind = SupplierStatusKind.Order,
 0113                    ExternalCode = statusName,
 0114                    StatusName = statusName,
 0115                    ExternalOrderNumber = group.Key.Vbeln,
 0116                    ExternalLineId = link.Posnr,
 0117                    ExpectedDeliveryAt = expectedAt
 0118                });
 119            }
 0120        }
 121
 1122        return result;
 1123    }
 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    {
 0133        var statuses = resp.Statuses;
 0134        if (statuses is not null)
 135        {
 0136            if (statuses.Delivered.Count > 0)
 0137                return ("Отгружен", null);
 138
 0139            if (statuses.Ready.Count > 0)
 140            {
 0141                var expected = statuses.Ready
 0142                    .Select(x => ParseArmtekDate(x.ExpectedDate) ?? ParseArmtekDate(x.DeliveryDate))
 0143                    .OfType<DateTime>()
 0144                    .OrderBy(x => x)
 0145                    .FirstOrDefault();
 0146                return ("Готов к отгрузке", expected == default ? null : expected);
 147            }
 148
 0149            if (statuses.Processing.Count > 0)
 150            {
 0151                var first = statuses.Processing[0];
 0152                var subStatus = string.IsNullOrWhiteSpace(first.SubStatus) ? "В обработке" : first.SubStatus;
 0153                var expected = statuses.Processing
 0154                    .Select(x => ParseArmtekDate(x.ExpectedDate) ?? ParseArmtekDate(x.Date))
 0155                    .OfType<DateTime>()
 0156                    .OrderBy(x => x)
 0157                    .FirstOrDefault();
 0158                return (subStatus, expected == default ? null : expected);
 159            }
 160
 0161            _logger.LogDebug(
 0162                "{Provider}: STATUSES пуст для заказа {Order} — используем HEADER.ORDER_STATUS",
 0163                ProviderKey, resp.Header?.Order);
 164        }
 165
 0166        var headerStatus = resp.Header?.OrderStatus;
 0167        return (string.IsNullOrWhiteSpace(headerStatus) ? null : headerStatus, null);
 168    }
 169
 170    private static DateTime? ParseArmtekDate(string? value) =>
 0171        DateTime.TryParse(value, out var parsed) ? parsed : null;
 172
 1173    private SupplierStatusSnapshot ErrorSnapshot(int orderId, string error) => new()
 1174    {
 1175        OrderId = orderId,
 1176        Kind = SupplierStatusKind.Order,
 1177        StatusName = string.Empty,
 1178        Error = error
 1179    };
 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)
 199        : base(db, armtek, cartReader, logger)
 200    {
 201    }
 202
 203    public override string ProviderKey => ApiSupplierSettings.ArmtekByProviderKey;
 204}