< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridSidebarPresetDto
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridSidebarPresetsService.cs
Line coverage
66%
Covered lines: 2
Uncovered lines: 1
Coverable lines: 3
Total lines: 193
Line coverage: 66.6%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Name()100%11100%
get_View()100%11100%
get_Count()100%210%

File(s)

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

#LineLine coverage
 1using System.Security.Claims;
 2using System.Text.Json;
 3using Microsoft.EntityFrameworkCore;
 4using Microsoft.Extensions.Caching.Memory;
 5using OzonDomains.Models.AccessControl;
 6using OzonRepositories.Context;
 7using Services.CacheServices.Cache.UserCacheService;
 8using Services.DataServices;
 9using Services.FiltersServices.SortModels;
 10
 11namespace OzonOrdersWeb.WebServices.Orders.Grid;
 12
 13public sealed class OrdersGridSidebarPresetDto
 14{
 615    public string Name { get; set; } = string.Empty;
 16
 17    /// <summary>Вид как сохранён клиентом (f/s/q/a/g/x/i) — клиент кодирует его в ?v= тем же способом, что и «Ссылка на
 218    public JsonElement View { get; set; }
 19
 20    /// <summary>Количество заказов под фильтрами пресета; null — посчитать не удалось.</summary>
 021    public int? Count { get; set; }
 22}
 23
 24public interface IOrdersGridSidebarPresetsService
 25{
 26    /// <param name="withCounts">Считать количество подходящих заказов (по одному COUNT на пресет — только по запросу па
 27    Task<List<OrdersGridSidebarPresetDto>> GetAsync(ClaimsPrincipal principal, bool withCounts, CancellationToken cancel
 28
 29    /// <summary>Сбросить кэш пользователя (после сохранения/удаления пресета).</summary>
 30    void Invalidate(string userId);
 31}
 32
 33/// <summary>
 34/// Сохранённые представления пользователя для левого меню с бейджем количества: вид пресета переводится в тот же
 35/// запрос, что шлёт грид, и считается COUNT по отфильтрованному запросу. Кэш на пользователя — минута.
 36/// </summary>
 37public sealed class OrdersGridSidebarPresetsService : IOrdersGridSidebarPresetsService
 38{
 39    public const int MaxPresets = 12;
 40    private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(60);
 41
 42    private readonly IOrdersGridUserSettingsService _settings;
 43    private readonly IOrdersGridQueryService _gridQueryService;
 44    private readonly IOrdersGridScopedQuery _scoped;
 45    private readonly IMemoryCache _cache;
 46    private readonly ILogger<OrdersGridSidebarPresetsService> _logger;
 47    private readonly IOrdersGridComputedResolver _computedResolver;
 48
 49    public OrdersGridSidebarPresetsService(
 50        IOrdersGridUserSettingsService settings,
 51        IOrdersGridQueryService gridQueryService,
 52        IOrdersGridScopedQuery scoped,
 53        IMemoryCache cache,
 54        ILogger<OrdersGridSidebarPresetsService> logger,
 55        IOrdersGridComputedResolver computedResolver)
 56    {
 57        _settings = settings;
 58        _gridQueryService = gridQueryService;
 59        _scoped = scoped;
 60        _cache = cache;
 61        _logger = logger;
 62        _computedResolver = computedResolver;
 63    }
 64
 65    public async Task<List<OrdersGridSidebarPresetDto>> GetAsync(ClaimsPrincipal principal, bool withCounts, Cancellatio
 66    {
 67        var user = await _scoped.ResolveUserAsync(principal);
 68        if (user is null)
 69        {
 70            return [];
 71        }
 72
 73        var all = await _settings.GetAllAsync(user.User.Id, cancellationToken);
 74        var presets = ParsePresets(all.GetValueOrDefault("presets"));
 75        if (presets.Count == 0 || !withCounts)
 76        {
 77            return presets;
 78        }
 79
 80        var cacheKey = CacheKey(user.User.Id);
 81        if (_cache.TryGetValue(cacheKey, out List<OrdersGridSidebarPresetDto>? cached) && cached != null)
 82        {
 83            return cached;
 84        }
 85
 86        // COUNT по каждому пресету последовательно (один DbContext — параллелить нельзя); кэш минута.
 87        foreach (var preset in presets)
 88        {
 89            try
 90            {
 91                preset.Count = await CountAsync(preset.View, user, cancellationToken);
 92            }
 93            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 94            {
 95                throw;
 96            }
 97            catch (Exception ex)
 98            {
 99                _logger.LogWarning(ex, "OrdersGrid: не удалось посчитать пресет «{Name}»", preset.Name);
 100            }
 101        }
 102
 103        _cache.Set(cacheKey, presets, CacheTtl);
 104        return presets;
 105    }
 106
 107    public void Invalidate(string userId) => _cache.Remove(CacheKey(userId));
 108
 109    private static string CacheKey(string userId) => "orders-grid:sidebar-presets:" + userId;
 110
 111    /// <summary>presets: [{ name, view: { f, s, q, a, g, x, i }, ... }]</summary>
 112    public static List<OrdersGridSidebarPresetDto> ParsePresets(string? json)
 113    {
 114        var result = new List<OrdersGridSidebarPresetDto>();
 115        if (string.IsNullOrWhiteSpace(json)) return result;
 116        try
 117        {
 118            using var doc = JsonDocument.Parse(json);
 119            if (doc.RootElement.ValueKind != JsonValueKind.Array) return result;
 120            foreach (var item in doc.RootElement.EnumerateArray())
 121            {
 122                if (item.ValueKind != JsonValueKind.Object) continue;
 123                var name = item.TryGetProperty("name", out var n) && n.ValueKind == JsonValueKind.String ? n.GetString()
 124                if (string.IsNullOrWhiteSpace(name) || !item.TryGetProperty("view", out var view) || view.ValueKind != J
 125                result.Add(new OrdersGridSidebarPresetDto { Name = name.Trim(), View = view.Clone() });
 126                if (result.Count >= MaxPresets) break;
 127            }
 128        }
 129        catch (JsonException)
 130        {
 131            // битые настройки — без пресетов
 132        }
 133
 134        return result;
 135    }
 136
 137    /// <summary>Вид грида → AgGridDataRequest (то же, что buildRequest в orders-grid.js).</summary>
 138    public static AgGridDataRequest ToRequest(JsonElement view, int blockSize)
 139    {
 140        var request = new AgGridDataRequest { StartRow = 0, EndRow = blockSize };
 141        if (view.TryGetProperty("f", out var f) && f.ValueKind == JsonValueKind.Object)
 142        {
 143            request.FilterModel = JsonSerializer.Deserialize<Dictionary<string, AgGridFilterModelItem>>(f.GetRawText(), 
 144                                  ?? new Dictionary<string, AgGridFilterModelItem>(StringComparer.OrdinalIgnoreCase);
 145        }
 146        if (view.TryGetProperty("q", out var q) && q.ValueKind == JsonValueKind.String) request.Search = q.GetString();
 147        if (view.TryGetProperty("a", out var a)) request.IncludeArchived = a.ValueKind == JsonValueKind.True;
 148        if (view.TryGetProperty("g", out var g))
 149        {
 150            request.GroupBy = g.ValueKind == JsonValueKind.String ? g.GetString() : g.ValueKind == JsonValueKind.True ? 
 151        }
 152        if (view.TryGetProperty("x", out var x) && x.ValueKind == JsonValueKind.Object)
 153        {
 154            request.Extra = new AgGridExtraFlags
 155            {
 156                OnlyWithZasyl = Flag(x, "onlyWithZasyl"),
 157                OnlyWithReturns = Flag(x, "onlyWithReturns"),
 158                OnlyWithTrackingNumber = Flag(x, "onlyWithTrackingNumber"),
 159                ReturnLogisticBarcode = x.TryGetProperty("returnLogisticBarcode", out var b) && b.ValueKind == JsonValue
 160            };
 161        }
 162        if (view.TryGetProperty("i", out var i) && i.ValueKind == JsonValueKind.Array)
 163        {
 164            request.OnlyIds = i.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.Number).Select(e => e.GetInt32(
 165        }
 166
 167        return request;
 168    }
 169
 170    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
 171
 172    private static bool Flag(JsonElement obj, string name) => obj.TryGetProperty(name, out var v) && v.ValueKind == Json
 173
 174    private async Task<int> CountAsync(JsonElement view, OrdersGridUserContext user, CancellationToken cancellationToken
 175    {
 176        var request = ToRequest(view, _gridQueryService.BlockSize);
 177        if (AgGridRequestLimits.Validate(request.FilterModel, request.SortModel) != null)
 178        {
 179            throw new InvalidOperationException("Пресет превышает лимиты запроса.");
 180        }
 181
 182        var mapped = AgGridRequestMapper.Map(request, _gridQueryService.BlockSize);
 183        var computedFilters = new List<(OrdersGridFormulaTranslator.Translation, OrdersGridComputedFilters.ComputedFilte
 184        foreach (var cf in mapped.ComputedFilters)
 185        {
 186            var ct = await _computedResolver.ResolveAsync(user.User.Id, cf.ColId, cancellationToken);
 187            if (ct != null) computedFilters.Add((ct, cf));
 188        }
 189
 190        var query = await _scoped.BuildFilteredAsync(user, mapped, computedFilters, request.OnlyIds, cancellationToken);
 191        return await query.CountAsync(cancellationToken);
 192    }
 193}

Methods/Properties

get_Name()
get_View()
get_Count()