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