< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.AnalysisServices.ProductAnalysisGridService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/ProductAnalysisGridService.cs
Line coverage
42%
Covered lines: 35
Uncovered lines: 48
Coverable lines: 83
Total lines: 163
Line coverage: 42.1%
Branch coverage
2%
Covered branches: 1
Total branches: 38
Branch coverage: 2.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.cctor()100%11100%
BuildDefinition()100%11100%
GetGroupsAsync(...)100%210%
LoadGroupsAsync()0%342180%
GetDetailAsync()0%2040%
QueryAsync()100%210%
Query(...)50%22100%
FacetsAsync()0%2040%
FacetKey(...)0%110100%

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
 42public sealed record ProductGridPage(List<ProductGroupDto> Rows, int TotalCount, ProductGridStats Stats);
 43
 44/// <summary>
 45/// Серверная часть грида «Продукты»: читает узкую проекцию заказов без Include, группирует по ProductKey,
 46/// кэширует готовый набор групп на короткое время (сортировка/фильтры/страницы — по кэшу в памяти).
 47/// </summary>
 048public sealed class ProductAnalysisGridService(OrdersDataServices orders, ProductAnalysisCache cache)
 49{
 150    private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3);
 51
 152    public static readonly ServerGridDefinition<ProductGroupDto> Definition = BuildDefinition();
 53
 54    private static ServerGridDefinition<ProductGroupDto> BuildDefinition() =>
 155        new ServerGridDefinition<ProductGroupDto>
 156        {
 257            DefaultOrder = q => q.OrderByDescending(p => p.TotalOrders),
 158            TieBreaker = p => (long)p.TotalQuantity
 159        }
 160        .Col("productKey", p => p.ProductKey)
 161        .Col("article", p => p.Article)
 162        .Col("productName", p => p.ProductName)
 163        .Col("manufacturerName", p => p.ManufacturerName)
 164        .Col("etProducerName", p => p.EtProducerName)
 165        .Col("topClients", p => p.TopClientsText)
 166        .Col("totalOrders", p => p.TotalOrders)
 167        .Col("totalQuantity", p => p.TotalQuantity)
 168        .Col("totalTrueQuantity", p => p.TotalTrueQuantity)
 169        .Col("totalShipmentAmount", p => p.TotalShipmentAmount)
 170        .Col("averageCheck", p => p.AverageCheck)
 171        .Col("minPrice", p => p.MinPrice)
 172        .Col("maxPrice", p => p.MaxPrice)
 173        .Col("avgPrice", p => p.AvgPrice)
 174        .Col("avgCostPrice", p => p.AvgCostPrice)
 175        .Col("verifiedOrdersPercent", p => p.VerifiedOrdersPercent)
 176        .Col("avgProfit", p => p.AvgProfit)
 177        .Col("avgDiscount", p => p.AvgDiscount)
 178        .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    {
 082        filter = filter with { StartDate = ProductAnalysisCache.Canon(filter.StartDate), EndDate = ProductAnalysisCache.
 083        return cache.GetOrAddAsync(("pa.products", filter), CacheTtl, ProductAnalysisCache.GroupsWeight,
 084            () => LoadGroupsAsync(filter, CancellationToken.None), ct);
 85    }
 86
 87    private async Task<IReadOnlyList<ProductGroupDto>> LoadGroupsAsync(ProductAnalysisFilter f, CancellationToken ct)
 88    {
 089        var query = orders.GetAnalyticsQueryable().Where(o => !string.IsNullOrEmpty(o.ProductKey));
 090        if (f.StartDate.HasValue) query = query.Where(o => o.ProcessingDate >= f.StartDate.Value);
 091        if (f.EndDate.HasValue) query = query.Where(o => o.ProcessingDate <= f.EndDate.Value);
 092        if (f.ManufacturerId.HasValue) query = query.Where(o => o.ManufacturerId == f.ManufacturerId.Value);
 093        if (f.AppStatusId.HasValue) query = query.Where(o => o.AppStatusId == f.AppStatusId.Value);
 094        if (f.ClientStatusId.HasValue) query = query.Where(o => o.ClientStatusId == f.ClientStatusId.Value);
 095        if (f.OzonClientId.HasValue) query = query.Where(o => o.OzonClientId == f.OzonClientId.Value);
 096        if (!string.IsNullOrEmpty(f.ClientName)) query = query.Where(o => o.OzonClient != null && o.OzonClient.Name == f
 097        if (f.MinPrice.HasValue) query = query.Where(o => o.Price >= f.MinPrice.Value);
 098        if (f.MaxPrice.HasValue) query = query.Where(o => o.Price <= f.MaxPrice.Value);
 99
 0100        var rows = await AnalyticsRowProjection.Project(query).ToListAsync(ct);
 101
 0102        return ProductGroupCalculator.Build(rows).OrderBy(p => p.ProductKey, StringComparer.Ordinal).ToList();
 0103    }
 104
 105    /// <summary>Детализация одного продукта за период (заказы одного ProductKey — небольшая выборка).</summary>
 106    public async Task<ProductDetailAnalysisDto?> GetDetailAsync(string productKey, DateTime? startDate, DateTime? endDat
 107    {
 0108        var query = orders.GetAnalyticsQueryable().Where(o => o.ProductKey == productKey);
 0109        if (startDate.HasValue) query = query.Where(o => o.ProcessingDate >= startDate.Value);
 0110        if (endDate.HasValue) query = query.Where(o => o.ProcessingDate <= endDate.Value);
 0111        var rows = await AnalyticsRowProjection.Project(query).ToListAsync(ct);
 0112        return ProductDetailBuilder.Build(productKey, rows);
 0113    }
 114
 115    /// <summary>Блок строк: фильтры/поиск/сортировка по всему набору групп, итоги — по отфильтрованному набору.</summar
 116    public async Task<ProductGridPage> QueryAsync(ServerGridRequest request, CancellationToken ct)
 117    {
 0118        var groups = await GetGroupsAsync(ProductAnalysisFilter.FromParams(request.Params), ct);
 0119        return Query(groups, request);
 0120    }
 121
 122    public static ProductGridPage Query(IEnumerable<ProductGroupDto> groups, ServerGridRequest request)
 123    {
 4124        var filtered = ServerGridQuery.ApplyFilters(groups.AsQueryable(), Definition, request).ToList();
 67125        var totalOrders = filtered.Sum(p => p.TotalOrders);
 67126        var totalAmount = filtered.Sum(p => p.TotalShipmentAmount);
 4127        var stats = new ProductGridStats(filtered.Count, totalOrders, totalAmount, totalOrders > 0 ? totalAmount / total
 128
 4129        var start = Math.Max(0, request.StartRow);
 4130        var take = Math.Clamp(request.EndRow - start, 1, ServerGridQuery.MaxBlockSize);
 4131        var page = ServerGridQuery.ApplySort(filtered.AsQueryable(), Definition, request.SortModel)
 4132            .Skip(start).Take(take).ToList();
 4133        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    {
 0139        if (string.IsNullOrWhiteSpace(request.ColId) || !Definition.Columns.TryGetValue(request.ColId, out var col))
 0140            return [];
 0141        var groups = await GetGroupsAsync(ProductAnalysisFilter.FromParams(request.Params), ct);
 0142        var filtered = ServerGridQuery.ApplyFilters(groups.AsQueryable(), Definition, request, excludeColId: request.Col
 143
 0144        var boxed = Expression.Lambda<Func<ProductGroupDto, object?>>(
 0145            Expression.Convert(col.Selector.Body, typeof(object)), col.Selector.Parameters);
 0146        var selector = boxed.Compile();
 0147        return filtered
 0148            .Select(selector)
 0149            .GroupBy(v => FacetKey(v))
 0150            .Select(g => new ServerGridFacet { Value = g.Key, Count = g.Count() })
 0151            .OrderByDescending(f => f.Count)
 0152            .Take(500)
 0153            .ToList();
 0154    }
 155
 0156    private static string FacetKey(object? key) => key switch
 0157    {
 0158        null => string.Empty,
 0159        bool b => b ? "true" : "false",
 0160        IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
 0161        _ => key.ToString() ?? string.Empty
 0162    };
 163}