< Summary

Information
Class: Services.SupplierStatusServices.OrderSupplierStatusSyncService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/OrderSupplierStatusSyncService.cs
Line coverage
100%
Covered lines: 62
Uncovered lines: 0
Coverable lines: 62
Total lines: 132
Line coverage: 100%
Branch coverage
90%
Covered branches: 18
Total branches: 20
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SyncAsync(...)100%11100%
SyncOrdersAsync(...)100%22100%
SyncCoreAsync()90%2020100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/OrderSupplierStatusSyncService.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Logging;
 3using Services.SystemMonitoringServices;
 4using OzonDomains.Models.SupplierStatuses;
 5using OzonRepositories.Context;
 6
 7namespace Services.SupplierStatusServices;
 8
 9public interface IOrderSupplierStatusSyncService
 10{
 11    /// <summary>Опрашивает поставщиков по активным строкам состояния и применяет результат.</summary>
 12    Task<int> SyncAsync(CancellationToken ct = default);
 13
 14    /// <summary>Синхронизация по конкретным заказам — для кнопки «обновить» в интерфейсе.</summary>
 15    Task<int> SyncOrdersAsync(IReadOnlyCollection<int> orderIds, CancellationToken ct = default);
 16}
 17
 18/// <summary>
 19/// Разбирает очередь активных состояний и раздаёт её провайдерам. Стадии обрабатываются
 20/// раздельно: Cart идёт в FetchCartAsync, Ordered — в FetchOrderAsync.
 21/// </summary>
 22public sealed class OrderSupplierStatusSyncService : IOrderSupplierStatusSyncService
 23{
 24    /// <summary>Верхняя граница батча: очередь дренируется за несколько прогонов, БД не залипает.</summary>
 25    private const int BatchSize = 500;
 26
 27    /// <summary>Строку, опрошенную недавно, пропускаем — иначе один поставщик съест весь батч.</summary>
 28    private const int MinResyncMinutes = 15;
 29
 30    /// <summary>За пределами окна статусы у поставщиков уже не меняются.</summary>
 31    private const int LookbackDays = 60;
 32
 33    private readonly ApplicationDbContext _db;
 34    private readonly IOrderSupplierStateService _stateService;
 35    private readonly IReadOnlyDictionary<string, ISupplierStatusProvider> _providers;
 36    private readonly ILogger<OrderSupplierStatusSyncService> _logger;
 37    private readonly ISystemMonitoringSignalWriter? _signals;
 38
 2439    public OrderSupplierStatusSyncService(
 2440        ApplicationDbContext db,
 2441        IOrderSupplierStateService stateService,
 2442        IEnumerable<ISupplierStatusProvider> providers,
 2443        ILogger<OrderSupplierStatusSyncService> logger,
 2444        ISystemMonitoringSignalWriter? signals = null)
 45    {
 2446        _db = db;
 2447        _stateService = stateService;
 4948        _providers = providers.ToDictionary(x => x.ProviderKey, StringComparer.Ordinal);
 2449        _logger = logger;
 2450        _signals = signals;
 2451    }
 52
 2053    public Task<int> SyncAsync(CancellationToken ct = default) => SyncCoreAsync(null, ct);
 54
 55    public Task<int> SyncOrdersAsync(IReadOnlyCollection<int> orderIds, CancellationToken ct = default) =>
 456        orderIds.Count == 0 ? Task.FromResult(0) : SyncCoreAsync(orderIds.ToList(), ct);
 57
 58    private async Task<int> SyncCoreAsync(List<int>? orderIds, CancellationToken ct)
 59    {
 2360        var query = _db.OrderSupplierStates
 2361            .AsNoTracking()
 2362            .Where(x => x.ActiveOrderId != null
 2363                        && (x.Stage == OrderSupplierStage.Cart || x.Stage == OrderSupplierStage.Ordered));
 64
 2365        if (orderIds is not null)
 66        {
 367            query = query.Where(x => orderIds.Contains(x.OrderId));
 68        }
 69        else
 70        {
 71            // Ручной запрос обновляет что просили; фоновой прогон дренирует очередь по свежести.
 2072            var resyncCutoff = DateTime.UtcNow.AddMinutes(-MinResyncMinutes);
 2073            var createdCutoff = DateTime.UtcNow.AddDays(-LookbackDays);
 2074            query = query
 2075                .Where(x => x.CreatedAt >= createdCutoff)
 2076                .Where(x => x.LastSyncedAt == null || x.LastSyncedAt < resyncCutoff)
 2077                .OrderBy(x => x.LastSyncedAt ?? DateTime.MinValue)
 2078                .Take(BatchSize);
 79        }
 80
 2381        var states = await query
 2382            .Select(x => new OrderSupplierStateRef(
 2383                x.Id, x.OrderId, x.ProviderKey, x.Stage, x.CartItemId, x.ExternalBasketItemId,
 2384                x.ExternalOrderNumber, x.ExternalOrderId, x.ExternalLineId, x.SupplierStatusId))
 2385            .ToListAsync(ct);
 86
 2887        if (states.Count == 0) return 0;
 88
 1689        var changed = 0;
 59490        foreach (var group in states.GroupBy(x => new { x.ProviderKey, x.Stage }))
 91        {
 1992            if (!_providers.TryGetValue(group.Key.ProviderKey, out var provider))
 93            {
 294                _logger.LogWarning(
 295                    "Нет провайдера статусов для '{ProviderKey}' — {Count} строк пропущено",
 296                    group.Key.ProviderKey, group.Count());
 597                var stateIds = group.Select(x => x.StateId).ToList();
 298                await _stateService.MarkSyncedAsync(stateIds, $"Нет провайдера статусов для '{group.Key.ProviderKey}'", 
 299                continue;
 100            }
 101
 17102            var batch = group.ToList();
 103            try
 104            {
 105                // Ошибка одного поставщика не должна ронять синхронизацию остальных.
 17106                var snapshots = group.Key.Stage == OrderSupplierStage.Cart
 17107                    ? await provider.FetchCartAsync(batch, ct)
 17108                    : await provider.FetchOrderAsync(batch, ct);
 109
 16110                changed += await _stateService.ApplySnapshotsAsync(group.Key.ProviderKey, snapshots, ct);
 16111                await (_signals?.RecordAsync("supplier-status", "success", group.Key.ProviderKey, cancellationToken: ct)
 112
 113                // Строки, по которым поставщик ничего не сказал, тоже отмечаем опрошенными,
 114                // иначе они займут весь следующий батч и заблокируют очередь.
 535115                var answered = snapshots.Select(x => x.OrderId).ToHashSet();
 537116                var silent = batch.Where(x => !answered.Contains(x.OrderId)).Select(x => x.StateId).ToList();
 16117                await _stateService.MarkSyncedAsync(silent, null, ct);
 16118            }
 1119            catch (Exception ex)
 120            {
 1121                _logger.LogError(ex,
 1122                    "Синхронизация статусов {ProviderKey} ({Stage}) не удалась",
 1123                    group.Key.ProviderKey, group.Key.Stage);
 1124                await _stateService.MarkSyncedAsync(
 2125                    batch.Select(x => x.StateId).ToList(), ex.Message, ct);
 1126                await (_signals?.RecordAsync("supplier-status", "failure", group.Key.ProviderKey, "Ошибка синхронизации 
 127            }
 17128        }
 129
 16130        return changed;
 22131    }
 132}