< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.OrdersClientStatusReconcileJob
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersClientStatusReconcileJob.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 39
Coverable lines: 39
Total lines: 87
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 8
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%210%
ExecuteAsync()0%7280%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersClientStatusReconcileJob.cs

#LineLine coverage
 1using Hangfire;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains;
 4using OzonRepositories.Context;
 5using Services.HangfireService;
 6
 7namespace OzonOrdersWeb.WebServices.Orders;
 8
 9/// <summary>
 10/// Страховка от потерянных пуш-уведомлений Ozon: по кругу опрашивает статусы незавершённых заказов
 11/// пачками и записывает изменения. Без неё статус в гриде менялся только при ручном «Обновить»
 12/// или пуше (с 28.08.2026 Ozon отключает адреса, отвечающие дольше 2,5 с).
 13/// </summary>
 14public sealed class OrdersClientStatusReconcileJob
 15{
 16    public const string JobId = "orders-client-status-reconcile";
 17    public const string DefaultCron = "*/10 * * * *";
 18
 19    /// <summary>Сколько заказов опрашиваем за один запуск (ограничивает нагрузку на API Ozon и БД).</summary>
 20    public const int BatchSize = 150;
 21
 22    /// <summary>Заказы старше окна не опрашиваем — по ним статус уже не меняется.</summary>
 023    public static readonly TimeSpan Window = TimeSpan.FromDays(45);
 24
 25    /// <summary>Статусы Ozon, после которых опрос бессмысленен.</summary>
 026    public static readonly string[] FinalStatusNames =
 027    [
 028        "Доставлен", "Доставлено", "Получено", "Отменён", "Отменён из-за разделения отправления"
 029    ];
 30
 31    private static int _cursor;
 32
 33    private readonly ApplicationDbContext _context;
 34    private readonly IOrdersClientStatusSyncService _statusSync;
 35    private readonly ILogger<OrdersClientStatusReconcileJob> _logger;
 36    private readonly OrdersStatusSyncHealthStore _health;
 37
 038    public OrdersClientStatusReconcileJob(
 039        ApplicationDbContext context,
 040        IOrdersClientStatusSyncService statusSync,
 041        ILogger<OrdersClientStatusReconcileJob> logger,
 042        OrdersStatusSyncHealthStore health)
 43    {
 044        _health = health;
 045        _context = context;
 046        _statusSync = statusSync;
 047        _logger = logger;
 048    }
 49
 50    [Queue(HangfireQueueNames.OrderStatusSync)]
 51    [DisableConcurrentExecution(timeoutInSeconds: 0)]
 52    [AutomaticRetry(Attempts = 0)]
 53    public async Task<string> ExecuteAsync()
 54    {
 055        var since = DateTime.Now - Window;
 056        var candidates = _context.Orders
 057            .AsNoTracking()
 058            .Where(o => o.OrderArchive == null
 059                        && o.OzonClientId != null
 060                        && o.ShipmentNumber != null
 061                        && o.ProcessingDate >= since
 062                        && (o.ClientStatus == null || !FinalStatusNames.Contains(o.ClientStatus.Name)));
 63
 64        // Курсор по Id: каждый запуск берёт следующую пачку, дойдя до конца — начинает с начала.
 065        var cursor = Volatile.Read(ref _cursor);
 066        var ids = await candidates.Where(o => o.Id > cursor).OrderBy(o => o.Id).Select(o => o.Id).Take(BatchSize).ToList
 067        if (ids.Count == 0)
 68        {
 069            Volatile.Write(ref _cursor, 0);
 070            ids = await candidates.OrderBy(o => o.Id).Select(o => o.Id).Take(BatchSize).ToListAsync();
 71        }
 72
 073        if (ids.Count == 0)
 074            return "Нет заказов для опроса";
 75
 076        Volatile.Write(ref _cursor, ids[^1]);
 077        var result = await _statusSync.SyncOrderStatusesAsync(ids);
 078        var failed = result.FailedOrderIds.Count;
 079        var summary = result.Error ?? $"Опрошено {ids.Count}, ошибок запроса {failed}";
 080        _logger.LogInformation("{JobId}: {Summary}", JobId, summary);
 81
 082        if (_health.Record(ids.Count, failed, result.Error, out var nowProblem) && nowProblem)
 083            _logger.LogError("Опрос статусов Ozon не получает ответов: {Details}", summary);
 84
 085        return summary;
 086    }
 87}