< Summary

Information
Class: Services.SupplierStatusServices.Providers.ProfitLigaSupplierStatusProvider
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/Providers/ProfitLigaSupplierStatusProvider.cs
Line coverage
100%
Covered lines: 99
Uncovered lines: 0
Coverable lines: 99
Total lines: 170
Line coverage: 100%
Branch coverage
96%
Covered branches: 58
Total branches: 60
Branch coverage: 96.6%
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(...)100%11100%
FetchOrderAsync()96.29%5454100%
SplitNumbers(...)100%22100%
ParseDate(...)100%22100%

File(s)

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

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Logging;
 3using Newtonsoft.Json.Linq;
 4using OzonDomains.Models;
 5using OzonDomains.Models.SupplierStatuses;
 6using OzonRepositories.Context;
 7using Services.ApiServices.ProfitLigaApi;
 8
 9namespace Services.SupplierStatusServices.Providers;
 10
 11/// <summary>
 12/// Статусы Профит-Лиги. Корзина локальная — читается из БД (<see cref="LocalCartStatusReader"/>);
 13/// заказы опрашиваются через GET /orders/list. Фильтр order_id принимает не более трёх номеров
 14/// за запрос, поэтому известные номера идут пачками по три.
 15/// </summary>
 16public sealed class ProfitLigaSupplierStatusProvider : ISupplierStatusProvider
 17{
 18    private readonly ApplicationDbContext _db;
 19    private readonly ProfitLigaDataManager _profitLiga;
 20    private readonly LocalCartStatusReader _localCart;
 21    private readonly ILogger<ProfitLigaSupplierStatusProvider> _logger;
 22
 3723    public ProfitLigaSupplierStatusProvider(ApplicationDbContext db, ProfitLigaDataManager profitLiga, LocalCartStatusRe
 3724        ILogger<ProfitLigaSupplierStatusProvider> logger)
 25    {
 3726        _db = db;
 3727        _profitLiga = profitLiga;
 3728        _localCart = localCart;
 3729        _logger = logger;
 3730    }
 31
 532    public string ProviderKey => ApiSupplierSettings.ProfitLigaProviderKey;
 33
 34    public Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(
 35        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct) =>
 336        _localCart.ReadAsync(ProviderKey, states, ct);
 37
 38    public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(
 39        IReadOnlyList<OrderSupplierStateRef> states, CancellationToken ct)
 40    {
 3441        if (states.Count == 0) return [];
 42
 43        // Один заказ Studio2 может ссылаться сразу на несколько номеров: /cart/order разбивает
 44        // корзину по складам и возвращает их через запятую.
 3245        var byNumber = new Dictionary<string, List<OrderSupplierStateRef>>(StringComparer.Ordinal);
 15246        foreach (var state in states)
 47        {
 17048            foreach (var number in SplitNumbers(state.ExternalOrderNumber))
 49            {
 4150                if (!byNumber.TryGetValue(number, out var list))
 3951                    byNumber[number] = list = [];
 4152                list.Add(state);
 53            }
 54        }
 55
 3656        if (byNumber.Count == 0) return [];
 57
 2858        var localOrders = await _db.ProfitLigaOrders
 2859            .Include(x => x.Items)
 2860            .Where(x => byNumber.Keys.Contains(x.OrderNumber))
 3861            .ToDictionaryAsync(x => x.OrderNumber, StringComparer.Ordinal, ct);
 62
 2663        var result = new List<SupplierStatusSnapshot>();
 2664        var localOrdersChanged = false;
 11065        foreach (var batch in byNumber.Keys.Chunk(ProfitLigaDataManager.OrdersPerRequest))
 66        {
 67            JToken response;
 68            try
 69            {
 2970                response = await _profitLiga.GetOrdersAsync(orderIds: string.Join(",", batch), ct: ct);
 2571            }
 472            catch (Exception ex) when (ex is ProfitLigaApiException or HttpRequestException or TaskCanceledException)
 73            {
 474                _logger.LogInformation("Профит-Лига: не удалось получить статусы заказов {Orders}: {Error}", string.Join
 75                // Недоступность API — не смена статуса: пишем ошибку по строке и оставляем статус прежним.
 2276                foreach (var number in batch)
 2877                    foreach (var state in byNumber[number])
 778                        result.Add(new SupplierStatusSnapshot
 779                        {
 780                            OrderId = state.OrderId,
 781                            Kind = SupplierStatusKind.Order,
 782                            StatusName = string.Empty,
 783                            ExternalOrderNumber = number,
 784                            Error = ex.Message
 785                        });
 486                continue;
 87            }
 88
 9889            foreach (var order in ProfitLigaJson.Objects((response as JObject)?["data"]))
 90            {
 2491                var number = ProfitLigaJson.String(order, "order_id");
 2492                if (number is null || !byNumber.TryGetValue(number, out var owners)) continue;
 93
 2294                var deliveryDate = ParseDate(ProfitLigaJson.String(order, "delivery_date"));
 2295                var lines = ProfitLigaJson.Objects(order["products"]).ToList();
 2296                localOrders.TryGetValue(number, out var localOrder);
 2297                if (localOrder is not null)
 98                {
 1099                    var orderStatuses = lines
 16100                        .Select(line => ProfitLigaJson.String(line, "status"))
 16101                        .Where(status => !string.IsNullOrWhiteSpace(status))
 10102                        .Distinct(StringComparer.Ordinal)
 10103                        .ToList();
 10104                    var localStatus = orderStatuses.Count switch
 10105                    {
 1106                        0 => "Создан",
 6107                        1 => orderStatuses[0] ?? "Создан",
 3108                        _ => string.Join("; ", orderStatuses)
 10109                    };
 10110                    localStatus = localStatus.Length > 50 ? localStatus[..50] : localStatus;
 10111                    if (!string.Equals(localOrder.Status, localStatus, StringComparison.Ordinal))
 112                    {
 9113                        localOrder.Status = localStatus;
 9114                        localOrdersChanged = true;
 115                    }
 116                }
 117
 100118                foreach (var line in lines)
 119                {
 28120                    var article = ProfitLigaJson.String(line, "article");
 28121                    var brand = ProfitLigaJson.String(line, "brand");
 28122                    var statusName = ProfitLigaJson.String(line, "status");
 28123                    if (string.IsNullOrWhiteSpace(statusName)) continue;
 124
 26125                    var lineId = ProfitLigaJson.String(line, "id");
 26126                    var localItem = localOrder?.Items.FirstOrDefault(item =>
 32127                        string.Equals(item.Article, article, StringComparison.OrdinalIgnoreCase) &&
 32128                        string.Equals(item.Brand, brand, StringComparison.OrdinalIgnoreCase));
 26129                    if (localItem is not null && !string.Equals(localItem.ExternalLineId, lineId, StringComparison.Ordin
 130                    {
 4131                        localItem.ExternalLineId = lineId;
 4132                        localOrdersChanged = true;
 133                    }
 134
 135                    // API не возвращает Studio2 ID. Когда состав сохранён, связываем строго по
 136                    // артикулу+бренду; для старых заказов без состава сохраняем прежний безопасный fallback.
 26137                    var matchedOwners = localItem is null
 26138                        ? []
 32139                        : owners.Where(state => state.OrderId == localItem.StudioOrderId).ToList();
 26140                    var lineOwners = matchedOwners.Count > 0 ? matchedOwners : owners;
 106141                    foreach (var state in lineOwners)
 27142                        result.Add(new SupplierStatusSnapshot
 27143                        {
 27144                            OrderId = state.OrderId,
 27145                            Kind = SupplierStatusKind.Order,
 27146                            ExternalCode = ProfitLigaJson.String(line, "status_id"),
 27147                            StatusName = statusName,
 27148                            ExternalOrderNumber = number,
 27149                            ExternalLineId = lineId,
 27150                            QuantityConfirmed = ProfitLigaJson.Decimal(line, "quantity"),
 27151                            ExpectedDeliveryAt = deliveryDate,
 27152                            StatusChangedAt = ParseDate(ProfitLigaJson.String(line, "status_update"))
 27153                        });
 154                }
 155            }
 25156        }
 157
 35158        if (localOrdersChanged) await _db.SaveChangesAsync(ct);
 26159        return result;
 31160    }
 161
 162    private static IEnumerable<string> SplitNumbers(string? value) =>
 44163        string.IsNullOrWhiteSpace(value)
 44164            ? []
 44165            : value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Distinct();
 166
 167    private static DateTime? ParseDate(string? value) =>
 49168        DateTime.TryParse(value, System.Globalization.CultureInfo.InvariantCulture,
 49169            System.Globalization.DateTimeStyles.AssumeLocal, out var parsed) ? parsed : null;
 170}