< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridSidebarPresetsService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridSidebarPresetsService.cs
Line coverage
41%
Covered lines: 36
Uncovered lines: 51
Coverable lines: 87
Total lines: 193
Line coverage: 41.3%
Branch coverage
67%
Covered branches: 47
Total branches: 70
Branch coverage: 67.1%
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%210%
GetAsync()0%156120%
Invalidate(...)100%210%
CacheKey(...)100%210%
ParsePresets(...)95%202093.33%
ToRequest(...)86.66%3030100%
Flag(...)100%22100%
CountAsync()0%4260%

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{
 15    public string Name { get; set; } = string.Empty;
 16
 17    /// <summary>Вид как сохранён клиентом (f/s/q/a/g/x/i) — клиент кодирует его в ?v= тем же способом, что и «Ссылка на
 18    public JsonElement View { get; set; }
 19
 20    /// <summary>Количество заказов под фильтрами пресета; null — посчитать не удалось.</summary>
 21    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;
 140    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
 049    public OrdersGridSidebarPresetsService(
 050        IOrdersGridUserSettingsService settings,
 051        IOrdersGridQueryService gridQueryService,
 052        IOrdersGridScopedQuery scoped,
 053        IMemoryCache cache,
 054        ILogger<OrdersGridSidebarPresetsService> logger,
 055        IOrdersGridComputedResolver computedResolver)
 56    {
 057        _settings = settings;
 058        _gridQueryService = gridQueryService;
 059        _scoped = scoped;
 060        _cache = cache;
 061        _logger = logger;
 062        _computedResolver = computedResolver;
 063    }
 64
 65    public async Task<List<OrdersGridSidebarPresetDto>> GetAsync(ClaimsPrincipal principal, bool withCounts, Cancellatio
 66    {
 067        var user = await _scoped.ResolveUserAsync(principal);
 068        if (user is null)
 69        {
 070            return [];
 71        }
 72
 073        var all = await _settings.GetAllAsync(user.User.Id, cancellationToken);
 074        var presets = ParsePresets(all.GetValueOrDefault("presets"));
 075        if (presets.Count == 0 || !withCounts)
 76        {
 077            return presets;
 78        }
 79
 080        var cacheKey = CacheKey(user.User.Id);
 081        if (_cache.TryGetValue(cacheKey, out List<OrdersGridSidebarPresetDto>? cached) && cached != null)
 82        {
 083            return cached;
 84        }
 85
 86        // COUNT по каждому пресету последовательно (один DbContext — параллелить нельзя); кэш минута.
 087        foreach (var preset in presets)
 88        {
 89            try
 90            {
 091                preset.Count = await CountAsync(preset.View, user, cancellationToken);
 092            }
 093            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 94            {
 095                throw;
 96            }
 097            catch (Exception ex)
 98            {
 099                _logger.LogWarning(ex, "OrdersGrid: не удалось посчитать пресет «{Name}»", preset.Name);
 0100            }
 0101        }
 102
 0103        _cache.Set(cacheKey, presets, CacheTtl);
 0104        return presets;
 0105    }
 106
 0107    public void Invalidate(string userId) => _cache.Remove(CacheKey(userId));
 108
 0109    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    {
 3114        var result = new List<OrdersGridSidebarPresetDto>();
 4115        if (string.IsNullOrWhiteSpace(json)) return result;
 116        try
 117        {
 2118            using var doc = JsonDocument.Parse(json);
 1119            if (doc.RootElement.ValueKind != JsonValueKind.Array) return result;
 10120            foreach (var item in doc.RootElement.EnumerateArray())
 121            {
 4122                if (item.ValueKind != JsonValueKind.Object) continue;
 4123                var name = item.TryGetProperty("name", out var n) && n.ValueKind == JsonValueKind.String ? n.GetString()
 4124                if (string.IsNullOrWhiteSpace(name) || !item.TryGetProperty("view", out var view) || view.ValueKind != J
 2125                result.Add(new OrdersGridSidebarPresetDto { Name = name.Trim(), View = view.Clone() });
 2126                if (result.Count >= MaxPresets) break;
 127            }
 1128        }
 1129        catch (JsonException)
 130        {
 131            // битые настройки — без пресетов
 1132        }
 133
 2134        return result;
 0135    }
 136
 137    /// <summary>Вид грида → AgGridDataRequest (то же, что buildRequest в orders-grid.js).</summary>
 138    public static AgGridDataRequest ToRequest(JsonElement view, int blockSize)
 139    {
 2140        var request = new AgGridDataRequest { StartRow = 0, EndRow = blockSize };
 2141        if (view.TryGetProperty("f", out var f) && f.ValueKind == JsonValueKind.Object)
 142        {
 1143            request.FilterModel = JsonSerializer.Deserialize<Dictionary<string, AgGridFilterModelItem>>(f.GetRawText(), 
 1144                                  ?? new Dictionary<string, AgGridFilterModelItem>(StringComparer.OrdinalIgnoreCase);
 145        }
 3146        if (view.TryGetProperty("q", out var q) && q.ValueKind == JsonValueKind.String) request.Search = q.GetString();
 3147        if (view.TryGetProperty("a", out var a)) request.IncludeArchived = a.ValueKind == JsonValueKind.True;
 2148        if (view.TryGetProperty("g", out var g))
 149        {
 2150            request.GroupBy = g.ValueKind == JsonValueKind.String ? g.GetString() : g.ValueKind == JsonValueKind.True ? 
 151        }
 2152        if (view.TryGetProperty("x", out var x) && x.ValueKind == JsonValueKind.Object)
 153        {
 1154            request.Extra = new AgGridExtraFlags
 1155            {
 1156                OnlyWithZasyl = Flag(x, "onlyWithZasyl"),
 1157                OnlyWithReturns = Flag(x, "onlyWithReturns"),
 1158                OnlyWithTrackingNumber = Flag(x, "onlyWithTrackingNumber"),
 1159                ReturnLogisticBarcode = x.TryGetProperty("returnLogisticBarcode", out var b) && b.ValueKind == JsonValue
 1160            };
 161        }
 2162        if (view.TryGetProperty("i", out var i) && i.ValueKind == JsonValueKind.Array)
 163        {
 5164            request.OnlyIds = i.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.Number).Select(e => e.GetInt32(
 165        }
 166
 2167        return request;
 168    }
 169
 1170    private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
 171
 3172    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    {
 0176        var request = ToRequest(view, _gridQueryService.BlockSize);
 0177        if (AgGridRequestLimits.Validate(request.FilterModel, request.SortModel) != null)
 178        {
 0179            throw new InvalidOperationException("Пресет превышает лимиты запроса.");
 180        }
 181
 0182        var mapped = AgGridRequestMapper.Map(request, _gridQueryService.BlockSize);
 0183        var computedFilters = new List<(OrdersGridFormulaTranslator.Translation, OrdersGridComputedFilters.ComputedFilte
 0184        foreach (var cf in mapped.ComputedFilters)
 185        {
 0186            var ct = await _computedResolver.ResolveAsync(user.User.Id, cf.ColId, cancellationToken);
 0187            if (ct != null) computedFilters.Add((ct, cf));
 0188        }
 189
 0190        var query = await _scoped.BuildFilteredAsync(user, mapped, computedFilters, request.OnlyIds, cancellationToken);
 0191        return await query.CountAsync(cancellationToken);
 0192    }
 193}