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

6.7 KiB
Raw Blame History

Спринт 3 — отчёты и аналитика (P1)

Автономная работа. После каждого пункта: dotnet build (SDK 8.0.126), unit + integration тесты этого пункта, коммит порцией, отметка [x] здесь, коммит прогресса.

Multi-tenant: все запросы фильтруются по OrganizationId через query filter AppDbContext. Каждый отчёт — отдельный e2e/integration на изоляцию orgA vs orgB.

Чек-лист

  1. P1-8 Отчёт «Продажи»/api/reports/sales с группировкой по период (день/неделя/месяц), товар, кассир, касса, способ оплаты; фильтры (от/до, магазин, группа товаров). Web /reports/sales: фильтр периода, табы по группировкам, экспорт CSV+XLSX. Реализация: проекция в плоский ряд на сервере + агрегация в C# (EF8 не переводит «distinct count» в group-проекции с nullable-ключами). CsvHelper + ClosedXML. Bonus: исправлен баг RetailSalesController.Update (DbUpdateConcurrency на свеже-созданном возврате). 5 интеграционных тестов.
  2. 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. 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. 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. P1-19 OpenAPI / SwaggerSwashbuckle.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.