using System.Globalization;
using MediatR;
namespace foodmarket.Application.Sales.Queries;
/// CQRS-образец №1 (TD-1): чистая логика агрегации продаж в C#
/// (без БД), пригодная для unit-тестирования без TestContainers. Сам
/// контроллер `/api/reports/sales` остаётся как было (pull-фласк +
/// группировка в C#) — этот handler — refactor-демонстрация на пути к
/// CQRS-разделению.
///
/// Принимает уже подготовленные «плоские строки» (FlatSale) и параметры
/// группировки/окна, возвращает агрегированный список. Идемпотентен,
/// safe для параллельного вызова.
public record GetSalesReportQuery(
IReadOnlyList Lines,
string GroupBy = "period:day",
DateTime? From = null,
DateTime? To = null) : IRequest>;
public record FlatSale(
Guid SaleId, DateTime Date,
Guid ProductId, string ProductName,
decimal Revenue,
decimal Quantity);
public record SalesReportRow(
string Key, string Label,
decimal Revenue, int Transactions, decimal Quantity);
public sealed class GetSalesReportHandler : IRequestHandler>
{
public Task> Handle(GetSalesReportQuery req, CancellationToken ct)
{
var filtered = req.Lines
.Where(x => req.From is null || x.Date >= req.From)
.Where(x => req.To is null || x.Date <= req.To)
.ToList();
IEnumerable> grouped = req.GroupBy switch
{
"period:day" => filtered.GroupBy(x => (
Key: x.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
Label: x.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))),
"period:month" => filtered.GroupBy(x => (
Key: $"{x.Date.Year:0000}-{x.Date.Month:00}",
Label: $"{x.Date.Year:0000}-{x.Date.Month:00}")),
"product" => filtered.GroupBy(x => (Key: x.ProductId.ToString(), Label: x.ProductName)),
_ => Enumerable.Empty>(),
};
IReadOnlyList rows = grouped
.Select(g => new SalesReportRow(
g.Key.Key, g.Key.Label,
g.Sum(x => x.Revenue),
g.Select(x => x.SaleId).Distinct().Count(),
g.Sum(x => x.Quantity)))
.OrderByDescending(r => r.Revenue)
.ToList();
return Task.FromResult(rows);
}
}