Skip to content

ТЗ — ОФД/фискализация (cms/ofd)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

Назначение и возможности

Коннекторы операторов фискальных данных (АТОЛ Онлайн, Эвотор, ОФД.ру) поверх cms/integrations-bus: принимает запросы на фискализацию чека от cms/commerce-receipts и доводит их до отправки в ККТ с контролем статуса.

  • Коннекторы АТОЛ Онлайн, Эвотор, ОФД.ру: provides: ofd-provider
  • Очередь фискализации — чек не отправляется синхронно из HTTP-запроса
  • Статусы чека (в очереди/отправлен/принят/ошибка) с журналом переходов
  • Повторная отправка при временной ошибке оператора (ретраи через cms/integrations-bus)
  • Мониторинг состояния подключённой ККТ (онлайн/офлайн/ошибка смены)
  • Fallback-провайдер: переключение на резервного оператора ОФД при недоступности основного

Зависимости и выключение

requires: cms/integrations-bus · suggests: cms/commerce-invoices · provides: ofd-provider (потребитель — cms/commerce-receipts)

Поведение при выключении: чеки ставятся в очередь «ожидает фискализации» и не отправляются, заказы оформляются штатно — фискализация требует ручного донабора после включения модуля.

Модель данных

ТаблицаКлючевые поляПримечание
cms_ofd_receipts_queueid, order_id, provider_code, status, payload (json), attemptsочередь фискализации с журналом попыток
cms_ofd_kkt_statusid, kkt_id, provider_code, status, last_checked_atмониторинг состояния подключённых ККТ

Индексы: FK order_idconstrained() + index(); status — PHP Enum; payloadjson()->nullable() + cast array; cms_ofd_receipts_queue — журнал, append-only по попыткам, BRIN по created_at; составной индекс status, created_at (выборка «в очереди дольше N минут» для алерта — см. «Производительность и кеш»).

ПДн-паспорт: cms_ofd_receipts_queue.payload может содержать состав корзины и ФИО/e-mail покупателя (при электронном чеке) — минимально необходимый набор для 54-ФЗ. Хранение — по регламенту оператора ОФД (фискальные документы хранятся дольше типового ретеншна контента); модуль не хранит ПДн дольше договора с оператором. Участие в хуках ядра «выгрузить всё по субъекту» / «забыть по запросу» (152-ФЗ) — ограниченное: фискальный номер и сумма сохраняются для бухгалтерского учёта (обязательное требование 54-ФЗ), обезличиваются только контактные поля (payload.customer_contact) — исключение из общего правила ядра фиксируется здесь явно, а не молчаливым игнором хука.

Входные и выходные данные

Принцип: всё, что не перечислено во входах ниже, модуль обязан отвергать — неизвестные поля payload, вебхуки без валидной подписи, provider_code, отсутствующий в реестре подключённых коннекторов.

ВходИсточникКаналПоляПроверка
Запрос фискализации чекаcms/commerce-receiptsканал 4 (requires), OfdService::enqueue()order_id, items[], amount, customer_contact?сумма сверяется с заказом (см. «Крайние случаи»), whitelist полей payload
Ручной ретрай чекаadmin UIAPI POST /queue/{id}/retryidofd.manage, Idempotency-Key (мутация с внешним эффектом)
Смена настроек (provider/fallback/kill-switch)adminAPI настроекключи группы ofdsettings.manage + ofd.manage
Вебхук статуса чека от оператора ОФДвнешний оператор (АТОЛ/Эвотор/ОФД.ру)cms/webhooks-incms/integrations-busorder_id, provider_code, fiscal_document_number, status, timestampподпись (HMAC, секрет из .env per-провайдер) обязательна; протухший timestamp (replay) отклоняется; неизвестный provider_code отклоняется
ВыходПолучательФормат
Ответ API очереди/статуса ККТadmin UI{data, meta}, keyset-пагинация
OfdReceiptSent / OfdReceiptFailed / OfdKktOfflineподписчики (канал 1)события ядра
Фискальный номер/ссылка на чекcms/commerce-receipts (через событие, не напрямую покупателю)поле fiscal_document_number в payload события

