# 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).