9.1 KiB
Спринт 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).
Чек-лист
- 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 при минусе). - P1-2 Списание (Loss) — Domain
Loss+LossLine+ enumLossReason(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-изоляция). - 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. PermissionTransferEdit. Тесты: 4 интеграционных, ключевой проверяет что движений ровно 2 после Post и ровно 4 после Unpost (никаких orphan). - 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 интеграционных. - 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 интеграционных. - P1-7 Возврат поставщику (SupplierReturn) — по аналогии для Supply.
Domain
SupplierReturn+Line(referenceSupplyId). Контроллер. Web. ✅ Зеркалит Supply, но Post с -Quantity (типSupplierReturn). Валидация что reference указывает на проведённую приёмку того же поставщика. Защита от ухода в минус. Permissions переиспользуютSuppliesEdit/Post/Delete. Тесты: 4 интеграционных. - P1-16 Hangfire dashboard + cleanup —
Hangfire.Dashboardс авторизацией только для SuperAdmin. Scheduled: ежедневный cleanupStockMovementстарше 2 лет, audit-log старше 90 дней. ✅Hangfire.PostgreSqlstorage на 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.PostgreSqlstorage, dashboard/hangfireсSuperAdminHangfireFilter, recurring jobsprune-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.