< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.AnalysisServices.ProductGridPage
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/ProductAnalysisGridService.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 163
Line coverage: 100%
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_Rows()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/ProductAnalysisGridService.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Linq.Expressions;
 3using Microsoft.EntityFrameworkCore;
 4using OzonOrdersWeb.Areas.Studio2.DTOs;
 5using OzonOrdersWeb.WebServices.Grid;
 6using Services.DataServices;
 7
 8namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices;
 9
 10/// <summary>Фильтры страницы анализа, которые применяются на уровне БД (ключ кэша набора групп).</summary>
 11public sealed record ProductAnalysisFilter(
 12    DateTime? StartDate,
 13    DateTime? EndDate,
 14    int? ManufacturerId,
 15    int? AppStatusId,
 16    int? ClientStatusId,
 17    int? OzonClientId,
 18    decimal? MinPrice,
 19    decimal? MaxPrice,
 20    string? ClientName = null)
 21{
 22    public static ProductAnalysisFilter FromParams(IReadOnlyDictionary<string, string?> p) => new(
 23        Date(p, "startDate"), Date(p, "endDate"),
 24        Int(p, "manufacturerId"), Int(p, "appStatusId"), Int(p, "clientStatusId"), Int(p, "ozonClientId"),
 25        Dec(p, "minPrice"), Dec(p, "maxPrice"), Raw(p, "clientName")?.Trim());
 26
 27    private static string? Raw(IReadOnlyDictionary<string, string?> p, string key) =>
 28        p.TryGetValue(key, out var v) && !string.IsNullOrWhiteSpace(v) ? v : null;
 29
 30    private static DateTime? Date(IReadOnlyDictionary<string, string?> p, string key) =>
 31        DateTime.TryParse(Raw(p, key), CultureInfo.InvariantCulture, DateTimeStyles.None, out var d) ? d : null;
 32
 33    private static int? Int(IReadOnlyDictionary<string, string?> p, string key) =>
 34        int.TryParse(Raw(p, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : null;
 35
 36    private static decimal? Dec(IReadOnlyDictionary<string, string?> p, string key) =>
 37        decimal.TryParse(Raw(p, key)?.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out var d) ?
 38}
 39
 40public sealed record ProductGridStats(int TotalProducts, int TotalOrders, decimal TotalAmount, decimal AverageCheck);
 41
 1342public sealed record ProductGridPage(List<ProductGroupDto> Rows, int TotalCount, ProductGridStats Stats);
 43
 44/// <summary>
 45/// Серверная часть грида «Продукты»: читает узкую проекцию заказов без Include, группирует по ProductKey,
 46/// кэширует готовый набор групп на короткое время (сортировка/фильтры/страницы — по кэшу в памяти).
 47/// </summary>
 48public sealed class ProductAnalysisGridService(OrdersDataServices orders, ProductAnalysisCache cache)
 49{
 50    private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3);
 51
 52    public static readonly ServerGridDefinition<ProductGroupDto> Definition = BuildDefinition();
 53
 54    private static ServerGridDefinition<ProductGroupDto> BuildDefinition() =>
 55        new ServerGridDefinition<ProductGroupDto>
 56        {
 57            DefaultOrder = q => q.OrderByDescending(p => p.TotalOrders),
 58            TieBreaker = p => (long)p.TotalQuantity
 59        }
 60        .Col("productKey", p => p.ProductKey)
 61        .Col("article", p => p.Article)
 62        .Col("productName", p => p.ProductName)
 63        .Col("manufacturerName", p => p.ManufacturerName)
 64        .Col("etProducerName", p => p.EtProducerName)
 65        .Col("topClients", p => p.TopClientsText)
 66        .Col("totalOrders", p => p.TotalOrders)
 67        .Col("totalQuantity", p => p.TotalQuantity)
 68        .Col("totalTrueQuantity", p => p.TotalTrueQuantity)
 69        .Col("totalShipmentAmount", p => p.TotalShipmentAmount)
 70        .Col("averageCheck", p => p.AverageCheck)
 71        .Col("minPrice", p => p.MinPrice)
 72        .Col("maxPrice", p => p.MaxPrice)
 73        .Col("avgPrice", p => p.AvgPrice)
 74        .Col("avgCostPrice", p => p.AvgCostPrice)
 75        .Col("verifiedOrdersPercent", p => p.VerifiedOrdersPercent)
 76        .Col("avgProfit", p => p.AvgProfit)
 77        .Col("avgDiscount", p => p.AvgDiscount)
 78        .Search(p => p.ProductKey, p => p.Article, p => p.ProductName, p => p.ManufacturerName);
 79
 80    public Task<IReadOnlyList<ProductGroupDto>> GetGroupsAsync(ProductAnalysisFilter filter, CancellationToken ct)
 81    {
 82        filter = filter with { StartDate = ProductAnalysisCache.Canon(filter.StartDate), EndDate = ProductAnalysisCache.
 83        return cache.GetOrAddAsync(("pa.products", filter), CacheTtl, ProductAnalysisCache.GroupsWeight,
 84            () => LoadGroupsAsync(filter, CancellationToken.None), ct);
 85    }
 86
 87    private async Task<IReadOnlyList<ProductGroupDto>> LoadGroupsAsync(ProductAnalysisFilter f, CancellationToken ct)
 88    {
 89        var query = orders.GetAnalyticsQueryable().Where(o => !string.IsNullOrEmpty(o.ProductKey));
 90        if (f.StartDate.HasValue) query = query.Where(o => o.ProcessingDate >= f.StartDate.Value);
 91        if (f.EndDate.HasValue) query = query.Where(o => o.ProcessingDate <= f.EndDate.Value);
 92        if (f.ManufacturerId.HasValue) query = query.Where(o => o.ManufacturerId == f.ManufacturerId.Value);
 93        if (f.AppStatusId.HasValue) query = query.Where(o => o.AppStatusId == f.AppStatusId.Value);
 94        if (f.ClientStatusId.HasValue) query = query.Where(o => o.ClientStatusId == f.ClientStatusId.Value);
 95        if (f.OzonClientId.HasValue) query = query.Where(o => o.OzonClientId == f.OzonClientId.Value);
 96        if (!string.IsNullOrEmpty(f.ClientName)) query = query.Where(o => o.OzonClient != null && o.OzonClient.Name == f
 97        if (f.MinPrice.HasValue) query = query.Where(o => o.Price >= f.MinPrice.Value);
 98        if (f.MaxPrice.HasValue) query = query.Where(o => o.Price <= f.MaxPrice.Value);
 99
 100        var rows = await AnalyticsRowProjection.Project(query).ToListAsync(ct);
 101
 102        return ProductGroupCalculator.Build(rows).OrderBy(p => p.ProductKey, StringComparer.Ordinal).ToList();
 103    }
 104
 105    /// <summary>Детализация одного продукта за период (заказы одного ProductKey — небольшая выборка).</summary>
 106    public async Task<ProductDetailAnalysisDto?> GetDetailAsync(string productKey, DateTime? startDate, DateTime? endDat
 107    {
 108        var query = orders.GetAnalyticsQueryable().Where(o => o.ProductKey == productKey);
 109        if (startDate.HasValue) query = query.Where(o => o.ProcessingDate >= startDate.Value);
 110        if (endDate.HasValue) query = query.Where(o => o.ProcessingDate <= endDate.Value);
 111        var rows = await AnalyticsRowProjection.Project(query).ToListAsync(ct);
 112        return ProductDetailBuilder.Build(productKey, rows);
 113    }
 114
 115    /// <summary>Блок строк: фильтры/поиск/сортировка по всему набору групп, итоги — по отфильтрованному набору.</summar
 116    public async Task<ProductGridPage> QueryAsync(ServerGridRequest request, CancellationToken ct)
 117    {
 118        var groups = await GetGroupsAsync(ProductAnalysisFilter.FromParams(request.Params), ct);
 119        return Query(groups, request);
 120    }
 121
 122    public static ProductGridPage Query(IEnumerable<ProductGroupDto> groups, ServerGridRequest request)
 123    {
 124        var filtered = ServerGridQuery.ApplyFilters(groups.AsQueryable(), Definition, request).ToList();
 125        var totalOrders = filtered.Sum(p => p.TotalOrders);
 126        var totalAmount = filtered.Sum(p => p.TotalShipmentAmount);
 127        var stats = new ProductGridStats(filtered.Count, totalOrders, totalAmount, totalOrders > 0 ? totalAmount / total
 128
 129        var start = Math.Max(0, request.StartRow);
 130        var take = Math.Clamp(request.EndRow - start, 1, ServerGridQuery.MaxBlockSize);
 131        var page = ServerGridQuery.ApplySort(filtered.AsQueryable(), Definition, request.SortModel)
 132            .Skip(start).Take(take).ToList();
 133        return new ProductGridPage(page, filtered.Count, stats);
 134    }
 135
 136    /// <summary>Значения колонки с числом групп при остальных фильтрах — для set-фильтров.</summary>
 137    public async Task<List<ServerGridFacet>> FacetsAsync(ServerGridRequest request, CancellationToken ct)
 138    {
 139        if (string.IsNullOrWhiteSpace(request.ColId) || !Definition.Columns.TryGetValue(request.ColId, out var col))
 140            return [];
 141        var groups = await GetGroupsAsync(ProductAnalysisFilter.FromParams(request.Params), ct);
 142        var filtered = ServerGridQuery.ApplyFilters(groups.AsQueryable(), Definition, request, excludeColId: request.Col
 143
 144        var boxed = Expression.Lambda<Func<ProductGroupDto, object?>>(
 145            Expression.Convert(col.Selector.Body, typeof(object)), col.Selector.Parameters);
 146        var selector = boxed.Compile();
 147        return filtered
 148            .Select(selector)
 149            .GroupBy(v => FacetKey(v))
 150            .Select(g => new ServerGridFacet { Value = g.Key, Count = g.Count() })
 151            .OrderByDescending(f => f.Count)
 152            .Take(500)
 153            .ToList();
 154    }
 155
 156    private static string FacetKey(object? key) => key switch
 157    {
 158        null => string.Empty,
 159        bool b => b ? "true" : "false",
 160        IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
 161        _ => key.ToString() ?? string.Empty
 162    };
 163}

Methods/Properties

get_Rows()