< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.OrdersIndexQueryRequest
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/OrdersIndexQueryService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 11
Coverable lines: 11
Total lines: 242
Line coverage: 0%
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_Filter()100%210%
get_Page()100%210%
get_PageSize()100%210%
get_SortOrder()100%210%
get_AccessProfile()100%210%
get_HasFilters()100%210%
get_HasUserAccessRestrictions()100%210%
get_UserId()100%210%
get_IsReturnsListPage()100%210%
get_CancellationToken()100%210%
get_QueryPostProcessor()100%210%

File(s)

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

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using OServices.FiltersServices.FilterModels;
 3using OzonDomains.Models;
 4using OzonDomains.Models.AccessControl;
 5using Services.DataServices;
 6using Services.FiltersServices.DataFilterManagers;
 7using Services.FiltersServices.SortModels;
 8
 9namespace OzonOrdersWeb.WebServices.Orders;
 10
 11public sealed class OrdersIndexQueryRequest
 12{
 013    public OrderFilterModel Filter { get; init; } = new();
 14
 015    public int Page { get; init; } = 1;
 16
 017    public int PageSize { get; init; } = 15;
 18
 019    public OrderSortState SortOrder { get; init; } = OrderSortState.StandardState;
 20
 021    public AccessProfile? AccessProfile { get; init; }
 22
 023    public bool HasFilters { get; init; }
 24
 025    public bool HasUserAccessRestrictions { get; init; }
 26
 027    public string? UserId { get; init; }
 28
 29    /// <summary>Страница возвратов: быстрый путь по <c>IsReturned</c> без прочих фильтров.</summary>
 030    public bool IsReturnsListPage { get; init; }
 31
 032    public CancellationToken CancellationToken { get; init; }
 33
 34    /// <summary>
 35    /// Дополнительное сужение запроса после общих фильтров (например, множественный выбор справочных
 36    /// значений в гриде). Применяется только на SQL-пути; вызывающий обязан выставить <see cref="HasFilters"/>.
 37    /// </summary>
 038    public Func<IQueryable<Order>, IQueryable<Order>>? QueryPostProcessor { get; init; }
 39}
 40
 41public sealed class OrdersIndexQueryResult
 42{
 43    public List<Order> Orders { get; init; } = new();
 44
 45    public int TotalCount { get; init; }
 46
 47    /// <summary>Нужна in-memory сортировка (нет фильтров и нет RLS).</summary>
 48    public bool RequiresInMemorySort { get; init; }
 49}
 50
 51public interface IOrdersIndexQueryService
 52{
 53    Task<OrdersIndexQueryResult> LoadPageAsync(OrdersIndexQueryRequest request);
 54
 55    List<Order> PrepareDistinctOrders(IReadOnlyList<Order> orders, bool groupByShipment);
 56
 57    List<Order> SelectOrdersForPage(
 58        IReadOnlyList<Order> distinctOrders,
 59        int page,
 60        int pageSize,
 61        out List<Order> ordersForPage);
 62
 63    Task<List<Order>> LoadByIdsPreservingOrderAsync(
 64        IReadOnlyList<int> orderIds,
 65        bool includeArchived);
 66
 67    /// <summary>
 68    /// Отфильтрованный запрос списка (фильтры, ES-сужение, RLS-скоуп) без пагинации —
 69    /// для агрегатов поверх текущего фильтра (например, счётчики значений в справочных фильтрах).
 70    /// </summary>
 71    Task<IQueryable<Order>> BuildFilteredQueryableAsync(OrdersIndexQueryRequest request);
 72}
 73
 74/// <summary>
 75/// Загрузка страницы заказов для Index / IndexAjax: фильтрация, ES, RLS, пагинация на уровне БД.
 76/// </summary>
 77public sealed class OrdersIndexQueryService : IOrdersIndexQueryService
 78{
 79    private readonly OrdersDataServices _orderServices;
 80    private readonly OrderDataFilterManager _dataFilterManager;
 81    private readonly IOrderDataScopeQueryApplier _dataScopeApplier;
 82
 83    public OrdersIndexQueryService(
 84        OrdersDataServices orderServices,
 85        OrderDataFilterManager dataFilterManager,
 86        IOrderDataScopeQueryApplier dataScopeApplier)
 87    {
 88        _orderServices = orderServices;
 89        _dataFilterManager = dataFilterManager;
 90        _dataScopeApplier = dataScopeApplier;
 91    }
 92
 93    public async Task<OrdersIndexQueryResult> LoadPageAsync(OrdersIndexQueryRequest request)
 94    {
 95        var page = request.Page < 1 ? 1 : request.Page;
 96        var pageSize = request.PageSize < 1 ? 15 : request.PageSize;
 97        var ct = request.CancellationToken;
 98
 99        if (!request.HasFilters && !request.HasUserAccessRestrictions)
 100        {
 101            if (request.IsReturnsListPage)
 102            {
 103                var baseQuery = _orderServices.GetOrdersQueryable(request.Filter.IncludeArchived)
 104                    .Where(o => o.IsReturned)
 105                    .OrderByDescending(o => o.ProcessingDate);
 106
 107                var total = await baseQuery.CountAsync(ct);
 108                var skip = (page - 1) * pageSize;
 109                var returnedOrderIds = await baseQuery
 110                    .Skip(skip)
 111                    .Take(pageSize)
 112                    .Select(o => o.Id)
 113                    .ToListAsync(ct);
 114
 115                var orders = returnedOrderIds.Count == 0
 116                    ? new List<Order>()
 117                    : await LoadByIdsPreservingOrderAsync(returnedOrderIds, request.Filter.IncludeArchived);
 118
 119                return new OrdersIndexQueryResult
 120                {
 121                    Orders = orders,
 122                    TotalCount = total,
 123                    RequiresInMemorySort = true
 124                };
 125            }
 126
 127            var indexSkip = (page - 1) * pageSize;
 128            var indexOrders = await _orderServices.GetOrders(indexSkip, pageSize);
 129            var indexTotal = await _orderServices.GetTotalCount();
 130            return new OrdersIndexQueryResult
 131            {
 132                Orders = indexOrders,
 133                TotalCount = indexTotal,
 134                RequiresInMemorySort = true
 135            };
 136        }
 137
 138        var ordersQuery = await BuildFilteredQueryableAsync(request);
 139
 140        // ES-сужение и RLS-скоуп сверху дописывают Where к уже отсортированному запросу и
 141        // не должны ломать порядок, но правило "сортировка по дате принятия, не по релевантности
 142        // ES" обязано соблюдаться строго для всех поисков — переустанавливаем ORDER BY явно
 143        // как последний шаг перед пагинацией.
 144        ordersQuery = _dataFilterManager.ApplyFinalSortQueryable(ordersQuery, request.SortOrder);
 145
 146        var totalCount = await ordersQuery.CountAsync(ct);
 147        var dbSkip = (page - 1) * pageSize;
 148        var filteredOrderIds = await ordersQuery
 149            .Skip(dbSkip)
 150            .Take(pageSize)
 151            .Select(o => o.Id)
 152            .ToListAsync(ct);
 153
 154        var loaded = filteredOrderIds.Count == 0
 155            ? new List<Order>()
 156            : await LoadByIdsPreservingOrderAsync(filteredOrderIds, request.Filter.IncludeArchived);
 157
 158        return new OrdersIndexQueryResult
 159        {
 160            Orders = loaded,
 161            TotalCount = totalCount,
 162            RequiresInMemorySort = false
 163        };
 164    }
 165
 166    public async Task<IQueryable<Order>> BuildFilteredQueryableAsync(OrdersIndexQueryRequest request)
 167    {
 168        var ordersQuery = _orderServices.GetOrdersQueryable(request.Filter.IncludeArchived);
 169        ordersQuery = _dataFilterManager.FilterAndSortQueryable(
 170            ordersQuery,
 171            request.Filter,
 172            request.AccessProfile,
 173            request.SortOrder);
 174        if (request.QueryPostProcessor != null)
 175        {
 176            ordersQuery = request.QueryPostProcessor(ordersQuery);
 177        }
 178        ordersQuery = await _dataFilterManager.NarrowByElasticsearchAsync(ordersQuery, request.Filter);
 179        return await _dataScopeApplier.ApplyAsync(ordersQuery, request.UserId, request.CancellationToken);
 180    }
 181
 182    public List<Order> PrepareDistinctOrders(IReadOnlyList<Order> orders, bool groupByShipment)
 183    {
 184        var distinct = orders
 185            .GroupBy(o => o.Id)
 186            .Select(g => g.First())
 187            .ToList();
 188
 189        if (!groupByShipment)
 190        {
 191            return distinct;
 192        }
 193
 194        var withShipment = distinct
 195            .Where(o => o.ShipmentId.HasValue)
 196            .GroupBy(o => o.ShipmentId!.Value)
 197            .SelectMany(g => g);
 198        var withoutShipment = distinct.Where(o => !o.ShipmentId.HasValue);
 199        return withShipment.Concat(withoutShipment).ToList();
 200    }
 201
 202    public List<Order> SelectOrdersForPage(
 203        IReadOnlyList<Order> distinctOrders,
 204        int page,
 205        int pageSize,
 206        out List<Order> ordersForPage)
 207    {
 208        var viewModelPage = 1;
 209        var skipForPage = (viewModelPage - 1) * pageSize;
 210        ordersForPage = distinctOrders.Skip(skipForPage).Take(pageSize).ToList();
 211
 212        var ordersForPageIds = new HashSet<int>(ordersForPage.Select(o => o.Id));
 213        foreach (var order in distinctOrders)
 214        {
 215            if (!ordersForPageIds.Contains(order.Id))
 216            {
 217                order.OrderExtensions = new List<OrderExtension>();
 218                order.CartItemStatus = null;
 219                order.CartItemStatusColor = null;
 220            }
 221        }
 222
 223        return distinctOrders.ToList();
 224    }
 225
 226    public async Task<List<Order>> LoadByIdsPreservingOrderAsync(
 227        IReadOnlyList<int> orderIds,
 228        bool includeArchived)
 229    {
 230        if (orderIds.Count == 0)
 231        {
 232            return new List<Order>();
 233        }
 234
 235        var ordersFromCache = await _orderServices.GetOrdersByIds(orderIds.ToList(), includeArchived);
 236        var orderDict = ordersFromCache.ToDictionary(o => o.Id);
 237        return orderIds
 238            .Select(id => orderDict.GetValueOrDefault(id))
 239            .OfType<Order>()
 240            .ToList();
 241    }
 242}