< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridFacetsService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridFacetsService.cs
Line coverage
3%
Covered lines: 3
Uncovered lines: 86
Coverable lines: 89
Total lines: 203
Line coverage: 3.3%
Branch coverage
29%
Covered branches: 30
Total branches: 102
Branch coverage: 29.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
Supports(...)85.71%3535100%
GetFacetsAsync()0%3782610%
GroupByComputedAsync()0%620%
GroupAsync()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>
 30    public Dictionary<string, int> Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
 31
 32    /// <summary>Всего заказов при остальных фильтрах (для пункта «Все»).</summary>
 33    public int Total { get; set; }
 34
 35    /// <summary>Для колонки счётчики не считаются (нет поля в БД).</summary>
 36    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
 057    public OrdersGridFacetsService(
 058        ApplicationDbContext context,
 059        IOrdersGridQueryService gridQueryService,
 060        IOrdersGridScopedQuery scoped,
 061        IOrdersGridComputedResolver computedResolver)
 62    {
 063        _computedResolver = computedResolver;
 064        _context = context;
 065        _gridQueryService = gridQueryService;
 066        _scoped = scoped;
 067    }
 68
 669    public static bool Supports(string column) => AgGridRequestMapper.IsComputedColumn(column) || column.ToLowerInvarian
 670        "ozonclient" or "deliveryprovider" or "deliveryname" or "clientstatus" or "appstatus"
 671        or "supplierstatus" or "shipmentwarehouse" or "supplier";
 72
 73    public async Task<OrdersGridFacetsResult> GetFacetsAsync(
 74        OrdersGridFacetsRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken)
 75    {
 076        if (!Supports(request.Column))
 77        {
 078            return new OrdersGridFacetsResult { Unsupported = true };
 79        }
 80
 081        var user = await _scoped.ResolveUserAsync(principal) ?? throw new UnauthorizedAccessException("Пользователь не н
 82
 083        var gridRequest = new AgGridDataRequest
 084        {
 085            StartRow = 0,
 086            EndRow = _gridQueryService.BlockSize,
 087            Search = request.Search,
 088            IncludeArchived = request.IncludeArchived,
 089            FilterModel = new Dictionary<string, AgGridFilterModelItem>(
 090                request.FilterModel.Where(kv => !string.Equals(kv.Key, request.Column, StringComparison.OrdinalIgnoreCas
 091                StringComparer.OrdinalIgnoreCase)
 092        };
 093        var mapped = AgGridRequestMapper.Map(gridRequest, _gridQueryService.BlockSize);
 094        var computedFilters = new List<(OrdersGridFormulaTranslator.Translation, OrdersGridComputedFilters.ComputedFilte
 095        foreach (var cf in mapped.ComputedFilters)
 96        {
 097            var ct = await _computedResolver.ResolveAsync(user.User.Id, cf.ColId, cancellationToken);
 098            if (ct != null) computedFilters.Add((ct, cf));
 099        }
 100
 101        // GROUP BY прямо поверх отфильтрованного RLS-запроса: EF выносит его ORDER BY в подзапрос, без повторного Order
 0102        var query = await _scoped.BuildFilteredAsync(user, mapped, computedFilters, null, cancellationToken);
 0103        var scoped = query;
 0104        var ids = scoped.Select(o => o.Id);
 105
 0106        var result = new OrdersGridFacetsResult();
 107        List<(string? Key, int Count)> groups;
 0108        if (AgGridRequestMapper.IsComputedColumn(request.Column))
 109        {
 110            // Вычисляемый столбец: GROUP BY по переведённой формуле (текст/да-нет); числа и даты — без счётчиков.
 0111            var t = await _computedResolver.ResolveAsync(user.User.Id, request.Column, cancellationToken);
 0112            if (t is null || (t.Type != "text" && t.Type != "bool"))
 113            {
 0114                return new OrdersGridFacetsResult { Unsupported = true };
 115            }
 116
 0117            groups = await GroupByComputedAsync(scoped, t, cancellationToken);
 0118            result.Total = await scoped.CountAsync(cancellationToken);
 0119            foreach (var (key, count) in groups)
 120            {
 0121                if (string.IsNullOrWhiteSpace(key)) continue;
 0122                result.Counts[key] = result.Counts.TryGetValue(key, out var existing) ? existing + count : count;
 123            }
 124
 0125            return result;
 126        }
 127
 0128        switch (request.Column.ToLowerInvariant())
 129        {
 130            case "ozonclient":
 0131                groups = await GroupAsync(scoped, o => o.OzonClient != null ? o.OzonClient.Name : null, cancellationToke
 0132                break;
 133            case "deliveryprovider":
 0134                groups = await GroupAsync(scoped, o => o.Delivery != null && o.Delivery.Provider != null ? o.Delivery.Pr
 0135                break;
 136            case "deliveryname":
 0137                groups = await GroupAsync(scoped, o => o.Delivery != null ? o.Delivery.Name : null, cancellationToken);
 0138                break;
 139            case "clientstatus":
 0140                groups = await GroupAsync(scoped, o => o.ClientStatus != null ? o.ClientStatus.Name : null, cancellation
 0141                break;
 142            case "appstatus":
 0143                groups = await GroupAsync(scoped, o => o.AppStatus != null ? o.AppStatus.Name : null, cancellationToken)
 144                // Заказы без статуса в отчёте — пункт «Не указан» в списке.
 0145                groups = groups.Select(g => ((string?)(g.Key ?? EmptyAppStatus), g.Count)).ToList();
 0146                break;
 147            case "shipmentwarehouse":
 0148                groups = await GroupAsync(scoped, o => o.ShipmentWarehouse != null ? o.ShipmentWarehouse.Name : null, ca
 0149                break;
 150            case "supplier":
 0151                groups = await GroupAsync(scoped, o => o.Supplier != null ? o.Supplier.Name : null, cancellationToken);
 0152                break;
 153            case "supplierstatus":
 154            {
 155                // Значение в справочнике — Id статуса поставщика (см. OrdersGridFilterOptionsService).
 0156                var states = await _context.OrderSupplierStates.AsNoTracking()
 0157                    .Where(s => s.ActiveOrderId != null && s.SupplierStatusId.HasValue && ids.Contains(s.OrderId))
 0158                    .GroupBy(s => s.SupplierStatusId!.Value)
 0159                    .Select(g => new { g.Key, Count = g.Count() })
 0160                    .ToListAsync(cancellationToken);
 0161                groups = states.Select(g => ((string?)g.Key.ToString(), g.Count)).ToList();
 0162                break;
 163            }
 164            default:
 0165                return new OrdersGridFacetsResult { Unsupported = true };
 166        }
 167
 0168        result.Total = await scoped.CountAsync(cancellationToken);
 0169        foreach (var (key, count) in groups)
 170        {
 0171            if (string.IsNullOrWhiteSpace(key)) continue;
 0172            result.Counts[key] = result.Counts.TryGetValue(key, out var existing) ? existing + count : count;
 173        }
 174
 0175        return result;
 0176    }
 177
 178    private static async Task<List<(string? Key, int Count)>> GroupByComputedAsync(
 179        IQueryable<Order> query, OrdersGridFormulaTranslator.Translation t, CancellationToken cancellationToken)
 180    {
 0181        if (t.Type == "text")
 182        {
 0183            var key = (System.Linq.Expressions.Expression<Func<Order, string?>>)t.Lambda;
 0184            return await GroupAsync(query, key, cancellationToken);
 185        }
 186
 0187        var boolKey = (System.Linq.Expressions.Expression<Func<Order, bool?>>)t.Lambda;
 0188        var rows = await query.GroupBy(boolKey).Select(g => new { g.Key, Count = g.Count() }).ToListAsync(cancellationTo
 0189        return rows.Select(r => ((string?)(r.Key == true ? "true" : "false"), r.Count)).ToList();
 0190    }
 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    {
 0197        var rows = await query
 0198            .GroupBy(keySelector)
 0199            .Select(g => new { g.Key, Count = g.Count() })
 0200            .ToListAsync(cancellationToken);
 0201        return rows.Select(r => (r.Key, r.Count)).ToList();
 0202    }
 203}