Публичных чтений, зависящих от статуса внешнего провайдера ОФД, у модуля нет — все admin-эндпоинты читают локальную БД (cms_ofd_receipts_queue/cms_ofd_kkt_status) и не деградируют от доступности оператора; конвенция ядра 200 + meta.degraded (ревизия 14.07.2026) здесь не применима впрямую именно поэтому, а не потому что модуль исключение из правила.

Настройки (группа ofd)

КлючТипДефолтaffectsPageCacheОписание
ofd.primary_providerstringnullнетКод основного провайдера ОФД
ofd.fallback_providerstringnullнетКод резервного провайдера при недоступности основного
ofd.retry_attemptsint5нетЧисло ретраев отправки чека
ofd.kkt_offline_alert_minutesint15нетПорог простоя ККТ для алерта
ofd.queue_size_alertint500нетЛимит размера очереди фискализации — превышение эскалирует алерт (риск законодательного порога 54-ФЗ, см. «Крайние случаи»)
ofd.disabled_providersarray[]нетKill-switch: коды провайдеров, временно исключённых из отправки без выключения модуля — чеки остаются в очереди
ofd.kkt_test_modebool (per-коннектор)falseнетТестовый режим коннектора/ККТ — блокирует боевую отправку
ofd.price_per_receiptdecimalnullнетСправочная стоимость чека у оператора (видимость расхода в админке, не тарификация)

Секреты доступа к API ОФД (токены, сертификаты) — только .env.

API

МетодПутьДоступНазначение
GET/api/v1/admin/ofd/queueadmin (ofd.view)Очередь фискализации с фильтрами по статусу
POST/api/v1/admin/ofd/queue/{id}/retryadmin (ofd.manage)Повторная отправка чека вручную
GET/api/v1/admin/ofd/kkt-statusadmin (ofd.view)Статус подключённых ККТ

Очередь фискализации — keyset-пагинация, не OFFSET. {id} в путях — публичный идентификатор строки очереди (ULID/UUID первичного ключа), не автоинкремент — см. «Крайние случаи» (⚠️ Противоречие со стандартом §4, если реализация выберет обычный id). POST /queue/{id}/retry — мутация с внешним эффектом (повторная отправка оператору): заголовок Idempotency-Key обязателен (стандарт §7).

Компоненты

Filament: очередь фискализации со статусами и ручным ретраем, виджет состояния ККТ. Команды: cms:ofd:retry-failed --json, cms:ofd:doctor --json (проверка доступности провайдера).

События и обмен

СобытиеКогдаPayload
OfdReceiptSentчек успешно передан операторуorder_id, provider_code, fiscal_document_number
OfdReceiptFailedотправка чека завершилась ошибкой после всех ретраевorder_id, provider_code, error
OfdKktOfflineККТ не отвечает дольше порогаkkt_id, provider_code

Реализует provides: ofd-provider — потребитель cms/commerce-receipts. Весь обмен с оператором ОФД — через cms/integrations-bus.

Взаимодействия

Сущность/модульКаналНаправлениеЧто происходит
cms/commerce-receipts4 (requires)receipts → ofdзапрос постановки чека в очередь (OfdService::enqueue()), сверка суммы
cms/commerce-receipts1 (событие)ofd → receiptsOfdReceiptSent/OfdReceiptFailed — receipts обновляет статус чека заказа
provides ofd-provider3 (сервисный контракт)receipts → реализация(и) ofdабстракция фискализации, реализуется коннекторами модуля
Оператор ОФД (внешний)5 (очередь) + cms/integrations-busofd → внешний APIотправка чека, обработка ответа асинхронно
Оператор ОФД → ofd (вебхук)внешний канал (webhooks-inintegrations-bus)внешний → ofdподтверждение приёма/ошибка чека, подпись обязательна
cms/healthhealth-чек + событие 1ofd → healthOfdKktOffline, эскалация переполнения очереди
cms/audit (если включён)1 (событие)ofd → auditручной ретрай, смена ККТ/провайдера, чек коррекции — в аудит

Любая связь вне этой таблицы — скрытая зависимость, анти-паттерн (стандарт §5).

Фоновая работа

Фискализация — именованная очередь ofd, чек не отправляется синхронно из HTTP-запроса оформления заказа. Ретраи с backoff при временной ошибке оператора, circuit breaker на провайдера с автопереключением на fallback_provider. Мониторинг ККТ — по расписанию. Восстановительная команда cms:ofd:retry-failed — идемпотентна, с --dry-run/--json, допустима к запуску на живом сайте без даунтайма (стандарт §15).

