food-market/docs/sprint11-progress.md
nns 0d3ef81f72 feat(s11): ОФД-scaffolding — IFiscalProvider + 4 провайдера + UI/тесты
Sprint 11 — каркас для интеграции с операторами фискальных данных РК.
Реальные ApiKey'и появятся у user'а позже; задача — построить такой
фрейм, чтобы подключение оператора сводилось к вписыванию кредов в UI
без правок кода/деплоя.

Что сделано:
- IFiscalProvider (Application/Common/Fiscal) + FiscalResult,
  FiscalProviderKind (None/Mock/Webkassa/Kassa24/OfdSolo),
  IFiscalProviderFactory, FiscalNotConfiguredException.
- 4 реализации в Infrastructure/Fiscal:
  • MockFiscalProvider — фейк MOCK-<8hex> через 300мс, идемпотентный
    по Sale.Id (используется dev/stage и интеграционными тестами);
  • WebkassaProvider — полный HTTP-pipeline Authorize→Check, парсинг
    JSON-ответа, NDS-в-ставке, retry-safe через ExternalCheckNumber;
  • Kassa24Provider / OfdSoloProvider — скелет с тем же контрактом,
    RegisterAsync бросает FiscalNotConfiguredException (нужны
    спецификации API от user'а, NDA-only).
- Миграция Phase11a: 5 колонок в retail_sales (FiscalNumber, QrCode,
  Url, ProviderTxId, ProviderKind) + 5 в organizations (FiscalProvider
  NOT NULL default 0, ApiKey/Secret encrypted, CashboxUniqueNumber,
  ApiBaseUrl). Default 0 = обратная совместимость, существующие чеки
  и продажи без фискализации работают как раньше.
- RetailSalesController.Post — TryFiscalizeAsync после commit'а
  stock-транзакции. Best-effort: сетевые/HTTP-ошибки логируются, чек
  остаётся проведённым. Идемпотентность по IsNullOrEmpty(FiscalNumber).
- OrgFiscalSettingsController: GET/PUT настройки + GET /providers
  (опции для select'а) + POST /test-send (фейк-чек к выбранному
  провайдеру, не сохраняет в БД).
- UI: FiscalSection в OrganizationSettingsPage с password-input'ами
  для ApiKey/Secret (шифруются DataProtection.purpose=foodmarket.fiscal,
  в GET — только has-* флаги), спец-значение "__clear__" для снятия,
  кнопка «Тестовая отправка».
- Тесты: 11 unit (Mock 5 + Webkassa payload 6) + 3 integration
  (Mock сохраняет FiscalNumber, test-send даёт MOCK-номер, None
  не фискализует).
- docs/ofd-integration.md — гид с архитектурой, шагами подключения
  Webkassa (полный pap), TODO для Касса24/ОФД-Соло, безопасностью
  кредов, retry-сценариями.

Все 68 unit + 8 integration в Fiscal/Loyalty/RetailOversell — зелёные.
Web vite build — зелёный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:27:17 +05:00

99 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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