< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridFacetsResult
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridFacetsService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 3
Coverable lines: 3
Total lines: 203
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_Counts()100%210%
get_Total()100%210%
get_Unsupported()100%210%

File(s)

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

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models;
 4using OzonDomains.Models.AccessControl;
 5using OzonRepositories.Authorization;
 6using OzonRepositories.Context;
 7using Services.CacheServices.Cache.UserCacheService;
 8using Services.DataServices;
 9using Services.FiltersServices.SortModels;
 10
 11namespace OzonOrdersWeb.WebServices.Orders.Grid;
 12
 13/// <summary>Счётчики значений справочной колонки при текущих остальных фильтрах.</summary>
 14public sealed class OrdersGridFacetsRequest
 15{
 16    /// <summary>colId справочной колонки (ozonClient, appStatus, supplier, …).</summary>
 17    public string Column { get; set; } = string.Empty;
 18
 19    /// <summary>Текущая модель фильтров грида; фильтр самой колонки игнорируется.</summary>
 20    public Dictionary<string, AgGridFilterModelItem> FilterModel { get; set; } = new(StringComparer.OrdinalIgnoreCase);
 21
 22    public string? Search { get; set; }
 23
 24    public bool IncludeArchived { get; set; }
 25}
 26
 27public sealed class OrdersGridFacetsResult
 28{
 29    /// <summary>Значение (как в справочнике фильтра) → количество заказов.</summary>
 030    public Dictionary<string, int> Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
 31
 32    /// <summary>Всего заказов при остальных фильтрах (для пункта «Все»).</summary>
 033    public int Total { get; set; }
 34
 35    /// <summary>Для колонки счётчики не считаются (нет поля в БД).</summary>
 036    public bool Unsupported { get; set; }
 37}
 38
 39public interface IOrdersGridFacetsService
 40{
 41    Task<OrdersGridFacetsResult> GetFacetsAsync(OrdersGridFacetsRequest request, ClaimsPrincipal principal, Cancellation
 42}
 43
 44/// <summary>
 45/// Один GROUP BY поверх отфильтрованного запроса списка (без пагинации): ключ — то же поле,
 46/// по которому фильтрует <see cref="OrdersGridMultiValueFilters"/>, поэтому значения совпадают со справочником.
 47/// </summary>
 48public sealed class OrdersGridFacetsService : IOrdersGridFacetsService
 49{
 50    public const string EmptyAppStatus = "Не указан";
 51
 52    private readonly ApplicationDbContext _context;
 53    private readonly IOrdersGridQueryService _gridQueryService;
 54    private readonly IOrdersGridScopedQuery _scoped;
 55    private readonly IOrdersGridComputedResolver _computedResolver;
 56
 57    public OrdersGridFacetsService(
 58        ApplicationDbContext context,
 59        IOrdersGridQueryService gridQueryService,
 60        IOrdersGridScopedQuery scoped,
 61        IOrdersGridComputedResolver computedResolver)
 62    {
 63        _computedResolver = computedResolver;
 64        _context = context;
 65        _gridQueryService = gridQueryService;
 66        _scoped = scoped;
 67    }
 68
 69    public static bool Supports(string column) => AgGridRequestMapper.IsComputedColumn(column) || column.ToLowerInvarian
 70        "ozonclient" or "deliveryprovider" or "deliveryname" or "clientstatus" or "appstatus"
 71        or "supplierstatus" or "shipmentwarehouse" or "supplier";
 72
 73    public async Task<OrdersGridFacetsResult> GetFacetsAsync(
 74        OrdersGridFacetsRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken)
 75    {
 76        if (!Supports(request.Column))
 77        {
 78            return new OrdersGridFacetsResult { Unsupported = true };
 79        }
 80
 81        var user = await _scoped.ResolveUserAsync(principal) ?? throw new UnauthorizedAccessException("Пользователь не н
 82
 83        var gridRequest = new AgGridDataRequest
 84        {
 85            StartRow = 0,
 86            EndRow = _gridQueryService.BlockSize,
 87            Search = request.Search,
 88            IncludeArchived = request.IncludeArchived,
 89            FilterModel = new Dictionary<string, AgGridFilterModelItem>(
 90                request.FilterModel.Where(kv => !string.Equals(kv.Key, request.Column, StringComparison.OrdinalIgnoreCas
 91                StringComparer.OrdinalIgnoreCase)
 92        };
 93        var mapped = AgGridRequestMapper.Map(gridRequest, _gridQueryService.BlockSize);
 94        var computedFilters = new List<(OrdersGridFormulaTranslator.Translation, OrdersGridComputedFilters.ComputedFilte
 95        foreach (var cf in mapped.ComputedFilters)
 96        {
 97            var ct = await _computedResolver.ResolveAsync(user.User.Id, cf.ColId, cancellationToken);
 98            if (ct != null) computedFilters.Add((ct, cf));
 99        }
 100
 101        // GROUP BY прямо поверх отфильтрованного RLS-запроса: EF выносит его ORDER BY в подзапрос, без повторного Order
 102        var query = await _scoped.BuildFilteredAsync(user, mapped, computedFilters, null, cancellationToken);
 103        var scoped = query;
 104        var ids = scoped.Select(o => o.Id);
 105
 106        var result = new OrdersGridFacetsResult();
 107        List<(string? Key, int Count)> groups;
 108        if (AgGridRequestMapper.IsComputedColumn(request.Column))
 109        {
 110            // Вычисляемый столбец: GROUP BY по переведённой формуле (текст/да-нет); числа и даты — без счётчиков.
 111            var t = await _computedResolver.ResolveAsync(user.User.Id, request.Column, cancellationToken);
 112            if (t is null || (t.Type != "text" && t.Type != "bool"))
 113            {
 114                return new OrdersGridFacetsResult { Unsupported = true };
 115            }
 116
 117            groups = await GroupByComputedAsync(scoped, t, cancellationToken);
 118            result.Total = await scoped.CountAsync(cancellationToken);
 119            foreach (var (key, count) in groups)
 120            {
 121                if (string.IsNullOrWhiteSpace(key)) continue;
 122                result.Counts[key] = result.Counts.TryGetValue(key, out var existing) ? existing + count : count;
 123            }
 124
 125            return result;
 126        }
 127
 128        switch (request.Column.ToLowerInvariant())
 129        {
 130            case "ozonclient":
 131                groups = await GroupAsync(scoped, o => o.OzonClient != null ? o.OzonClient.Name : null, cancellationToke
 132                break;
 133            case "deliveryprovider":
 134                groups = await GroupAsync(scoped, o => o.Delivery != null && o.Delivery.Provider != null ? o.Delivery.Pr
 135                break;
 136            case "deliveryname":
 137                groups = await GroupAsync(scoped, o => o.Delivery != null ? o.Delivery.Name : null, cancellationToken);
 138                break;
 139            case "clientstatus":
 140                groups = await GroupAsync(scoped, o => o.ClientStatus != null ? o.ClientStatus.Name : null, cancellation
 141                break;
 142            case "appstatus":
 143                groups = await GroupAsync(scoped, o => o.AppStatus != null ? o.AppStatus.Name : null, cancellationToken)
 144                // Заказы без статуса в отчёте — пункт «Не указан» в списке.
 145                groups = groups.Select(g => ((string?)(g.Key ?? EmptyAppStatus), g.Count)).ToList();
 146                break;
 147            case "shipmentwarehouse":
 148                groups = await GroupAsync(scoped, o => o.ShipmentWarehouse != null ? o.ShipmentWarehouse.Name : null, ca
 149                break;
 150            case "supplier":
 151                groups = await GroupAsync(scoped, o => o.Supplier != null ? o.Supplier.Name : null, cancellationToken);
 152                break;
 153            case "supplierstatus":
 154            {
 155                // Значение в справочнике — Id статуса поставщика (см. OrdersGridFilterOptionsService).
 156                var states = await _context.OrderSupplierStates.AsNoTracking()
 157                    .Where(s => s.ActiveOrderId != null && s.SupplierStatusId.HasValue && ids.Contains(s.OrderId))
 158                    .GroupBy(s => s.SupplierStatusId!.Value)
 159                    .Select(g => new { g.Key, Count = g.Count() })
 160                    .ToListAsync(cancellationToken);
 161                groups = states.Select(g => ((string?)g.Key.ToString(), g.Count)).ToList();
 162                break;
 163            }
 164            default:
 165                return new OrdersGridFacetsResult { Unsupported = true };
 166        }
 167
 168        result.Total = await scoped.CountAsync(cancellationToken);
 169        foreach (var (key, count) in groups)
 170        {
 171            if (string.IsNullOrWhiteSpace(key)) continue;
 172            result.Counts[key] = result.Counts.TryGetValue(key, out var existing) ? existing + count : count;
 173        }
 174
 175        return result;
 176    }
 177
 178    private static async Task<List<(string? Key, int Count)>> GroupByComputedAsync(
 179        IQueryable<Order> query, OrdersGridFormulaTranslator.Translation t, CancellationToken cancellationToken)
 180    {
 181        if (t.Type == "text")
 182        {
 183            var key = (System.Linq.Expressions.Expression<Func<Order, string?>>)t.Lambda;
 184            return await GroupAsync(query, key, cancellationToken);
 185        }
 186
 187        var boolKey = (System.Linq.Expressions.Expression<Func<Order, bool?>>)t.Lambda;
 188        var rows = await query.GroupBy(boolKey).Select(g => new { g.Key, Count = g.Count() }).ToListAsync(cancellationTo
 189        return rows.Select(r => ((string?)(r.Key == true ? "true" : "false"), r.Count)).ToList();
 190    }
 191
 192    private static async Task<List<(string? Key, int Count)>> GroupAsync(
 193        IQueryable<Order> query,
 194        System.Linq.Expressions.Expression<Func<Order, string?>> keySelector,
 195        CancellationToken cancellationToken)
 196    {
 197        var rows = await query
 198            .GroupBy(keySelector)
 199            .Select(g => new { g.Key, Count = g.Count() })
 200            .ToListAsync(cancellationToken);
 201        return rows.Select(r => (r.Key, r.Count)).ToList();
 202    }
 203}