< Summary

Information
Class: Services.SupplierStatusServices.Providers.MikadoSupplierStatusProvider
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/MikadoSupplierStatusProvider.cs
Line coverage
85%
Covered lines: 95
Uncovered lines: 16
Coverable lines: 111
Total lines: 205
Line coverage: 85.5%
Branch coverage
66%
Covered branches: 37
Total branches: 56
Branch coverage: 66%
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%210%
FetchCartAsync()87.5%8885.29%
FetchOrderAsync()75%5455.55%
FromHistory(...)60%1010100%
CloseRow(...)100%11100%
IsClosed(...)100%22100%
RemovedSnapshot(...)100%11100%
ReadBasketItemId(...)50%44100%
ParseBasket(...)50%22100%
ParseHistory(...)100%11100%
Failures(...)100%11100%
Failure(...)100%11100%
IsApiFailure(...)87.5%88100%
CartStatusName(...)25%5466.66%
ReadString(...)50%66100%
ReadDecimal(...)50%22100%
ReadBool(...)100%11100%
ParseDate(...)66.66%66100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using System.Xml.Linq;
 3using Microsoft.EntityFrameworkCore;
 4using OzonDomains.Models;
 5using OzonDomains.Models.SupplierStatuses;
 6using OzonRepositories.Context;
 7using Services.ApiServices.MikadoApi;
 8
 9namespace Services.SupplierStatusServices.Providers;
 10
 11/// <summary>
 12/// Сверяет корзину Микадо и судьбу её позиций. У Микадо нет отдельной сущности заказа:
 13/// позиция живёт по своему BasketID, поэтому Basket_List показывает, что ещё в буфере,
 14/// а Zakaz_History — ушло ли оно в заказ (InZakaz) или было удалено.
 15/// </summary>
 16public sealed class MikadoSupplierStatusProvider : ISupplierStatusProvider
 17{
 18    private readonly ApplicationDbContext _db;
 19    private readonly MikadoDataManager _api;
 1020    public MikadoSupplierStatusProvider(ApplicationDbContext db, MikadoDataManager api) => (_db, _api) = (db, api);
 021    public string ProviderKey => ApiSupplierSettings.MikadoMskHod20ProviderKey;
 22
 23    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(IReadOnlyList<OrderSupplierStateRef> states,
 24    {
 25        Dictionary<long, XElement> basket;
 26        try
 27        {
 428            basket = ParseBasket(await _api.GetBasketAsync(ct));
 329        }
 130        catch (Exception ex) when (IsApiFailure(ex))
 31        {
 132            return Failures(states, SupplierStatusKind.Cart, ex);
 33        }
 34
 35        var orderIds = states.Select(x => x.OrderId).ToList();
 336        var rows = await _db.MikadoCartItems
 337            .Where(x => orderIds.Contains(x.StudioOrderId) && x.ActiveStudioOrderId != null)
 338            .ToListAsync(ct);
 39
 340        var result = new List<SupplierStatusSnapshot>();
 1241        foreach (var state in states)
 42        {
 643            var row = rows.FirstOrDefault(x => x.StudioOrderId == state.OrderId);
 344            if (row is null)
 45            {
 046                result.Add(RemovedSnapshot(state.OrderId));
 047                continue;
 48            }
 49
 350            if (basket.TryGetValue(row.ApiBasketItemId, out var item))
 51            {
 152                result.Add(new SupplierStatusSnapshot
 153                {
 154                    OrderId = state.OrderId,
 155                    Kind = SupplierStatusKind.Cart,
 156                    ExternalCode = row.Status,
 157                    StatusName = CartStatusName(row.Status),
 158                    ExpectedDeliveryAt = ParseDate(ReadString(item, "Srok"))
 159                });
 160                continue;
 61            }
 62
 63            // Позиции нет в буфере: либо её перевели в заказ, либо сняли.
 64            try
 65            {
 266                var history = ParseHistory(await _api.GetOrderHistoryAsync(row.ApiBasketItemId, ct));
 267                var snapshot = FromHistory(state.OrderId, row.ApiBasketItemId, history);
 368                if (IsClosed(snapshot)) CloseRow(row);
 269                result.Add(snapshot);
 270            }
 071            catch (Exception ex) when (IsApiFailure(ex))
 72            {
 073                result.Add(Failure(state.OrderId, SupplierStatusKind.Cart, ex));
 074            }
 275        }
 76
 377        await _db.SaveChangesAsync(ct);
 378        return result;
 479    }
 80
 81    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(IReadOnlyList<OrderSupplierStateRef> states
 82    {
 183        var result = new List<SupplierStatusSnapshot>();
 484        foreach (var state in states)
 85        {
 186            var basketItemId = ReadBasketItemId(state);
 187            if (basketItemId <= 0)
 88            {
 089                result.Add(new SupplierStatusSnapshot
 090                {
 091                    OrderId = state.OrderId, Kind = SupplierStatusKind.Order, StatusName = "Ожидает статуса"
 092                });
 093                continue;
 94            }
 95
 96            try
 97            {
 198                var history = ParseHistory(await _api.GetOrderHistoryAsync(basketItemId, ct));
 199                result.Add(FromHistory(state.OrderId, basketItemId, history));
 1100            }
 0101            catch (Exception ex) when (IsApiFailure(ex))
 102            {
 0103                result.Add(Failure(state.OrderId, SupplierStatusKind.Order, ex));
 0104            }
 1105        }
 106
 1107        return result;
 1108    }
 109
 110    /// <summary>Раскладывает Zakaz_History в снимок: InZakaz — заказ, остальное закрывает отслеживание.</summary>
 111    private static SupplierStatusSnapshot FromHistory(int orderId, long basketItemId, XElement? history)
 112    {
 3113        var status = ReadString(history, "ZakazStatus");
 3114        if (!string.Equals(status, "InZakaz", StringComparison.OrdinalIgnoreCase))
 115        {
 116            // InBuffer здесь означает, что позиции уже нет в Basket_List — доверяем корзине и закрываем.
 1117            return RemovedSnapshot(orderId);
 118        }
 119
 32120        var lines = history?.Descendants().Where(x => x.Name.LocalName == "ZakazRow").ToList() ?? [];
 8121        var shipped = lines.Where(x => !ReadBool(x, "Otkaz")).Sum(x => ReadDecimal(x, "QTY"));
 8122        var rejected = lines.Where(x => ReadBool(x, "Otkaz")).Sum(x => ReadDecimal(x, "QTY"));
 10123        var expected = lines.Select(x => ParseDate(ReadString(x, "dDate"))).Where(x => x.HasValue).Max();
 124
 2125        return new SupplierStatusSnapshot
 2126        {
 2127            OrderId = orderId,
 2128            Kind = SupplierStatusKind.Order,
 2129            ExternalCode = "InZakaz",
 2130            StatusName = "В заказе у Микадо",
 2131            ExternalOrderId = basketItemId.ToString(CultureInfo.InvariantCulture),
 2132            ExternalOrderNumber = ReadString(history, "ZakazCode"),
 2133            QuantityConfirmed = ReadDecimal(history, "ZakazQty"),
 2134            QuantityShipped = shipped > 0 ? shipped : null,
 2135            QuantityRejected = rejected > 0 ? rejected : null,
 2136            ExpectedDeliveryAt = expected
 2137        };
 138    }
 139
 140    private static void CloseRow(MikadoCartItem row)
 141    {
 1142        row.Status = MikadoCartItemStatus.Removed;
 1143        row.ActiveStudioOrderId = null;
 1144        row.UpdatedAtUtc = DateTime.UtcNow;
 1145    }
 146
 147    private static bool IsClosed(SupplierStatusSnapshot snapshot) =>
 2148        snapshot.Kind == SupplierStatusKind.Cart && snapshot.ExternalCode == SupplierCartStatusCode.Removed;
 149
 1150    private static SupplierStatusSnapshot RemovedSnapshot(int orderId) => new()
 1151    {
 1152        OrderId = orderId, Kind = SupplierStatusKind.Cart,
 1153        ExternalCode = SupplierCartStatusCode.Removed, StatusName = "Удалён из корзины"
 1154    };
 155
 156    private static long ReadBasketItemId(OrderSupplierStateRef state) =>
 1157        long.TryParse(state.ExternalBasketItemId ?? state.ExternalOrderId, NumberStyles.Integer,
 1158            CultureInfo.InvariantCulture, out var id) ? id : 0;
 159
 160    private static Dictionary<long, XElement> ParseBasket(string xml) =>
 26161        XDocument.Parse(xml).Descendants().Where(x => x.Name.LocalName == "BasketItem")
 1162            .GroupBy(x => long.TryParse(ReadString(x, "ID"), out var id) ? id : 0)
 1163            .Where(x => x.Key > 0)
 6164            .ToDictionary(x => x.Key, x => x.First());
 165
 166    private static XElement? ParseHistory(string xml) =>
 15167        XDocument.Parse(xml).Descendants().FirstOrDefault(x => x.Name.LocalName == "Zakaz_HistoryResult");
 168
 169    private static IReadOnlyList<SupplierStatusSnapshot> Failures(
 170        IReadOnlyList<OrderSupplierStateRef> states, string kind, Exception ex) =>
 2171        states.Select(x => Failure(x.OrderId, kind, ex)).ToList();
 172
 1173    private static SupplierStatusSnapshot Failure(int orderId, string kind, Exception ex) => new()
 1174    {
 1175        OrderId = orderId, Kind = kind, StatusName = string.Empty, Error = $"Микадо: {ex.Message}"
 1176    };
 177
 178    private static bool IsApiFailure(Exception ex) =>
 1179        ex is MikadoApiException or HttpRequestException or TaskCanceledException or System.Xml.XmlException;
 180
 1181    private static string CartStatusName(string status) => status switch
 1182    {
 1183        MikadoCartItemStatus.Ready => "Проверен, готов к заказу",
 0184        MikadoCartItemStatus.Failed => "Ошибка проверки",
 0185        _ => "В корзине, нужна проверка"
 1186    };
 187
 188    private static string? ReadString(XElement? x, string name) =>
 90189        x?.Descendants().FirstOrDefault(e => e.Name.LocalName == name)?.Value?.Trim();
 190
 191    private static decimal ReadDecimal(XElement? x, string name) =>
 6192        decimal.TryParse(ReadString(x, name), NumberStyles.Number, CultureInfo.InvariantCulture, out var value) ? value 
 193
 194    private static bool ReadBool(XElement? x, string name) =>
 8195        bool.TryParse(ReadString(x, name), out var value) && value;
 196
 197    /// <summary>Микадо отдаёт даты как dd.MM.yyyy (Srok) и ISO (dDate).</summary>
 198    private static DateTime? ParseDate(string? value)
 199    {
 5200        if (string.IsNullOrWhiteSpace(value)) return null;
 5201        if (DateTime.TryParseExact(value, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact
 1202            return exact;
 4203        return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) ? parsed : nu
 204    }
 205}