# Спринт 5 — оптовые продажи, email, аудит, надёжность import-job Автономная работа. После каждого пункта: `dotnet build` (SDK 8.0.126), unit + integration тесты, коммит порцией, отметка `[x]`, коммит прогресса. Multi-tenant: все новые сущности — `TenantEntity` с `OrganizationId` + query filter. E2E на изоляцию A vs B где применимо. ## Чек-лист 1. [x] **P1-5 Оптовая отгрузка (Demand)** — Domain `Demand`+`DemandLine` (CounterpartyId юрлица, способ оплаты нал/безнал, цена опт., НДС). EF + миграция. Контроллер CRUD + Post/Unpost. Web `/sales/demands`. `StockMovement` тип `WholesaleSale`. Multi-tenant. Тесты. ✅ Зеркалит RetailSale без RetailPoint/Cashier; `DemandPayment.Credit` (постоплата/дебиторка), `PaidAmount` для отслеживания. Permissions переиспользуют существующие `DemandsEdit/Post`. Метрики `documents_posted{type="demand"}`. 3 интеграционных теста. 2. [x] **P1-18 Аудит мутаций tenant'а** — Domain `OrgAuditLog` (как `SuperAdminAuditLog`, но per-org). Hook через EF SaveChangesInterceptor на Supply/Sale/Demand/Product/Counterparty. UI: `/audit-log` для админа. Multi-tenant строго. Тесты. ✅ `OrgAuditInterceptor` снимает diff на `SavingChanges` (до commit) — атомарно с мутацией. ChangesJson: `{"field":{"before":X,"after":Y}}`. Белый список типов: Supply/SupplierReturn/RetailSale/Demand/Product/ ProductPrice/ProductBarcode/Counterparty. Web `/audit-log` с фильтрами и diff-viewer'ом. Tenant-isolation через query-filter. 3 интеграционных. 3. [x] **P1-22 Email-шаблоны** — расширить MailKit-сервис: `Resources/EmailTemplates/*.html`. Шаблоны: приглашение сотрудника с временным паролем (sendInvite=true), еженедельный summary владельцу (Hangfire recurring), low-stock alert (Hangfire daily). Тесты рендеринга. ✅ `IEmailSender.SendHtmlAsync` (multipart/alternative с plain fallback); `EmailTemplateRenderer` (mustache-light: `{{key}}` escape, `{{{raw}}}`, `{{#key}}…{{/key}}` условие); `EmailTemplates` загружает embedded `Resources/EmailTemplates/*.html` (Subject: первой строкой). Шаблоны: invite/weekly-summary/low-stock. Hangfire: weekly понедельник 07:00, low-stock ежедневно 08:00. 8 unit-тестов. 4. [x] **TD-5 ImportJobRegistry в БД** — сейчас in-memory `ConcurrentDictionary`, теряется при рестарте. Перевод на таблицу `ImportJobs` (Id, OrgId, Status, Progress, Total, StartedAt, FinishedAt, Errors JSON). Миграция. `MoySkladImportController` использует. Тесты. ✅ `Domain.Integrations.ImportJob` (TenantEntity); миграция `Phase8c_ImportJobs`. `ImportJobRegistry` теперь IServiceScopeFactory-backed: `Create` пишет строку немедленно, `SaveAsync` обновляет, `Get/RecentlyFinished` читают из БД. Контроллер `MoySkladImportController.RunInBackgroundAsync` дополнен periodic flush через Timer (каждые 2 сек) + финальный flush в finally — UI видит реальный прогресс, terminal-state сохраняется через рестарт. 3 интеграционных теста (survives across scope, RecentlyFinished, tenant-isolation). ## Итог **Все 4 пункта выполнены.** Спринт 5 завершён 2026-05-28. Сводка: - **P1-5 Demand** — оптовая отгрузка контрагенту-юрлицу с `DemandPayment.Credit` и `PaidAmount`-полем; 3 интеграционных. - **P1-18 OrgAuditLog** — per-tenant журнал через `SaveChangesInterceptor`, atomic с мутацией, diff в jsonb; 3 интеграционных. - **P1-22 Email** — `IEmailSender.SendHtmlAsync` (multipart) + mustache-light renderer + 3 шаблона + 2 recurring Hangfire-джоба; 8 unit. - **TD-5 ImportJob** — persistence в БД, переживает рестарт, periodic flush для live-прогресса; 3 интеграционных. **Сборка:** зелёная. **Тесты:** 35 unit + 68 integration = **103 зелёных**. ### Что осталось вне scope - ОФД-оператор (нужен внешний участник: `Транском`/`Касса24`); - MoySklad webhook-токены прод; - WPF/POS UI (Windows-SDK); - Стейдж→прод-деплой; - Real SMTP-сервер для прод (Mailgun/Sendgrid/etc). ## Лог - Каждый пункт: build + тесты + коммит порцией + отметка [x] + коммит прогресса. - Все правки на `main` (origin Forgejo), без коммита `global.json`.