< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridStatusRefreshService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridStatusRefreshService.cs
Line coverage
95%
Covered lines: 38
Uncovered lines: 2
Coverable lines: 40
Total lines: 112
Line coverage: 95%
Branch coverage
75%
Covered branches: 18
Total branches: 24
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
RefreshAsync()75%242492.3%
CooldownKey(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridStatusRefreshService.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Security.Claims;
 3using Microsoft.Extensions.Caching.Memory;
 4
 5namespace OzonOrdersWeb.WebServices.Orders.Grid;
 6
 7public sealed class OrdersGridRefreshStatusesRequest
 8{
 9    public List<int> Ids { get; set; } = [];
 10}
 11
 12public sealed class OrdersGridStatusRefreshResult
 13{
 14    public string? Message { get; set; }
 15
 16    public string? Error { get; set; }
 17
 18    /// <summary>Свежие строки грида по доступным пользователю заказам (после синхронизации с Ozon).</summary>
 19    public List<OrderGridRowDto> Rows { get; set; } = [];
 20}
 21
 22public interface IOrdersGridStatusRefreshService
 23{
 24    Task<OrdersGridStatusRefreshResult> RefreshAsync(IReadOnlyList<int> ids, ClaimsPrincipal principal, CancellationToke
 25}
 26
 27/// <summary>
 28/// Ручное «Обновить» статусов из грида: синхронно запрашивает статусы в Ozon и сразу возвращает
 29/// перечитанные строки, чтобы грид обновил их на месте, без перехода на страницу старого списка.
 30/// </summary>
 31public sealed class OrdersGridStatusRefreshService : IOrdersGridStatusRefreshService
 32{
 33    public const int MaxIds = 100;
 34
 35    /// <summary>Повторно опрашивать Ozon по одному и тому же заказу не чаще, чем раз в это время.</summary>
 136    public static readonly TimeSpan OrderCooldown = TimeSpan.FromSeconds(15);
 37
 38    // Один пользователь — один опрос одновременно (двойной клик / спам по меню не множит запросы к Ozon).
 139    private static readonly ConcurrentDictionary<string, byte> InFlight = new();
 40
 41    private readonly IOrdersClientStatusSyncService _statusSync;
 42    private readonly IOrdersGridRowHydrator _hydrator;
 43    private readonly IOrdersGridScopedQuery _scoped;
 44    private readonly IMemoryCache _cache;
 45
 546    public OrdersGridStatusRefreshService(
 547        IOrdersClientStatusSyncService statusSync,
 548        IOrdersGridRowHydrator hydrator,
 549        IOrdersGridScopedQuery scoped,
 550        IMemoryCache cache)
 51    {
 552        _cache = cache;
 553        _statusSync = statusSync;
 554        _hydrator = hydrator;
 555        _scoped = scoped;
 556    }
 57
 58    public async Task<OrdersGridStatusRefreshResult> RefreshAsync(
 59        IReadOnlyList<int> ids, ClaimsPrincipal principal, CancellationToken cancellationToken)
 60    {
 961        var distinct = ids.Distinct().Take(MaxIds).ToList();
 962        if (distinct.Count == 0)
 63        {
 164            return new OrdersGridStatusRefreshResult { Error = "Не переданы заказы для обновления." };
 65        }
 66
 867        var user = await _scoped.ResolveUserAsync(principal);
 868        if (user is null)
 69        {
 070            return new OrdersGridStatusRefreshResult { Error = "Пользователь не найден." };
 71        }
 72
 73        // RLS: чужие заказы не синхронизируем и не отдаём.
 874        var accessible = await _scoped.FilterAccessibleAsync(user, distinct, includeArchived: true, cancellationToken);
 875        if (accessible.Count == 0)
 76        {
 077            return new OrdersGridStatusRefreshResult { Error = "Нет доступных заказов для обновления." };
 78        }
 79
 880        var userKey = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? principal.Identity?.Name ?? "anonymous";
 881        if (!InFlight.TryAdd(userKey, 0))
 82        {
 183            return new OrdersGridStatusRefreshResult { Error = "Обновление уже выполняется — дождитесь завершения." };
 84        }
 85
 86        try
 87        {
 88            // Недавно опрошенные заказы повторно к Ozon не отправляем — отдаём текущие строки из БД.
 1989            var toSync = accessible.Where(id => !_cache.TryGetValue(CooldownKey(id), out _)).ToList();
 3290            foreach (var id in toSync)
 991                _cache.Set(CooldownKey(id), true, OrderCooldown);
 92
 793            OrdersClientStatusSyncResult? sync = null;
 794            if (toSync.Count > 0)
 695                sync = await _statusSync.SyncOrderStatusesAsync(toSync, cancellationToken);
 96
 797            var rows = await _hydrator.HydrateAsync(accessible, principal, cancellationToken);
 798            return new OrdersGridStatusRefreshResult
 799            {
 7100                Message = sync?.Message ?? "Статусы обновлялись только что — изменений нет.",
 7101                Error = sync?.Error,
 7102                Rows = rows
 7103            };
 104        }
 105        finally
 106        {
 7107            InFlight.TryRemove(userKey, out _);
 108        }
 9109    }
 110
 21111    private static string CooldownKey(int orderId) => $"og-status-refresh:{orderId}";
 112}