Производительность и кеш

Ожидаемые объёмы (оценочно): небольшой/средний интернет-магазин — до 1–5 тыс. чеков/сутки, крупный ритейл/маркетплейс — до нескольких десятков тысяч/сутки; уточняется под конкретный проект на этапе разработки.

Горячие пути: постановка в очередь (OfdService::enqueue(), синхронный вызов из commerce-receipts) — ≤1 запрос (INSERT), без внешнего HTTP; чтение очереди/статусов ККТ в админке — keyset, ≤2 запроса на страницу (список + агрегат по статусам, без COUNT(*) по всей таблице).

Индексы: cms_ofd_receipts_queue — составной status, created_at (выборка просроченных для алерта), FK order_id (см. «Модель данных»); cms_ofd_kkt_statuskkt_id/ provider_code, kkt_id для мониторинга.

Внешние вызовы к оператору ОФД — только из очереди. В отличие от delivery-ru (где расчёт стоимости — синхронный на checkout с таймаутом), у cms/ofd синхронных внешних вызовов нет вовсе: постановка в очередь — чисто локальная операция, отправка оператору всегда асинхронна. Это упрощает бюджет: 0 внешних HTTP на публичном горячем пути.

Собственных тегов кеша нет — очередь и статусы чеков читаются напрямую (консистентность критична для фискального учёта).

Безопасность

  • Повторная отправка одного и того же чека не создаёт дублирующий фискальный документ (идемпотентность по order_id + provider_code; ретрай использует тот же ключ — оператор либо возвращает уже созданный документ, либо создаёт новый по своей дедупликации).
  • Вебхуки статуса чека — подпись HMAC (секрет per-провайдер из .env, не в настройках) + проверка timestamp против replay (окно приёма, аналогично интеграциям).
  • Секреты доступа к API оператора ОФД — только .env.
  • Права: ofd.view, ofd.manage.

Матрица ролей:

Действиеадминменеджерstudio
Просмотр очереди/статусов ККТ (ofd.view)
Ручной ретрай чека (ofd.manage)
Смена provider/fallback, kill-switch (ofd.manage+settings.manage)
Чек коррекции (необратимая фискальная операция)

Стоимость чека у оператора (если тарификация «за чек») — видна в админке через ofd.price_per_receipt, справочно, без автоматического биллинга ядра.

UX-требования

Админ:

  • Статус синхронизации: индикатор очереди фискализации (размер, доля просроченных) на дашборде модуля, не только в списке.
  • Журнал обмена с провайдером (запросы/ответы, отдельно от бизнес-лога) — доступен для разбора инцидентов, без ПДн в открытом виде.
  • Ошибки на человеческом языке: «ОФД временно недоступен, чеки поставлены в очередь» вместо Error 500; «Не удалось отправить чек: исчерпаны попытки — требуется ручная проверка».
  • Подтверждение необратимых операций: чек коррекции — отдельная форма с явным предупреждением («создаст фискальный документ коррекции, отменить нельзя»); смена ККТ/оператора — предупреждение о переносе новых чеков на новую конфигурацию.
  • Пустое состояние очереди — «Все чеки фискализированы», не пустая таблица без пояснения.

