6.7 KiB
Спринт 3 — отчёты и аналитика (P1)
Автономная работа. После каждого пункта: dotnet build (SDK 8.0.126),
unit + integration тесты этого пункта, коммит порцией, отметка [x] здесь,
коммит прогресса.
Multi-tenant: все запросы фильтруются по OrganizationId через query filter
AppDbContext. Каждый отчёт — отдельный e2e/integration на изоляцию orgA vs orgB.
Чек-лист
- P1-8 Отчёт «Продажи» —
/api/reports/salesс группировкой по период (день/неделя/месяц), товар, кассир, касса, способ оплаты; фильтры (от/до, магазин, группа товаров). Web/reports/sales: фильтр периода, табы по группировкам, экспорт CSV+XLSX. ✅ Реализация: проекция в плоский ряд на сервере + агрегация в C# (EF8 не переводит «distinct count» в group-проекции с nullable-ключами).CsvHelper+ClosedXML. Bonus: исправлен багRetailSalesController.Update(DbUpdateConcurrency на свеже-созданном возврате). 5 интеграционных тестов. - P1-9 Отчёт «Остатки на дату» —
/api/reports/stockвосстанавливает остатки на произвольную дату через журналStockMovement(Σ движений до даты по продукту). Web/reports/stock: выбор даты, фильтр магазин/группа, экспорт. Edge: дата в будущем, дата раньше первой операции. ✅ Реконструкция через ΣStockMovement.QuantityгдеOccurredAt ≤ date. Стоимость — последнийUnitCostдвижения до даты + fallback наProduct.Cost. Edge'ы покрыты тестами: 5 интеграционных (today=current, before-first→empty, future=current, future-supply исключается на «сегодня», tenant-изоляция). - P1-10 Отчёт «Прибыль» —
/api/reports/profit= выручка − себестоимость по периодам/группам/товарам. Cost-snapshot уже есть вRetailSaleLine(черезUnitCostmovement'а). Защита от деления на ноль при нулевой выручке. ✅ Cost-snapshot —Product.Cost(скользящее среднее; точный FIFO потребует партий и вынесен из scope). Margin = profit/revenue·100, при revenue=0 возвращаем 0. Возврат вычитает и выручку, и COGS симметрично. 3 интеграционных. - P1-11 Отчёт «ABC-анализ» — топ товаров по выручке за период,
классы A/B/C по Парето (A=80%, B=15%, C=5% накопительной выручки).
Параметр метрики (выручка/прибыль/маржа). Web с визуализацией.
✅
GET /api/reports/abc?metric=revenue|profit|margin. Граница A —cumulativeShare ≤ 80, B —≤ 95, C — остальное. Товары с неположительной метрикой исключаются. Web: цветные плашки класса + полоса накопительной доли. 4 интеграционных теста. - P1-19 OpenAPI / Swagger —
Swashbuckle.AspNetCore,/swagger/v1/swagger.jsonв Development. Сгенерировать TS-клиент для food-market.web (openapi-typescript/nswag) и подключить для пары контроллеров как образец. ✅ SwaggerGen с Bearer security-scheme + стабильные operationId (Controller_VerbAction — verb включён против коллизии WipeAll/WipeAllAsync)- уникальные schemaId с namespace-префиксом. UI только в Development.
openapi-typescriptкак devDependency + npm-scriptgen:api.src/lib/api.generated.ts+apiClient.ts(тонкая обёртка) — образец на Reports/Sales/ABC/Profit.docs/openapi.md— workflow генерации.
- уникальные schemaId с namespace-префиксом. UI только в Development.
Итог
Все 5 пунктов выполнены. Спринт 3 (отчёты и аналитика) завершён 2026-05-28.
Сводка:
- P1-8 Sales —
/api/reports/sales7 группировок + CSV/XLSX, 5 интеграционных. - P1-9 Stock — реконструкция через журнал движений, 5 интеграционных (вкл. edges).
- P1-10 Profit — netto с защитой от деления на ноль, 3 интеграционных.
- P1-11 ABC — Парето 80/15/5 + 3 метрики + цветная UI-визуализация, 4 интеграционных.
- P1-19 OpenAPI — Swagger + TS-клиент через openapi-typescript,
обёртка
apiClient.tsдля подсказки IDE.
Сборка: зелёная. Тесты: 24 unit + 49 integration (32 sprint1+2 + 17 sprint3) зелёные.
Web: pnpm build зелёный (4 новых report-страницы + boilerplate api.generated.ts).
Архитектурное замечание
Все отчётные контроллеры идут паттерном «плоский pull + group в C#» — EF8 не
переводит g.Select(...).Distinct().Count() в SQL для group-проекций с
nullable join-ключами (cashier, retail-point). Объёмы отчётов (~десятки
тыс. строк/месяц) держатся в RAM спокойно; на крупных тенантах перейдём
на raw SQL/views (вне scope этого спринта).
Bonus
Поймал и исправил баг RetailSalesController.Update: DbUpdateConcurrency
expected 1 row, affected 0 воспроизводился на возвратах сразу после
create-return. Лечение — ApplyLines добавляет строки напрямую в DbSet
(а не через nav-collection), Update не делает Include(Lines), старые
строки удаляются ExecuteDelete.
Лог
- Каждый пункт: build + тесты + коммит порцией + отметка [x] + коммит прогресса.
- Все правки на
main(origin Forgejo), без коммитаglobal.json.