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

9.1 KiB
Raw Permalink Blame History

Спринт 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. 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. 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. 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. 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. 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. P1-7 Возврат поставщику (SupplierReturn) — по аналогии для Supply. Domain SupplierReturn+Line (referenceSupplyId). Контроллер. Web. Зеркалит Supply, но Post с -Quantity (тип SupplierReturn). Валидация что reference указывает на проведённую приёмку того же поставщика. Защита от ухода в минус. Permissions переиспользуют SuppliesEdit/Post/Delete. Тесты: 4 интеграционных.
  7. P1-16 Hangfire dashboard + cleanupHangfire.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 CustomerReturnRetailSale.IsReturn + ReferenceSaleId + RetailSaleLine.QtyReturned, эндпоинт POST /create-return (Phase6e_RetailSaleReturns); 3 интеграционных.
  • P1-7 SupplierReturn — зеркало Supply (/api/purchases/supplier-returns, Phase6f_SupplierReturns); 4 интеграционных, валидация совпадения поставщика при ссылке на приёмку.
  • P1-16 HangfireHangfire.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.