< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridQueryService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridQueryService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 142
Coverable lines: 142
Total lines: 266
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 64
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_BlockSize()100%210%
LoadBlockAsync()0%2162460%
LoadOrderedPageAsync()0%4260%
GetAllowedColumnsAsync()0%620%
ResolveUserAsync()0%7280%
ComputeAllowedColumnsAsync()100%210%
LoadLabelExistsAsync()0%620%

File(s)

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

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models;
 4using OzonDomains.Models.AccessControl;
 5using OzonRepositories.Authorization;
 6using OzonRepositories.Context;
 7using OzonRepositories.Context.Identity;
 8using Services.Authorization.Services;
 9using Services.CacheServices.Cache.UserCacheService;
 10using Services.DataServices;
 11using Services.FiltersServices.DataFilterManagers;
 12using Services.LabelServices;
 13
 14namespace OzonOrdersWeb.WebServices.Orders.Grid;
 15
 16public interface IOrdersGridQueryService
 17{
 18    /// <summary>Размер блока Infinite Row Model — клиент обязан запрашивать ровно такими блоками.</summary>
 19    int BlockSize { get; }
 20
 21    Task<OrdersGridDataResult> LoadBlockAsync(AgGridDataRequest request, ClaimsPrincipal principal, CancellationToken ca
 22
 23    /// <summary>Видимые пользователю колонки (для построения colDefs на странице).</summary>
 24    Task<IReadOnlyCollection<string>> GetAllowedColumnsAsync(ClaimsPrincipal principal, CancellationToken cancellationTo
 25}
 26
 27/// <summary>
 28/// Оркестрация блока строк для AG Grid поверх общего конвейера списка заказов
 29/// (тот же путь, что <c>OrdersController.IndexTableAjax</c>, без cookies/ViewData).
 30/// </summary>
 31public sealed class OrdersGridQueryService : IOrdersGridQueryService
 32{
 33    private const int DefaultBlockSize = 100;
 34    private const int MaxOnlyIds = 2000;
 35
 36    private readonly IUserCacheService _userCacheService;
 37    private readonly AccessProfileDataServices _accessProfileDataServices;
 38    private readonly IPermissionService _permissionService;
 39    private readonly IUserScopeProvider _userScopeProvider;
 40    private readonly OrderDataFilterManager _dataFilterManager;
 41    private readonly IOrdersIndexQueryService _ordersIndexQueryService;
 42    private readonly IOrdersIndexPageDetailsService _pageDetailsService;
 43    private readonly ILabelExistenceService _labelExistenceService;
 44    private readonly IOrdersIndexClientStatusRefreshService _clientStatusRefreshService;
 45    private readonly IOrdersGridComputedResolver _computedResolver;
 46    private readonly ApplicationDbContext _context;
 47
 048    public OrdersGridQueryService(
 049        IUserCacheService userCacheService,
 050        AccessProfileDataServices accessProfileDataServices,
 051        IPermissionService permissionService,
 052        IUserScopeProvider userScopeProvider,
 053        OrderDataFilterManager dataFilterManager,
 054        IOrdersIndexQueryService ordersIndexQueryService,
 055        IOrdersIndexPageDetailsService pageDetailsService,
 056        ILabelExistenceService labelExistenceService,
 057        IOrdersIndexClientStatusRefreshService clientStatusRefreshService,
 058        IOrdersGridComputedResolver computedResolver,
 059        ApplicationDbContext context)
 60    {
 061        _computedResolver = computedResolver;
 062        _clientStatusRefreshService = clientStatusRefreshService;
 063        _context = context;
 064        _userCacheService = userCacheService;
 065        _accessProfileDataServices = accessProfileDataServices;
 066        _permissionService = permissionService;
 067        _userScopeProvider = userScopeProvider;
 068        _dataFilterManager = dataFilterManager;
 069        _ordersIndexQueryService = ordersIndexQueryService;
 070        _pageDetailsService = pageDetailsService;
 071        _labelExistenceService = labelExistenceService;
 072    }
 73
 074    public int BlockSize => DefaultBlockSize;
 75
 76    public async Task<OrdersGridDataResult> LoadBlockAsync(
 77        AgGridDataRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken)
 78    {
 079        var (user, accessProfile) = await ResolveUserAsync(principal);
 080        if (user is null)
 81        {
 082            throw new UnauthorizedAccessException("Пользователь не найден.");
 83        }
 84
 085        var mapped = AgGridRequestMapper.Map(request, BlockSize);
 086        var filter = mapped.Filter;
 87
 088        var onlyIds = request.OnlyIds is { Count: > 0 } ? request.OnlyIds.Take(MaxOnlyIds).ToList() : null;
 89
 90        // Вычисляемые столбцы: формулы пользователя → SQL-выражения для фильтра и сортировки (непереводимые пропускаем)
 091        var computedFilters = new List<(OrdersGridFormulaTranslator.Translation, OrdersGridComputedFilters.ComputedFilte
 092        foreach (var cf in mapped.ComputedFilters)
 93        {
 094            var t = await _computedResolver.ResolveAsync(user.Id, cf.ColId, cancellationToken);
 095            if (t != null) computedFilters.Add((t, cf));
 096        }
 097        var computedSortKeys = new Dictionary<string, System.Linq.Expressions.LambdaExpression>(StringComparer.OrdinalIg
 098        foreach (var sortItem in mapped.Sorts.Where(x => AgGridRequestMapper.IsComputedColumn(x.ColId)))
 99        {
 0100            var t = await _computedResolver.ResolveAsync(user.Id, sortItem.ColId, cancellationToken);
 0101            if (t != null) computedSortKeys[sortItem.ColId] = t.Lambda;
 0102        }
 103
 0104        var hasPostFilters = mapped.MultiValueFilters.Count > 0 || mapped.ExtraFilters.Count > 0 || onlyIds != null || c
 0105        var hasFilters = hasPostFilters || !_dataFilterManager.AreAllPropertiesNull(filter);
 0106        var hasUserAccessRestrictions = (accessProfile != null && accessProfile.ScopeItems.Any())
 0107                                        || !string.IsNullOrEmpty(_userScopeProvider.GetFilterExpression("Order"));
 108
 0109        var groupBy = OrdersGridGrouping.Supports(request.GroupBy) ? request.GroupBy
 0110            : request.GroupByShipment ? OrdersGridGrouping.Shipment : null;
 111
 0112        var queryRequest = new OrdersIndexQueryRequest
 0113        {
 0114            Filter = filter,
 0115            Page = mapped.Page,
 0116            PageSize = BlockSize,
 0117            SortOrder = mapped.Sort,
 0118            AccessProfile = accessProfile,
 0119            HasFilters = AgGridRequestMapper.ShouldUseDbPath(hasFilters || groupBy != null, mapped.Sort),
 0120            HasUserAccessRestrictions = hasUserAccessRestrictions,
 0121            UserId = user.Id,
 0122            CancellationToken = cancellationToken,
 0123            QueryPostProcessor = hasPostFilters
 0124                ? q =>
 0125                {
 0126                    var filtered = OrdersGridExtraFilters.Apply(
 0127                        OrdersGridMultiValueFilters.Apply(q, mapped.MultiValueFilters, _context), mapped.ExtraFilters);
 0128                    filtered = OrdersGridComputedFilters.Apply(filtered, computedFilters);
 0129                    return onlyIds != null ? filtered.Where(o => onlyIds.Contains(o.Id)) : filtered;
 0130                }
 0131                : null
 0132        };
 133
 134        // Свой ORDER BY нужен при группировке, сортировке по нескольким столбцам и по вычисляемому столбцу.
 0135        var customOrder = groupBy != null || mapped.Sorts.Count > 1 || computedSortKeys.Count > 0;
 0136        var queryResult = customOrder
 0137            ? await LoadOrderedPageAsync(queryRequest, groupBy, mapped.Sorts, computedSortKeys, cancellationToken)
 0138            : await _ordersIndexQueryService.LoadPageAsync(queryRequest);
 139
 0140        IReadOnlyList<Order> orders = queryResult.Orders;
 0141        if (queryResult.RequiresInMemorySort)
 142        {
 0143            orders = _dataFilterManager.ApplySortOrder(orders, mapped.Sort).ToList();
 144        }
 145
 0146        var distinct = _ordersIndexQueryService.PrepareDistinctOrders(orders, filter.GroupByShipment);
 0147        _ordersIndexQueryService.SelectOrdersForPage(distinct, 1, BlockSize, out var ordersForPage);
 148
 0149        if (filter.SyncClientStatusesOnLoad && ordersForPage.Count > 0)
 150        {
 0151            await _clientStatusRefreshService.RefreshOnPageLoadAsync(
 0152                distinct, ordersForPage.Select(o => o.Id).Distinct().ToArray(), cancellationToken);
 153        }
 154
 0155        await _pageDetailsService.LoadOrderDetailsForPageAsync(ordersForPage, accessProfile, cancellationToken);
 156
 0157        var allowed = await ComputeAllowedColumnsAsync(user, cancellationToken);
 0158        var labelExists = await LoadLabelExistsAsync(ordersForPage);
 159
 0160        var rows = OrderGridRowMapper.MapBlock(ordersForPage, allowed, labelExists, filter.GroupByShipment, groupBy);
 161
 0162        return new OrdersGridDataResult
 0163        {
 0164            Rows = rows,
 0165            TotalCount = queryResult.TotalCount,
 0166            LastRow = queryResult.TotalCount,
 0167            AllowedColumns = allowed,
 0168            BlockSize = BlockSize
 0169        };
 0170    }
 171
 172    /// <summary>
 173    /// Тот же отфильтрованный запрос, но со своим порядком: ключ группы (если есть) → сортировки пользователя
 174    /// (несколько столбцов) → дата принятия, новые сверху → Id.
 175    /// </summary>
 176    private async Task<OrdersIndexQueryResult> LoadOrderedPageAsync(
 177        OrdersIndexQueryRequest request, string? groupBy, IReadOnlyList<OrdersGridSorting.SortItem> sorts,
 178        IReadOnlyDictionary<string, System.Linq.Expressions.LambdaExpression> computedKeys, CancellationToken cancellati
 179    {
 0180        var query = await _ordersIndexQueryService.BuildFilteredQueryableAsync(request);
 0181        if (groupBy != null)
 182        {
 0183            query = OrdersGridGrouping.ApplyOrder(query, groupBy);
 184        }
 185
 0186        query = OrdersGridSorting.Apply(query, sorts, alreadyOrdered: groupBy != null, out var ordered,
 0187            resolveExtra: colId => computedKeys.GetValueOrDefault(colId));
 0188        query = ordered
 0189            ? ((IOrderedQueryable<Order>)query).ThenByDescending(o => o.ProcessingDate).ThenByDescending(o => o.Id)
 0190            : query.OrderByDescending(o => o.ProcessingDate).ThenByDescending(o => o.Id);
 191
 0192        var total = await query.CountAsync(cancellationToken);
 0193        var ids = await query
 0194            .Skip((request.Page - 1) * request.PageSize)
 0195            .Take(request.PageSize)
 0196            .Select(o => o.Id)
 0197            .ToListAsync(cancellationToken);
 0198        var orders = ids.Count == 0
 0199            ? new List<Order>()
 0200            : await _ordersIndexQueryService.LoadByIdsPreservingOrderAsync(ids, request.Filter.IncludeArchived);
 201
 0202        return new OrdersIndexQueryResult { Orders = orders, TotalCount = total, RequiresInMemorySort = false };
 0203    }
 204
 205    public async Task<IReadOnlyCollection<string>> GetAllowedColumnsAsync(ClaimsPrincipal principal, CancellationToken c
 206    {
 0207        var (user, _) = await ResolveUserAsync(principal);
 0208        if (user is null)
 209        {
 0210            return [];
 211        }
 212
 0213        return await ComputeAllowedColumnsAsync(user, cancellationToken);
 0214    }
 215
 216    private async Task<(CustomIdentityUser? User, AccessProfile? Profile)> ResolveUserAsync(ClaimsPrincipal principal)
 217    {
 0218        var user = await _userCacheService.GetCachedUserAsync(principal);
 0219        if (user is null)
 220        {
 0221            return (null, null);
 222        }
 223
 224        // Синтетический профиль (Id <= 0) означает «без профиля» — как в OrdersController.
 0225        if (user.AccessProfileId is <= 0)
 226        {
 0227            user.AccessProfileId = null;
 228        }
 229
 0230        AccessProfile? profile = null;
 0231        if (user.AccessProfileId != null)
 232        {
 0233            profile = await _accessProfileDataServices.GetAccessProfileAsync(user.AccessProfileId.Value);
 234            // Гейт колонок читает AvailableOrderColumns через user.AccessProfile.
 0235            user.AccessProfile = profile;
 236        }
 237
 0238        return (user, profile);
 0239    }
 240
 241    private async Task<HashSet<string>> ComputeAllowedColumnsAsync(
 242        CustomIdentityUser user, CancellationToken cancellationToken)
 243    {
 244        // Как на старой странице: матрица AppField грузится всегда, SuperAdmin получает всё разрешённым.
 0245        var readMap = await OrderListFieldReadAccessBuilder.FromUserIdAsync(_permissionService, user.Id, cancellationTok
 0246        return OrdersGridColumnAccess.Compute(user, readMap);
 0247    }
 248
 249    private async Task<IReadOnlyDictionary<string, bool>?> LoadLabelExistsAsync(IReadOnlyList<Order> orders)
 250    {
 0251        var shipmentNumbers = orders
 0252            .Select(o => o.ShipmentNumber)
 0253            .Where(s => !string.IsNullOrWhiteSpace(s))
 0254            .Distinct(StringComparer.OrdinalIgnoreCase)
 0255            .Cast<string>()
 0256            .ToList();
 257
 0258        if (shipmentNumbers.Count == 0)
 259        {
 0260            return null;
 261        }
 262
 0263        var map = await _labelExistenceService.CheckBatchAsync(shipmentNumbers);
 0264        return map.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
 0265    }
 266}