# Sprint 11 — ОФД-scaffolding (фискализация РК)

Цель: построить фрейм для интеграции с операторами фискальных данных
Казахстана (Webkassa / Касса24 / ОФД-Соло), чтобы как только пользователь
получит реальный ApiKey — провайдер «оживал» одной настройкой в UI,
без правок кода/деплоя. Реальные аккаунты у user'а пока нет; задача
этого спринта — каркас + Mock + один полностью описанный провайдер
(Webkassa) с тестами на HTTP-контракт.

Старт: 2026-06-07. Исполнитель: Claude Opus 4.7 (автономный режим).

## Принципы

- Multi-tenant обязателен: настройка ОФД хранится на уровне
  Organization (как SMTP — но per-tenant, не глобально). API-ключи
  шифруются через DataProtection (purpose=`foodmarket.fiscal`).
- Поведение «по умолчанию» (`Fiscal:Provider=None` или не задано) —
  ровно как до спринта: RetailSale.Post не зовёт никакого провайдера,
  FiscalNumber остаётся пустым. Это даёт обратную совместимость.
- Каждый пункт: build + локальные тесты + `~/deploy-stage.sh` + retest.
- НЕ трогать: `global.json`, прод-стек, POS WPF.

## Чек-лист

- [x] **1. IFiscalProvider абстракция** — `Application/Common/Fiscal/IFiscalProvider.cs`
  с `FiscalResult`, `FiscalProviderKind` (None/Mock/Webkassa/Kassa24/OfdSolo),
  `IFiscalProviderFactory`, `FiscalNotConfiguredException`,
  `FiscalProviderException`. Миграция `Phase11a_FiscalScaffolding`
  добавляет 5 колонок в `retail_sales` (FiscalNumber, FiscalQrCode,
  FiscalUrl, FiscalProviderTxId, FiscalProviderKind) и 5 в `organizations`
  (FiscalProvider, FiscalApiKeyEncrypted, FiscalApiSecretEncrypted,
  FiscalCashboxUniqueNumber, FiscalApiBaseUrl). `FiscalProvider` NOT NULL
  с default 0 (None) — обратная совместимость. `RetailSalesController.Post`
  получил `TryFiscalizeAsync` (best-effort после commit'а stock-tx,
  идемпотентность по `IsNullOrEmpty(FiscalNumber)`).
- [x] **2. MockFiscalProvider** — `Infrastructure/Fiscal/MockFiscalProvider.cs`,
  имитация 300мс задержки, детерминированный фейк `MOCK-<8hex>` от
  `Sale.Id`. 5 unit-тестов (контракт + идемпотентность + latency) +
  integration-тест `FiscalMockFlowTests` (3 сценария: Mock даёт FiscalNumber,
  test-send отвечает OK, None не фискализует).
- [x] **3. WebkassaProvider skeleton** — `Infrastructure/Fiscal/WebkassaProvider.cs`,
  полный HTTP-flow `Authorize → Check`, парсинг JSON-ответа Webkassa.
  Token берётся каждым вызовом (TTL-кеш — следующий спринт).
  `BuildCheckPayload` public для тестируемости. 6 unit-тестов на маппинг
  (positions, payments, ndsв-ставке, returns, mixed/cash fallback,
  JSON camelCase). Без реального ApiKey/CashboxNumber бросает
  `FiscalNotConfiguredException` с подсказкой «заполните в настройках».
- [x] **4. Kassa24Provider skeleton** — заготовка с тем же контрактом,
  RegisterAsync бросает `FiscalNotConfiguredException` («интеграция ещё
  не реализована, нужны спецификации API»). Подробности — в docs.
- [x] **5. OfdSoloProvider skeleton** — аналогично.
- [x] **6. UI: настройка ОФД-провайдера** — секция `FiscalSection` в
  `OrganizationSettingsPage.tsx` + backend `OrgFiscalSettingsController`
  (`GET/PUT /api/organization/fiscal`, `GET /providers` со списком
  опций, `POST /test-send`). Поля ApiKey/ApiSecret — password-input,
  шифруются на сервере; в GET возвращаются только has-* флаги.
  Спец-значение `"__clear__"` — снять креды. Кнопка «Тестовая отправка»
  вызывает провайдера на фейк-чеке (не сохраняет в БД), показывает
  FiscalNumber или сообщение об ошибке.
- [x] **7. docs/ofd-integration.md** — гид «как подключить оператора»
  (Webkassa — полный pap, Касса24/ОФД-Соло — TODO для будущих спринтов,
  безопасность кредов, поведение на retry/network failure).

## Журнал

### 2026-06-07 старт
Sprint 10 закрыт (4/4 ✓). Поехали по ОФД-чек-листу.

### 2026-06-07 п.1–п.5 (абстракция + 4 провайдера)
`IFiscalProvider` + `FiscalProviderFactory` + 4 реализации (Mock полная,
3 оператора скелет с осмысленным `FiscalNotConfiguredException`).
Миграция Phase11a добавила 10 колонок в `retail_sales` + `organizations`.
`RetailSalesController.Post` — `TryFiscalizeAsync` после commit'а.
Тесты: 11 unit (Mock + Webkassa payload) + 3 integration. Все зелёные.

### 2026-06-07 п.6 (UI)
`OrgFiscalSettingsController` (5 endpoints) + `FiscalSection` в
существующей странице OrganizationSettings. UI прячет поля кредов
для провайдеров None/Mock, показывает их для трёх реальных операторов.
Тестовая отправка работает с любым провайдером — для скелет-операторов
вернёт «не реализовано», для Mock — настоящий MOCK-номер.

### 2026-06-07 п.7 (docs)
`docs/ofd-integration.md` — архитектура, поведение по умолчанию, шаги
подключения каждого оператора, безопасность, retry-сценарии.

### Итог
Все 7 пунктов ✓. Suite-тесты:
- 68/68 unit (включая 11 новых для Fiscal).
- 8/8 integration (Fiscal + Loyalty + RetailOversell в одной группе).
- Web `vite build` зелёный, TS — без ошибок.

API готов: пользователь заводит аккаунт у любого оператора, вписывает
ApiKey/Secret/CashboxNumber в «Настройки организации → ОФД», нажимает
«Тестовая отправка» — если оператор отвечает, следующий проведённый
чек получит фискальный номер автоматически. Для Webkassa полный
HTTP-pipeline реализован; для Касса24/ОФД-Соло нужны спецификации API
от user'а (NDA-only).
