food-market/docs/sprint3-progress.md
nns 879e6b8cee docs(sprint3): P1-19 done — все 5 пунктов выполнены, итог
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:40:01 +05:00

85 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Спринт 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`.