| | | 1 | | using OzonOrdersWeb.Areas.Studio2.DTOs; |
| | | 2 | | |
| | | 3 | | namespace OzonOrdersWeb.Areas.Studio2.AnalysisServices; |
| | | 4 | | |
| | | 5 | | /// <summary>Детализация продукта (вкладка «Обзор»/«Прибыль») из узкой проекции заказов одного ProductKey.</summary> |
| | | 6 | | public static class ProductDetailBuilder |
| | | 7 | | { |
| | 1 | 8 | | private static readonly string[] RussianMonths = |
| | 1 | 9 | | ["", "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Д |
| | | 10 | | |
| | | 11 | | public static ProductDetailAnalysisDto? Build(string productKey, IReadOnlyCollection<AnalyticsOrderRow> orders) |
| | | 12 | | { |
| | 3 | 13 | | if (orders.Count == 0) return null; |
| | | 14 | | |
| | 2 | 15 | | var summary = ProductGroupCalculator.Build(orders).FirstOrDefault(g => g.ProductKey == productKey) |
| | 1 | 16 | | ?? ProductGroupCalculator.Build(orders).First(); |
| | 1 | 17 | | var normalizer = new ProductGroupCalculator.ProfitNormalizer(orders); |
| | | 18 | | |
| | 1 | 19 | | var clientBreakdown = orders |
| | 3 | 20 | | .Where(o => !string.IsNullOrEmpty(o.ClientName)) |
| | 3 | 21 | | .GroupBy(o => o.ClientName!) |
| | 1 | 22 | | .Select(g => |
| | 1 | 23 | | { |
| | 5 | 24 | | var amount = g.Sum(o => o.ShipmentAmount ?? 0); |
| | 2 | 25 | | return new ClientDetailDto |
| | 2 | 26 | | { |
| | 2 | 27 | | ClientName = g.Key, |
| | 2 | 28 | | TotalOrders = g.Count(), |
| | 3 | 29 | | TotalQuantity = g.Sum(o => o.Quantity ?? 0), |
| | 2 | 30 | | TotalShipmentAmount = amount, |
| | 2 | 31 | | AverageCheck = amount / g.Count() |
| | 2 | 32 | | }; |
| | 1 | 33 | | }) |
| | 2 | 34 | | .OrderByDescending(c => c.TotalShipmentAmount) |
| | 1 | 35 | | .ToList(); |
| | | 36 | | |
| | 1 | 37 | | var byMonth = orders |
| | 3 | 38 | | .Where(o => o.ProcessingDate.HasValue) |
| | 3 | 39 | | .GroupBy(o => (o.ProcessingDate!.Value.Year, o.ProcessingDate!.Value.Month)) |
| | 4 | 40 | | .OrderBy(g => g.Key.Year).ThenBy(g => g.Key.Month) |
| | 1 | 41 | | .ToList(); |
| | | 42 | | |
| | 1 | 43 | | var timeAnalysis = new List<TimeAnalysisDto>(); |
| | 1 | 44 | | decimal? prevAvgCheck = null; |
| | 6 | 45 | | foreach (var g in byMonth) |
| | | 46 | | { |
| | 5 | 47 | | var amount = g.Sum(o => o.ShipmentAmount ?? 0); |
| | 5 | 48 | | var qty = g.Sum(o => o.Quantity ?? 0); |
| | 2 | 49 | | var avgCheck = amount / g.Count(); |
| | 2 | 50 | | var trend = "stable"; |
| | 2 | 51 | | if (prevAvgCheck.HasValue) |
| | | 52 | | { |
| | 2 | 53 | | if (avgCheck > prevAvgCheck.Value * 1.1m) trend = "up"; |
| | 0 | 54 | | else if (avgCheck < prevAvgCheck.Value * 0.9m) trend = "down"; |
| | | 55 | | } |
| | | 56 | | |
| | 2 | 57 | | prevAvgCheck = avgCheck; |
| | 2 | 58 | | timeAnalysis.Add(new TimeAnalysisDto |
| | 2 | 59 | | { |
| | 2 | 60 | | Period = $"{RussianMonths[g.Key.Month]} {g.Key.Year}", |
| | 2 | 61 | | PeriodSort = g.Key.Year * 100 + g.Key.Month, |
| | 2 | 62 | | OrdersCount = g.Count(), |
| | 2 | 63 | | TotalQuantity = qty, |
| | 2 | 64 | | TotalAmount = amount, |
| | 2 | 65 | | AverageCheck = avgCheck, |
| | 2 | 66 | | AvgPrice = qty > 0 ? amount / qty : 0, |
| | 2 | 67 | | Trend = trend, |
| | 3 | 68 | | TopClients = g.Where(o => !string.IsNullOrEmpty(o.ClientName)) |
| | 3 | 69 | | .GroupBy(o => o.ClientName!) |
| | 6 | 70 | | .OrderByDescending(cg => cg.Count()).ThenBy(cg => cg.Key, StringComparer.Ordinal) |
| | 3 | 71 | | .Take(3).Select(cg => cg.Key).ToList() |
| | 2 | 72 | | }); |
| | | 73 | | } |
| | | 74 | | |
| | 1 | 75 | | var statusDistribution = orders |
| | 3 | 76 | | .Where(o => !string.IsNullOrEmpty(o.AppStatusName)) |
| | 3 | 77 | | .GroupBy(o => o.AppStatusName!) |
| | 3 | 78 | | .ToDictionary(g => g.Key, g => g.Count()); |
| | | 79 | | |
| | 1 | 80 | | var profitByMonth = byMonth |
| | 2 | 81 | | .Select(g => new { Period = $"{RussianMonths[g.Key.Month]} {g.Key.Year}", TotalProfit = g.Sum(normalizer.Nor |
| | 1 | 82 | | .ToList(); |
| | | 83 | | |
| | 1 | 84 | | var profitByClient = orders |
| | 3 | 85 | | .Where(o => !string.IsNullOrEmpty(o.ClientName)) |
| | 3 | 86 | | .GroupBy(o => o.ClientName!) |
| | 2 | 87 | | .Select(g => new { ClientName = g.Key, TotalProfit = g.Sum(normalizer.Normalize) }) |
| | 2 | 88 | | .OrderByDescending(x => x.TotalProfit) |
| | 1 | 89 | | .Take(10) |
| | 1 | 90 | | .ToList(); |
| | | 91 | | |
| | 1 | 92 | | var priceChart = orders |
| | 3 | 93 | | .Where(o => o.ProcessingDate.HasValue && o.Price.HasValue) |
| | 3 | 94 | | .OrderBy(o => o.ProcessingDate) |
| | 3 | 95 | | .Select(o => new PriceChartPointDto |
| | 3 | 96 | | { |
| | 3 | 97 | | Date = o.ProcessingDate!.Value, |
| | 3 | 98 | | Price = o.Price!.Value, |
| | 3 | 99 | | ShipmentAmount = o.ShipmentAmount, |
| | 3 | 100 | | Profit = ProductGroupCalculator.HasProfit(o) ? normalizer.Normalize(o) : null, |
| | 3 | 101 | | OrderId = o.Id, |
| | 3 | 102 | | ShipmentNumber = o.ShipmentNumber ?? string.Empty |
| | 3 | 103 | | }) |
| | 1 | 104 | | .ToList(); |
| | | 105 | | |
| | 1 | 106 | | return new ProductDetailAnalysisDto |
| | 1 | 107 | | { |
| | 1 | 108 | | ProductSummary = summary, |
| | 1 | 109 | | ClientBreakdown = clientBreakdown, |
| | 1 | 110 | | TimeAnalysis = timeAnalysis, |
| | 1 | 111 | | StatusDistribution = statusDistribution, |
| | 1 | 112 | | PriceChart = priceChart, |
| | 1 | 113 | | ProfitByMonth = profitByMonth, |
| | 1 | 114 | | ProfitByClient = profitByClient |
| | 1 | 115 | | }; |
| | | 116 | | } |
| | | 117 | | } |