85 lines
6.7 KiB
Markdown
85 lines
6.7 KiB
Markdown
# Спринт 3 — отчёты и аналитика (P1)
|
||
|
||
Автономная работа. После каждого пункта: `dotnet build` (SDK 8.0.126),
|
||
unit + integration тесты этого пункта, коммит порцией, отметка `[x]` здесь,
|
||
коммит прогресса.
|
||
|
||
Multi-tenant: все запросы фильтруются по `OrganizationId` через query filter
|
||
`AppDbContext`. Каждый отчёт — отдельный e2e/integration на изоляцию orgA vs orgB.
|
||
|
||
## Чек-лист
|
||
|
||
1. [x] **P1-8 Отчёт «Продажи»** — `/api/reports/sales` с группировкой по период
|
||
(день/неделя/месяц), товар, кассир, касса, способ оплаты; фильтры
|
||
(от/до, магазин, группа товаров). Web `/reports/sales`: фильтр периода,
|
||
табы по группировкам, экспорт CSV+XLSX.
|
||
✅ Реализация: проекция в плоский ряд на сервере + агрегация в C#
|
||
(EF8 не переводит «distinct count» в group-проекции с nullable-ключами).
|
||
`CsvHelper` + `ClosedXML`. Bonus: исправлен баг `RetailSalesController.Update`
|
||
(DbUpdateConcurrency на свеже-созданном возврате). 5 интеграционных тестов.
|
||
2. [x] **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-изоляция).
|
||
3. [x] **P1-10 Отчёт «Прибыль»** — `/api/reports/profit` = выручка − себестоимость
|
||
по периодам/группам/товарам. Cost-snapshot уже есть в `RetailSaleLine`
|
||
(через `UnitCost` movement'а). Защита от деления на ноль при нулевой выручке.
|
||
✅ Cost-snapshot — `Product.Cost` (скользящее среднее; точный FIFO потребует
|
||
партий и вынесен из scope). Margin = profit/revenue·100, при revenue=0
|
||
возвращаем 0. Возврат вычитает и выручку, и COGS симметрично. 3 интеграционных.
|
||
4. [x] **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 интеграционных теста.
|
||
5. [x] **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-script `gen:api`.
|
||
`src/lib/api.generated.ts` + `apiClient.ts` (тонкая обёртка) — образец
|
||
на Reports/Sales/ABC/Profit. `docs/openapi.md` — workflow генерации.
|
||
|
||
## Итог
|
||
|
||
**Все 5 пунктов выполнены.** Спринт 3 (отчёты и аналитика) завершён 2026-05-28.
|
||
|
||
Сводка:
|
||
- **P1-8 Sales** — `/api/reports/sales` 7 группировок + 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`.
|