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