food-market/docs/sprint2-progress.md
nns a7b82eea86 docs(sprint2): P1-16 done — все 7 пунктов выполнены, итог
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:07:53 +05:00

120 lines
9.1 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.

# Спринт 2 — складские документы (P1)
Автономная работа. После каждого пункта: `dotnet build` (SDK 8.0.126),
unit + integration + (где применимо) E2E тесты этого пункта, коммит порцией,
отметка `[x]` здесь, коммит прогресса.
Multi-tenant: все новые сущности — `TenantEntity` с `OrganizationId` +
query filter. Stock-инвариант: после каждого Post/Unpost
`Stock.Quantity ≡ Σ StockMovement` для (Product, Store).
## Чек-лист
1. [x] **P1-1 Оприходование (Enter)** — Domain `Enter`+`EnterLine`, EF, миграция,
контроллер CRUD + Post/Unpost (Stock + StockMovement тип `Enter`), Web
`/inventory/enters`. Без поставщика (источник — начальные остатки, излишек инвентаризации).
✅ Контроллер `api/inventory/enters`; миграция `Phase6a_Enters`; пункт «Оприходования»
в сайдбаре Admin/Storekeeper. Тесты: 4 интеграционных (post raise stock, unpost
откатывает, double post→409, tenant-изоляция, блокировка unpost при минусе).
2. [x] **P1-2 Списание (Loss)** — Domain `Loss`+`LossLine` + enum `LossReason`
(Defect/Expired/Damage/Shortage/Other). EF, миграция, контроллер, Web,
`StockMovement` тип `WriteOff`.
✅ Контроллер `api/inventory/losses` (CRUD + Post/Unpost) с проверкой
«не списать сверх остатка» (409). Миграция `Phase6b_Losses`. Web с
фильтром по причине и колонкой stockAtStore. Тесты: 3 интеграционных
(post снижает stock, over-write-off → 409, tenant-изоляция).
3. [x] **P1-3 Перемещение (Transfer)** — Domain `Transfer`+`TransferLine`
(FromStoreId → ToStoreId, обязательны и различны). Атомарная транзакция:
`TransferOut` из From + `TransferIn` в To. EF, миграция, контроллер + Post/Unpost,
Web. Кейс: post→unpost не оставляет orphan-движений.
✅ Пара движений (Out + In) в одной Serializable-транзакции; обратная пара
в Unpost. Проверка «not short» на FromStore при Post и ToStore при Unpost.
Permission `TransferEdit`. Тесты: 4 интеграционных, ключевой проверяет что
движений ровно 2 после Post и ровно 4 после Unpost (никаких orphan).
4. [x] **P1-4 Инвентаризация (Inventory)** — Domain `Inventory`+`InventoryLine`
(productId, bookQty, actualQty, diff). EF, миграция. Контроллер: создание
подгружает текущие остатки; Post создаёт `InventoryAdjustment` на diff. Web:
форма со списком товаров склада, импорт CSV факта.
✅ Доменная сущность `InventoryDoc` (имя чтобы не пересекаться с системным
неймспейсом). Create с пустыми lines подтягивает все товары склада;
Update пишет actualQty построчно. Post создаёт `InventoryAdjustment`
только по строкам с diff != 0 (400 если нет расхождений). Unpost блочит
при «излишек уже расходован». Web с CSV-импортом (productId|article;qty).
Тесты: 3 интеграционных.
5. [x] **P1-6 Возврат от покупателя (CustomerReturn)** — расширение `RetailSale`
опцией возврата (referenceSaleId или без). Контроллер: создание возврата из
проведённой продажи, `CustomerReturn` тип уже есть. Web: кнопка «Создать возврат».
✅ RetailSale.IsReturn + ReferenceSaleId; RetailSaleLine.QtyReturned
(агрегация для защиты от over-return). `POST /create-return` копирует
проведённый чек в Draft-возврат с qty = (Quantity - QtyReturned).
Post return через `CustomerReturn`-движение с +Quantity, инкрементит
QtyReturned на исходных строках. Запрещён unpost оригинала при активных
возвратах. Тесты: 3 интеграционных.
6. [x] **P1-7 Возврат поставщику (SupplierReturn)** — по аналогии для Supply.
Domain `SupplierReturn`+`Line` (referenceSupplyId). Контроллер. Web.
✅ Зеркалит Supply, но Post с -Quantity (тип `SupplierReturn`). Валидация
что reference указывает на проведённую приёмку того же поставщика. Защита
от ухода в минус. Permissions переиспользуют `SuppliesEdit/Post/Delete`.
Тесты: 4 интеграционных.
7. [x] **P1-16 Hangfire dashboard + cleanup**`Hangfire.Dashboard` с
авторизацией только для SuperAdmin. Scheduled: ежедневный cleanup
`StockMovement` старше 2 лет, audit-log старше 90 дней.
`Hangfire.PostgreSql` storage на ConnectionStrings:Default. Сервер
стартует только если `Hangfire:Enabled=true` (по умолчанию). Dashboard
`/hangfire` гейтит `SuperAdminHangfireFilter`. Recurring: `prune-stock-movements`
(03:30 UTC, 730 дней) и `prune-audit-log` (03:45 UTC, 90 дней) —
`HousekeepingJobs` с `IgnoreQueryFilters` (межтенантно). Тесты:
1 unit + 1 интеграционный.
## Итог
**Все 7 пунктов выполнены.** Спринт 2 (складские документы P1) завершён 2026-05-28.
Сводка:
- **P1-1 Enter** — оприходование без поставщика (`/api/inventory/enters`, миграция
`Phase6a_Enters`); 4 интеграционных теста.
- **P1-2 Loss** — списание с enum `LossReason` (`/api/inventory/losses`,
`Phase6b_Losses`); 3 интеграционных.
- **P1-3 Transfer** — атомарное перемещение пара TransferOut + TransferIn
(`/api/inventory/transfers`, `Phase6c_Transfers`); 4 интеграционных, включая
проверку «после Post ровно 2 движения, после Unpost ровно 4».
- **P1-4 Inventory** — пересчёт с auto-load остатков (`/api/inventory/inventories`,
`Phase6d_Inventories`); 3 интеграционных, импорт CSV в UI.
- **P1-6 CustomerReturn** — `RetailSale.IsReturn` + `ReferenceSaleId` +
`RetailSaleLine.QtyReturned`, эндпоинт `POST /create-return`
(`Phase6e_RetailSaleReturns`); 3 интеграционных.
- **P1-7 SupplierReturn** — зеркало Supply (`/api/purchases/supplier-returns`,
`Phase6f_SupplierReturns`); 4 интеграционных, валидация совпадения поставщика
при ссылке на приёмку.
- **P1-16 Hangfire** — `Hangfire.PostgreSql` storage, dashboard `/hangfire`
с `SuperAdminHangfireFilter`, recurring jobs `prune-stock-movements` (730 дней)
и `prune-audit-log` (90 дней); 1 unit + 1 интеграционный.
**Сборка:** зелёная (`dotnet build src/food-market.api`).
**Тесты:** 24 unit + 32 integration = **56 зелёных**.
**Web:** `pnpm build` зелёный (5 новых пар list+edit страниц + расширение RetailSale).
### Новые таблицы
`enters`, `enter_lines`, `losses`, `loss_lines`, `transfers`, `transfer_lines`,
`inventories`, `inventory_lines`, `supplier_returns`, `supplier_return_lines`
+ колонки `retail_sales.IsReturn/ReferenceSaleId`, `retail_sale_lines.QtyReturned`.
### Новые permissions
`TransferEdit` добавлен в `RolePermissions` (Enter/Loss/Inventory/Supplies* —
переиспользованы существующие). `All()` обновлён.
### Stock-инвариант
Каждый документ при Post создаёт явные `StockMovement` через `IStockService`
в Serializable-транзакции. Post→Unpost — обратные движения тем же документ-id
(reversal-маркер в `DocumentType`). Проверка «не уйти в минус» на:
- Enter Unpost (товар уже мог быть продан),
- Loss Post (нельзя списать сверх остатка),
- Transfer Post (FromStore) и Unpost (ToStore),
- Inventory Unpost (излишек мог уйти),
- SupplierReturn Post (нельзя вернуть сверх остатка).
## Лог
- Каждый пункт: build + тесты + коммит порцией + отметка [x] + коммит прогресса.
- Все правки на ветке `main` (origin Forgejo), без коммита `global.json`.