< Summary

Information
Class: Services.SupplierStatusServices.Providers.MoskvorechieIstraSupplierStatusProvider
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/MoskvorechieIstraSupplierStatusProvider.cs
Line coverage
100%
Covered lines: 85
Uncovered lines: 0
Coverable lines: 85
Total lines: 145
Line coverage: 100%
Branch coverage
95%
Covered branches: 81
Total branches: 85
Branch coverage: 95.2%
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%
FetchCartAsync()93.75%1616100%
FetchOrderAsync()100%1818100%
ErrorSnapshot(...)100%11100%
GetRows(...)50%44100%
Matches(...)75%88100%
Read(...)100%22100%
ParseDecimal(...)100%22100%
StatusName(...)100%3535100%

File(s)

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

#LineLine coverage
 1using System.Globalization;
 2using Microsoft.EntityFrameworkCore;
 3using Microsoft.Extensions.Logging;
 4using Newtonsoft.Json.Linq;
 5using OzonDomains.Models;
 6using OzonDomains.Models.SupplierStatuses;
 7using OzonRepositories.Context;
 8using Services.ApiServices.MoskvorechieApi;
 9
 10namespace Services.SupplierStatusServices.Providers;
 11
 12/// <summary>
 13/// Москворечье (Истра): корзина сверяется с <c>my_basket</c>, а созданные заказы —
 14/// с <c>make_order_v2&amp;oids=…</c>. API отдаёт только начальный этап размещения;
 15/// дальнейшее движение товара поставщик рекомендует получать отдельными методами учётной системы.
 16/// </summary>
 17public sealed class MoskvorechieIstraSupplierStatusProvider : ISupplierStatusProvider
 18{
 19    private readonly ApplicationDbContext _db;
 20    private readonly MoskvorechieIstraDataManager _api;
 21    private readonly ILogger<MoskvorechieIstraSupplierStatusProvider> _logger;
 22
 3123    public MoskvorechieIstraSupplierStatusProvider(ApplicationDbContext db, MoskvorechieIstraDataManager api,
 6224        ILogger<MoskvorechieIstraSupplierStatusProvider> logger) => (_db, _api, _logger) = (db, api, logger);
 25
 226    public string ProviderKey => ApiSupplierSettings.MoskvorechieIstraProviderKey;
 27
 28    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(
 29        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 30    {
 931        if (states.Count == 0) return [];
 32
 33        JToken basket;
 1334        try { basket = await _api.GetBasketAsync(ct); }
 135        catch (Exception ex) when (ex is MoskvorechieApiException or HttpRequestException or TaskCanceledException)
 36        {
 137            _logger.LogInformation("Москворечье: не удалось прочитать API-корзину: {Error}", ex.Message);
 338            return states.Select(x => ErrorSnapshot(x.OrderId, SupplierStatusKind.Cart, ex)).ToList();
 39        }
 40
 1241        var orderIds = states.Select(x => x.OrderId).Distinct().ToList();
 642        var items = await _db.MoskvorechieIstraCartItems
 643            .Where(x => x.ActiveStudioOrderId != null && orderIds.Contains(x.ActiveStudioOrderId.Value))
 644            .ToListAsync(ct);
 645        var rows = GetRows(basket);
 646        var result = new List<SupplierStatusSnapshot>(states.Count);
 647        var changed = false;
 2448        foreach (var state in states)
 49        {
 2050            var item = items.Where(x => x.ActiveStudioOrderId == state.OrderId).OrderByDescending(x => x.UpdatedAtUtc).F
 1151            var exists = item is not null && rows.Any(row => Matches(row, item));
 652            var code = exists ? SupplierCartStatusCode.Pending : SupplierCartStatusCode.Removed;
 653            if (!exists && item is not null)
 54            {
 355                item.Status = "Removed";
 356                item.ActiveStudioOrderId = null;
 357                item.LastError = "Позиция отсутствует в API-корзине Москворечья.";
 358                item.UpdatedAtUtc = DateTime.UtcNow;
 359                changed = true;
 60            }
 661            result.Add(new SupplierStatusSnapshot
 662            {
 663                OrderId = state.OrderId, Kind = SupplierStatusKind.Cart, ExternalCode = code,
 664                StatusName = code == SupplierCartStatusCode.Pending ? "В корзине, нужна проверка" : "Удалён из корзины"
 665            });
 66        }
 967        if (changed) await _db.SaveChangesAsync(ct);
 668        return result;
 869    }
 70
 71    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(
 72        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 73    {
 2374        if (states.Count == 0) return [];
 4375        var ids = states.Select(x => x.ExternalOrderId ?? x.ExternalOrderNumber)
 4376            .Where(x => !string.IsNullOrWhiteSpace(x)).Cast<string>().Distinct(StringComparer.Ordinal).ToList();
 2477        if (ids.Count == 0) return [];
 78
 79        JToken response;
 3580        try { response = await _api.GetOrderStatesAsync(ids, ct); }
 181        catch (Exception ex) when (ex is MoskvorechieApiException or HttpRequestException or TaskCanceledException)
 82        {
 183            _logger.LogInformation("Москворечье: не удалось получить статусы заказов: {Error}", ex.Message);
 384            return states.Select(x => ErrorSnapshot(x.OrderId, SupplierStatusKind.Order, ex)).ToList();
 85        }
 86
 3487        var rows = GetRows(response).Where(x => !string.IsNullOrWhiteSpace(Read(x, "id")))
 3488            .ToDictionary(x => Read(x, "id")!, StringComparer.Ordinal);
 1789        var localOrders = await _db.MoskvorechieIstraOrders
 1790            .Where(x => x.ExternalOrderNumber != null && ids.Contains(x.ExternalOrderNumber))
 1791            .ToListAsync(ct);
 1792        var result = new List<SupplierStatusSnapshot>(states.Count);
 6893        foreach (var state in states)
 94        {
 1795            var id = state.ExternalOrderId ?? state.ExternalOrderNumber;
 1796            if (string.IsNullOrWhiteSpace(id) || !rows.TryGetValue(id, out var row)) continue;
 1697            var code = Read(row, "status_id");
 1698            var name = Read(row, "status_txt") ?? StatusName(code);
 1899            var local = localOrders.FirstOrDefault(x => x.ExternalOrderNumber == id);
 16100            if (local is not null)
 101            {
 2102                local.Status = name.Length > 30 ? name[..30] : name;
 2103                local.ResponseJson = row.ToString(Newtonsoft.Json.Formatting.None);
 104            }
 16105            result.Add(new SupplierStatusSnapshot
 16106            {
 16107                OrderId = state.OrderId, Kind = SupplierStatusKind.Order, ExternalCode = code, StatusName = name,
 16108                ExternalOrderNumber = id, ExternalOrderId = id
 16109            });
 110        }
 17111        await _db.SaveChangesAsync(ct);
 17112        return result;
 22113    }
 114
 4115    private static SupplierStatusSnapshot ErrorSnapshot(int orderId, string kind, Exception ex) => new()
 4116    {
 4117        OrderId = orderId, Kind = kind, StatusName = string.Empty, Error = $"Москворечье: {ex.Message}"
 4118    };
 119
 120    private static IReadOnlyList<JObject> GetRows(JToken response) =>
 23121        (response["result"] ?? response) is JArray array ? array.OfType<JObject>().ToList() : [];
 122
 123    private static bool Matches(JObject row, MoskvorechieIstraCartItem item)
 124    {
 5125        if (!string.Equals(Read(row, "nr"), item.Article, StringComparison.OrdinalIgnoreCase) ||
 5126            !string.Equals(Read(row, "brand"), item.Brand, StringComparison.OrdinalIgnoreCase)) return false;
 5127        var quantity = ParseDecimal(Read(row, "quantity"));
 5128        var price = ParseDecimal(Read(row, "price"));
 5129        return quantity >= item.Quantity && (!item.SourcePrice.HasValue || Math.Abs(price - item.SourcePrice.Value) < 0.
 130    }
 131
 86132    private static string? Read(JObject row, string name) => row.Properties()
 247133        .FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))?.Value.ToString();
 134
 10135    private static decimal ParseDecimal(string? value) => decimal.TryParse(value, NumberStyles.Number,
 10136        CultureInfo.InvariantCulture, out var parsed) ? parsed : 0m;
 137
 13138    private static string StatusName(string? code) => code switch
 13139    {
 3140        "-1" => "Подключение к учётной системе", "-2" => "Создание заказа", "-3" => "Резервирование товара",
 3141        "-4" => "Создание расходной накладной", "-9" => "Передача менеджеру", "0" => "Заказ не найден",
 3142        "1" => "Размещён в учётной системе", "2" => "Создана расходная накладная", "3" => "Создана заявка",
 4143        "9" => "Ручная обработка менеджером", _ => string.IsNullOrWhiteSpace(code) ? "Ожидает статуса" : $"Статус API {c
 13144    };
 145}