Крайние случаи и типовые баги

  1. Очередь чеков растёт при недоступном ОФД → отслеживается по составному индексу status, created_at; при превышении ofd.queue_size_alert или приближении к законодательному сроку хранения нефискализированных данных в фискальном накопителе (по 54-ФЗ, точный срок — по паспорту конкретного ФН) алерт эскалируется в cms/health уровнем critical, не просто пишется в лог — риск блокировки ККТ. kkt_offline_alert_minutes покрывает другой, более быстрый сценарий — недоступность самой кассы.
  2. Чек коррекции → отдельный тип операции (operation_type = correction), не путается с обычным чеком в очереди; создаётся только явным действием администратора (форма с указанием причины расхождения), требует подтверждения и роли не ниже ofd.manage.
  3. Смена ККТ или ОФД посреди работы (например, перерегистрация кассы) → чеки, уже поставленные в очередь под старой конфигурацией (provider_code/kkt_id зафиксированы в строке на момент постановки), продолжают уходить через прежнего провайдера до исчерпания очереди; новые чеки — на новую конфигурацию; append-only история не переписывается задним числом.
  4. Тест-режим без боевой фискализацииofd.kkt_test_mode явно маркирует ККТ/коннектор как тестовый; отправка тестового чека боевым коннектором (несовпадение флагов) блокируется доменным исключением, не тихим пропуском.
  5. Дубль вебхука о статусе чека от оператора → идемпотентность по order_id + provider_code: повторный вебхук с тем же статусом не создаёт вторую запись в журнале переходов и не переиздаёт OfdReceiptSent повторно (проверка текущего состояния перед переходом, не слепая запись).
  6. Недоступность основного провайдера дольше порога → circuit breaker переключает на ofd.fallback_provider; чеки, уже отправленные основному и повисшие без ответа, не переотправляются автоматически через fallback (риск задвоения) — остаются в статусе «ожидает ответа» до ручной проверки или подтверждённого таймаута, только новые постановки идут через fallback.
  7. Таймаут ответа оператора при отправке чека → ретрай выполняется с тем же идемпотентным ключом (order_id+provider_code); модуль использует updateOrCreate по этому ключу — вторая строка в очереди на один заказ не создаётся.
  8. Смена схемы ответа API оператора (breaking change у провайдера) → парсер ответа деградирует контролируемо: чек остаётся в статусе «ошибка обработки ответа» (не «отправлен» по умолчанию), алерт разработчику через cms/health с телом неожиданного ответа в диагностическом логе (без ПДн).
  9. Расхождение суммы чека с суммой заказа (данные разошлись до отправки) → сверка payload.amount с order.total перед постановкой в очередь; расхождение блокирует постановку (ReceiptAmountMismatch), алерт — чек с неверной суммой оператору не уходит.
  10. Пустая/резко выросшая очередь (0 записей — штатно; всплеск после простоя) → всплеск обрабатывается тем же батчингом ретраев с backoff, не заваливает оператора пиковой нагрузкой (rate-limit воркера).

Мини-ранбук (эксплуатация):

СимптомЧто проверитьКоманда
Очередь фискализации растёт, чеки не уходятдоступность оператора, статус circuit breaker, ofd.disabled_providerscms:ofd:doctor --json
ККТ офлайн дольше kkt_offline_alert_minutesсеть до кассы, cms_ofd_kkt_status, физическое подключениеcms:ofd:doctor --json (секция kkt)
Чеки уходят, но статус не обновляетсядоставка вебхуков в cms/webhooks-in, совпадение подписижурнал вебхуков + cms:ofd:retry-failed --json

Донорский код

Донор: — (новая разработка). Легаси-импорт (стандарт §16) не применим — исторических чеков у донора нет. Если в будущем появится перенос из внешней бухгалтерской/фискальной системы, используется тот же паттерн команды cms:ofd:import-legacy --source=<профиль>, идемпотентной по external_id, с --dry-run.

Тесты и приёмка

  • [ ] Мок API оператора ОФД: тест успешной фискализации и обработки ошибки с ретраем
  • [ ] Фискализация выполняется из очереди, не из HTTP-запроса оформления заказа
  • [ ] При недоступности основного провайдера включается fallback_provider (тест переключения)
  • [ ] Повторная отправка одного и того же чека не создаёт дублирующий фискальный документ
  • [ ] При выключении модуля заказ оформляется без падения, чек остаётся в очереди
  • [ ] Права ofd.manage разграничивают просмотр очереди и ручной ретрай
  • [ ] Вебхук статуса чека: подпись проверяется, невалидная подпись/протухший timestamp отклоняются
  • [ ] Дубль вебхука не создаёт повторную запись перехода статуса и не переиздаёт событие
  • [ ] Расхождение суммы чека с заказом блокирует постановку в очередь (ReceiptAmountMismatch)
  • [ ] Тест-режим ККТ не допускает боевую отправку тестового чека
  • [ ] Превышение ofd.queue_size_alert эскалирует алерт уровнем critical в cms/health
  • [ ] Матрица ролей: ofd.view/ofd.manage проверены тестами доступа
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут; тестовая БД только ofd_test, migrate:fresh запрещён

Внутренняя база знаний студии