< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.AnalysisServices.ProductAnalysisReportService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/AnalysisServices/ProductAnalysisReportService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 58
Coverable lines: 58
Total lines: 111
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
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%210%
get_Start()100%210%
CachedAsync(...)100%210%
RowsAsync(...)100%210%
OverallAsync(...)100%210%
MetricsAsync(...)100%210%
ComparativeAsync(...)0%620%
SeasonalityAsync(...)0%7280%
OrderStatusesAsync(...)0%4260%
WarehouseRegionAsync(...)0%620%
ForecastAsync(...)0%620%
SummaryAsync(...)100%210%

File(s)

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

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Services.DataServices;
 3
 4namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices;
 5
 6/// <summary>
 7/// Отчёты вкладок «Общая статистика», «Доп. метрики», «Сравнительная» и «Сезонность»: узкая проекция заказов
 8/// без Include, расчёт в <see cref="ProductAnalysisReportCalculator"/>, кэш результата на 3 минуты.
 9/// Строки периода кэшируются на минуту — общая статистика и метрики за один период читают БД один раз.
 10/// </summary>
 011public sealed class ProductAnalysisReportService(OrdersDataServices orders, ProductAnalysisCache cache,
 012    SalesForecastService forecast, TrendAnalysisService trends, PurchaseRecommendationService purchases)
 13{
 014    private static readonly TimeSpan ResultTtl = TimeSpan.FromMinutes(3);
 015    private static readonly TimeSpan RowsTtl = TimeSpan.FromMinutes(1);
 16
 017    private sealed record RowsKey(DateTime? Start, DateTime? End, string? ProductKey, int? AppStatusId, bool OnlyDated, 
 18
 19    private Task<T> CachedAsync<T>(object key, TimeSpan ttl, int weight, Func<Task<T>> factory, CancellationToken ct) =>
 020        cache.GetOrAddAsync(key, ttl, weight, factory, ct);
 21
 22    private Task<IReadOnlyList<AnalyticsOrderRow>> RowsAsync(RowsKey key, CancellationToken ct) =>
 023        CachedAsync<IReadOnlyList<AnalyticsOrderRow>>(key, RowsTtl, ProductAnalysisCache.RowsWeight, async () =>
 024        {
 025            var query = orders.GetAnalyticsQueryable();
 026            if (key.OnlyDated) query = query.Where(o => o.ProcessingDate.HasValue);
 027            if (key.OnlyWithProductKey) query = query.Where(o => !string.IsNullOrEmpty(o.ProductKey));
 028            if (key.Start.HasValue) query = query.Where(o => o.ProcessingDate >= key.Start.Value);
 029            if (key.End.HasValue) query = query.Where(o => o.ProcessingDate <= key.End.Value);
 030            if (key.AppStatusId.HasValue) query = query.Where(o => o.AppStatusId == key.AppStatusId.Value);
 031            if (!string.IsNullOrWhiteSpace(key.ProductKey)) query = query.Where(o => o.ProductKey == key.ProductKey);
 032            return await AnalyticsRowProjection.Project(query).ToListAsync(CancellationToken.None);
 033        }, ct);
 34
 035    private static readonly CancellationToken Shared = CancellationToken.None;
 36
 37    // Границы периода приводятся к каноничным (ProductAnalysisCache.Canon): иначе «сейчас» с секундами даёт новый ключ 
 38
 39    public Task<OverallStats> OverallAsync(DateTime? start, DateTime? end, CancellationToken ct)
 40    {
 041        (start, end) = (ProductAnalysisCache.Canon(start), ProductAnalysisCache.Canon(end));
 042        return CachedAsync(("pa.overall", start, end), ResultTtl, ProductAnalysisCache.ResultWeight, async () =>
 043            ProductAnalysisReportCalculator.Overall((await RowsAsync(new RowsKey(start, end, null, null, false), Shared)
 44    }
 45
 46    public Task<AdditionalMetrics> MetricsAsync(DateTime? start, DateTime? end, CancellationToken ct)
 47    {
 048        (start, end) = (ProductAnalysisCache.Canon(start), ProductAnalysisCache.Canon(end));
 049        return CachedAsync(("pa.metrics", start, end), ResultTtl, ProductAnalysisCache.ResultWeight, async () =>
 050            ProductAnalysisReportCalculator.Metrics((await RowsAsync(new RowsKey(start, end, null, null, false), Shared)
 51    }
 52
 53    public Task<ComparativeResult> ComparativeAsync(int monthsBack, string? productKey, CancellationToken ct)
 54    {
 055        monthsBack = Math.Clamp(monthsBack, 1, 24);
 056        var key = string.IsNullOrWhiteSpace(productKey) ? null : productKey.Trim();
 057        var now = DateTime.Now;
 058        return CachedAsync(("pa.comparative", monthsBack, key, now.Date), ResultTtl, ProductAnalysisCache.ResultWeight, 
 059        {
 060            var (_, currentEnd, previousStart, _) = ProductAnalysisReportCalculator.ComparativeRanges(now, monthsBack);
 061            var rows = await RowsAsync(new RowsKey(previousStart, currentEnd, key, null, true), Shared);
 062            return ProductAnalysisReportCalculator.Comparative(rows.ToList(), now, monthsBack, key);
 063        }, ct);
 64    }
 65
 66    public Task<SeasonalityResult> SeasonalityAsync(int? appStatusId, string? productKey, DateTime? start, DateTime? end
 67    {
 068        var key = string.IsNullOrWhiteSpace(productKey) ? null : productKey.Trim();
 069        var status = appStatusId is > 0 ? appStatusId : null;
 70        // Конец периода включает следующий день — как и прежде.
 071        var endExclusive = end?.AddDays(1);
 072        return CachedAsync(("pa.seasonality", status, key, start, end), ResultTtl, ProductAnalysisCache.ResultWeight, as
 073            ProductAnalysisReportCalculator.Seasonality((await RowsAsync(new RowsKey(start, endExclusive, key, status, t
 74    }
 75
 76    public Task<OrderStatusesResult> OrderStatusesAsync(string? statusType, string? productKey, DateTime? start, DateTim
 77    {
 078        var type = string.Equals(statusType, "client", StringComparison.OrdinalIgnoreCase) ? "client" : "app";
 079        var key = string.IsNullOrWhiteSpace(productKey) ? null : productKey.Trim();
 080        var endExclusive = end?.AddDays(1);
 081        return CachedAsync(("pa.statuses", type, key, start, end), ResultTtl, ProductAnalysisCache.ResultWeight, async (
 082            ProductAnalysisReportCalculator.OrderStatuses((await RowsAsync(new RowsKey(start, endExclusive, key, null, t
 83    }
 84
 85    public Task<WarehouseRegionResult> WarehouseRegionAsync(DateTime? start, DateTime? end, string? productKey, Cancella
 86    {
 087        (start, end) = (ProductAnalysisCache.Canon(start), ProductAnalysisCache.Canon(end));
 088        var key = string.IsNullOrWhiteSpace(productKey) ? null : productKey.Trim();
 089        return CachedAsync(("pa.warehouse-region", start, end, key), ResultTtl, ProductAnalysisCache.ResultWeight, async
 090            ProductAnalysisReportCalculator.WarehouseRegion((await RowsAsync(new RowsKey(start, end, key, null, false, t
 91    }
 92
 93    public Task<DTOs.ForecastTrendAnalysisDto> ForecastAsync(string? productKey, DateTime? start, DateTime? end, int for
 94    {
 095        (start, end) = (ProductAnalysisCache.Canon(start), ProductAnalysisCache.Canon(end));
 096        var key = string.IsNullOrWhiteSpace(productKey) ? null : productKey.Trim();
 097        var days = Math.Clamp(forecastDays, 7, 90);
 098        return CachedAsync(("pa.forecast", key, start, end, days), ResultTtl, ProductAnalysisCache.ResultWeight, async (
 099        {
 0100            var rows = await RowsAsync(new RowsKey(start, end, key, null, false), Shared);
 0101            return ForecastDtoMapper.Build(AnalyticsRowProjection.ToOrders(rows), days, forecast, trends, purchases);
 0102        }, ct);
 103    }
 104
 105    public Task<SummaryResult> SummaryAsync(DateTime? start, DateTime? end, CancellationToken ct)
 106    {
 0107        (start, end) = (ProductAnalysisCache.Canon(start), ProductAnalysisCache.Canon(end));
 0108        return CachedAsync(("pa.summary", start, end), ResultTtl, ProductAnalysisCache.ResultWeight, async () =>
 0109            ProductAnalysisReportCalculator.Summary((await RowsAsync(new RowsKey(start, end, null, null, false), Shared)
 110    }
 111}

Methods/Properties

.ctor(Services.DataServices.OrdersDataServices,OzonOrdersWeb.Areas.Studio2.AnalysisServices.ProductAnalysisCache,OzonOrdersWeb.Areas.Studio2.AnalysisServices.SalesForecastService,OzonOrdersWeb.Areas.Studio2.AnalysisServices.TrendAnalysisService,OzonOrdersWeb.Areas.Studio2.AnalysisServices.PurchaseRecommendationService)
.cctor()
get_Start()
CachedAsync(System.Object,System.TimeSpan,System.Int32,System.Func`1<System.Threading.Tasks.Task`1<T>>,System.Threading.CancellationToken)
RowsAsync(OzonOrdersWeb.Areas.Studio2.AnalysisServices.ProductAnalysisReportService/RowsKey,System.Threading.CancellationToken)
OverallAsync(System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.Threading.CancellationToken)
MetricsAsync(System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.Threading.CancellationToken)
ComparativeAsync(System.Int32,System.String,System.Threading.CancellationToken)
SeasonalityAsync(System.Nullable`1<System.Int32>,System.String,System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.Threading.CancellationToken)
OrderStatusesAsync(System.String,System.String,System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.Threading.CancellationToken)
WarehouseRegionAsync(System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.String,System.Threading.CancellationToken)
ForecastAsync(System.String,System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.Int32,System.Threading.CancellationToken)
SummaryAsync(System.Nullable`1<System.DateTime>,System.Nullable`1<System.DateTime>,System.Threading.CancellationToken)