Skip to content

© 2026 ООО «Аксиома» ИНН:7842223709 · mock-services · Apache-2.0 SPDX-License-Identifier: Apache-2.0

kassa-mock — мок облачной кассы OFD Ferma (T-038) ​

Пара к tbank-mock: вместе они образуют локальный мок-стенд «банк + касса» для тестирования платёжно-фискального контура по контракту Ferma. Стенд не проксирует запросы в Ferma и не предназначен для боевых реквизитов; переключать интеграцию на production нужно отдельно, не только заменой URL.

Node.js >= 18; внешние npm-зависимости не нужны. Сервер использует общий модуль packages/core для HTTP, персистентности, sandbox и защитных helpers.

Контракт (по документации Ferma) ​

МетодПоведение
POST /possystem/v1/getToken{login, pass} → {token} (TTL 12 ч, MOCK_TOKEN_TTL_MS); неверные → error.code=14, истёкший токен → error.code=11
POST /possystem/v1/{group}/sellчек прихода («Предоплата/Аванс/Полный расчёт/Зачёт аванса») → {uuid, status:"wait"}; дубли external_id → error.code=12; невалидный чек → error.code=4 + field + hint
POST /possystem/v1/{group}/sell_refundчек возврата прихода (опц. origin_external_id со сверкой остатка)
POST /possystem/v1/{group}/sell_correctionчек коррекции прихода (валидация correction_info)
POST /possystem/v1/{group}/buyчек расхода (выплаты физлицам / агентские и неагентские схемы)
POST /possystem/v1/{group}/buy_refundчек возврата расхода (опц. origin_external_id со сверкой остатка)
POST /possystem/v1/{group}/buy_correctionчек коррекции расхода
GET /possystem/v1/{group}/report/{uuid}{status: wait|done|fail, ofd_status: pending|sent|rejected, payload:{ФД/ФП, ofd_receipt_url, vat_amounts, marking_verified,…}}
GET /possystem/v1/ofd/queueочередь документов в ОФД (ofdMode, count, items)
POST /possystem/v1/ofd/modeуправление связью с ОФД ({mode: "online"|"offline"|"reject", flush: true})
GET /possystem/v1/stats/cashboxes/extendedмониторинг кассы (kktState, тариф, счётчик чеков, ofdMode, ofdQueueCount)
GET/POST/PUT /mock/timeвиртуальные часы ({"advance":"13h"}, {"reset":true})
GET/POST/DELETE /mock/scenariosсценарное API + заголовок X-Mock-Scenario: fail|hang|ofd-queue|ofd-reject|mark-fail|webhook-drop
GET /adminконсоль: последние 50 документов, фискальные данные, статус ОФД, журнал запросов; формы действий защищены подписанным CSRF-токеном и same-origin проверкой
GET /admin/journalJSON-журнал последних API-запросов; креды getToken (login/pass), токены и реквизиты покупателя редактируются — [REDACTED]/маска, заголовки только из allowlist
GET /healthzok/касса недоступна + ofdMode и ofdQueueCount

Правила, история срабатываний и журнал /admin/journal принадлежат текущей песочнице (#52): другие песочницы их не видят и не очищают, POST /mock/reset действует только на целевую песочницу. Лимиты: до 200 правил на песочницу и до 8 KiB на правило (превышение — HTTP 400 с code rule_cap_exceeded/rule_too_large).

Валидация чека (DX-слой) ​

До «печати» проверяются инварианты, которые в бою проверит касса/ФНС:

  • items[] — непустой массив, у позиции name, price, quantity, sum = price × quantity;
  • total = сумме позиций; Σ payments[].sum = total;
  • vat.type — из справочника (none, vat0, vat5, vat7, vat10, vat20, vat22);
  • payment_method — из справочника 54-ФЗ (full_prepayment, advance, full_payment, consideration, …);
  • payments[].type — 1..5 (электронными/предоплата/постоплата/иная/наличные).

Ошибка приходит в формате Ferma с расширением DX: {"error":{"code":4,"field":"receipt.total","hint":"Итог 50000 ≠ сумме позиций 100…"}}. Отключается MOCK_VALIDATE_RECEIPTS=false.

Через MOCK_PROCESS_MS (по умолчанию 15 с; 0 — печатать мгновенно) документ «печатается»: фиксируется время печати, присваивается уникальный номер чека за смену, генерируются ФД/ФП/фискальный признак, в лог печатается текст чека, результат POST'ом уходит на receipts.service.callback_url (ретраи, как у Ferma, и продолжение ретраев после рестарта с MOCK_STATE_FILE). Чек покупателю «доставляется» страницей GET /receipts/{uuid} (это и есть ofd_receipt_url).

Негативные сценарии (драйвер Фазы 4) ​

  • external_id с префиксом fail- → документ обрабатывается, статус fail + error → ядро получает callback с ошибкой, джоба ретраит;
  • префикс hang- → вечный wait, callback не отправляется (зависший чек);
  • MOCK_FERMA_DOWN=1 (опц. MOCK_FERMA_DOWN_MS=N для авто-восстановления) → все /possystem/* отвечают 503 «касса недоступна» — сценарий «приняли деньги, касса легла»;
  • неверный логин/пароль/токен → ошибки контракта (14/11);
  • чек с нарушенными инвариантами → ошибка 4 с указанием поля (см. выше);
  • кнопка «callback» в /admin — повторная доставка результата (проверка идемпотентности на стороне интегратора).

Префиксы fail-/hang- — удобная конвенция, если вы контролируете external_id. Для произвольных идентификаторов используйте заголовок X-Mock-Scenario или программируемые правила сценариев /mock/scenarios; административные кнопки работают только через форму с CSRF-токеном.

Запуск ​

bash
docker compose up -d kassa-mock      # порт 3101
# или голым node
MOCK_FERMA_LOGIN=loviitest MOCK_FERMA_PASSWORD=mockpass123 node services/kassa-mock/server.js

Самотест (129 проверок: контракт Ferma и 54-ФЗ, сценарии ОФД/маркировки, управляющие токены и CSRF, песочницы, динамические ephemeral-порты, ожидание состояний вместо фиксированных sleep и рестарт callback-очереди):

bash
npm run test:kassa

Подключение своего приложения к моку ​

Приложение ходит в мок как в боевую кассу Ferma: укажите в конфигурации базовый URL мока и креды стенда (внутри docker-сети сервис доступен как http://kassa-mock:3101):

env
FERMA_API_URL=http://kassa-mock:3101   # или http://localhost:3101 с хоста
FERMA_LOGIN=loviitest
FERMA_PASSWORD=mockpass123
FERMA_GROUP=Main
FERMA_CALLBACK_URL=https://<ваш домен>/api/v1/fiscal/callback

Клиент не меняется: результаты документов приходят callback'ом на callback_url из запроса (ретраи как у Ferma), чек покупателю доступен по ссылке ofd_receipt_url. Имена переменных в вашем приложении могут отличаться — важен контракт. Пошаговая цепочка curl — в гайде «за 10 минут».

Конфиг (env) ​

ПеременнаяПо умолчаниюЧто делает
MOCK_PORT / MOCK_HOST3101 / 0.0.0.0прослушивание
MOCK_FERMA_LOGIN / MOCK_FERMA_PASSWORDloviitest / mockpass123креды стенда
MOCK_TOKEN_TTL_MS12 чсрок жизни токена (контракт Ferma)
MOCK_PROCESS_MS15000задержка «печати»; 0 — мгновенно
MOCK_VALIDATE_RECEIPTStrueпроверять инварианты чека
MOCK_GROUPS— (любая)список разрешённых групп кассы, напр. Main,Shop-1
MOCK_CALLBACK_RETRY_MS / MOCK_CALLBACK_MAX_ATTEMPTS10000 / 24политика доставки callback'ов
MOCK_PUBLIC_BASE_URLhttp://localhost:${MOCK_KASSA_HOST_PORT:-3101}база для ofd_receipt_url (виден покупателю!)
MOCK_AGENT_LINE / MOCK_AGENT_INN«ТЕСТОВАЯ КАССА (МОК)» / 7800000000шапка ленты чека (без бренда владельца стенда)
MOCK_BLOCK_PRIVATE_WEBHOOKSfalse (в MOCK_HOSTED=1 — true)не отправлять callback'и на приватные адреса и имена, которые в них резолвятся (1/true; 0/false выключает явно)
MOCK_OPERATOR_TOKEN—токен управляющей поверхности: /mock/*, смены ККТ, /fn/state, /ofd/* (fallback — MOCK_ADMIN_TOKEN)
MOCK_ADMIN_TOKEN—токен админки /admin*; query-токен и owner-token в owned tenancy сохраняются в action URL/redirect, POST дополнительно требует CSRF-токен и same-origin
MOCK_HOSTEDfalsehosted-режим: управляющая поверхность fail-closed без токена, SSRF-политика по умолчанию
MOCK_TENANCYowned при MOCK_HOSTED=1, иначе namespacesowned/namespaces/off; legacy 1/on/true/yes → owned, 0/false/no → off; неизвестное непустое значение пишет WARN и безопасно возвращается к default
MOCK_SANDBOX_TOKEN—owner-token default-песочницы. При MOCK_HOSTED=1 без него default не обслуживается (MS-12: бизнес/form → 404, стартовый лог default=closed); в self-hosted default открыта с предупреждением при старте
MOCK_SANDBOX_TTL_MS24 ч в hosted, иначе 0 (бессрочно)TTL именованной песочницы; истёкшие удаляет sweep-воркер
MOCK_SANDBOX_MAX100 в hosted, иначе 256максимум именованных песочниц
MOCK_SANDBOX_MAX_DOCUMENTS5000 в hosted, иначе 0 (без лимита)квота документов песочницы (F-15)
MOCK_SANDBOX_HOST_SUFFIX—суффикс поддоменов песочниц, напр. mock-services.dev: <id>.mock-services.dev
MOCK_SANDBOX_CREATE_LIMIT20 в hosted, иначе 0лимит создания песочниц на IP в час
MOCK_SANDBOX_FILE<MOCK_STATE_FILE>.sandboxes.jsonбаза реестра песочниц: поштучные файлы в каталоге <база без .json>.records/ (owner-токены — только хэши); legacy-файл <база> переносится при старте и переименовывается в .migrated
MOCK_SANDBOX_SWEEP_MS60000интервал воркера TTL/retention
MOCK_SANDBOX_RETENTION_MS0 (выкл.)retention диагностических записей (отправленные ОФД-записи, истёкшие токены)
MOCK_FERMA_DOWN / MOCK_FERMA_DOWN_MSfalse / 0старт «касса недоступна» и авто-восстановление
MOCK_FN_NUMBER / MOCK_ECR_RN / MOCK_OFD_INN / MOCK_FNS_SITEтестовыереквизиты ФН/ККТ/ОФД и сайта ФНС в фискальном снимке
MOCK_FN_CAPACITY250000ресурс ФН: исчерпание → ошибка 63 без ожидания реального ФН
MOCK_OFD_MODEonlineстартовое состояние канала ОФД кассы: online / offline (очередь растёт) / reject
MOCK_OPERATOR«Кассир-мок»имя оператора в фискальном снимке чека
MOCK_VIRTUAL_CLOCK—ISO-старт виртуальных часов стенда (TTL токена 12 ч, смена ККТ 24 ч, ресурс ФН, ретраи callback)
MOCK_STATE_FILE—файл персистентности; без него состояние в памяти

Песочницы и hosted tenancy (RFC-001, A-21) ​

Каждый запрос разрешает свою песочницу один раз и дальше видит только её состояние, креды и персистентность: параллельные A/B-запросы не смешиваются между await (включая токены getToken), а запрос без селектора всегда попадает в default. Селектор: X-Mock-Sandbox: <id> → ?sandbox=<id> → поддомен <id>.<MOCK_SANDBOX_HOST_SUFFIX> (если суффикс задан).

РежимВключаетсяПоведение
namespacesдефолт self-hosted; MOCK_TENANCY=namespacesX-Mock-Sandbox создаёт изолированное пространство имён для параллельных CI-воркеров; креды и управляющие токены — общие
ownedMOCK_HOSTED=1 по умолчанию; MOCK_TENANCY=owned (legacy 1/on/true/yes)у каждой песочницы свои креды Ferma и owner-token (показывается один раз); знание id без owner-token доступа не даёт (401, код 11), неизвестный id — 404 (код 10), истёкший — 410
offMOCK_TENANCY=off (legacy 0/false/no)одна общая песочница default; селектор игнорируется

API (в owned — с owner-token; в namespaces те же маршруты управляют пространствами имён без owner-token):

ЗапросЧто делает
POST /sandboxesсоздать песочницу: id, url, creds (свой логин/пароль/группа Ferma) и ownerToken — показывается один раз
GET /sandboxes (алиас GET /mock/sandboxes)список: владельцу — своя песочница, оператору (MOCK_OPERATOR_TOKEN) — все
DELETE /sandboxes?sandbox=<id>удалить песочницу вместе с состоянием (владелец или оператор)
GET /sandboxes/meсвоя песочница: состояние, креды, TTL, расход квоты; POST /sandboxes/me/reset, POST /sandboxes/me/reset-creds, DELETE /sandboxes/me

Owner-token принимается в Authorization: Bearer, X-Sandbox-Token или ?sandbox_token=. Персистентность: реестр песочниц пишется атомарно и поштучно — по файлу <MOCK_STATE_FILE>.sandboxes.records/<id>.json на песочницу (хэши owner-токенов + снимки состояния; мутация переписывает только свою песочницу, PD-02), а default всегда восстанавливается только из MOCK_STATE_FILE — состояние именованной песочницы не может быть выдано за default (F-14). Просроченные песочницы удаляет sweep-воркер; квота документов (MOCK_SANDBOX_MAX_DOCUMENTS, F-15) проверяется до фискализации и отвечает ошибкой 4 с подсказкой.

staging ​

Сервис в compose-core-staging-extras.yml (профиль mock, порт 3101 на localhost). Админ-консоль: curl http://localhost:3101/admin с сервера.

Границы симуляции ​

  • Маршруты, перечисленные выше, реализуют тестовую модель; это не облачная касса и запросы не отправляются в ОФД или ФНС. Не используйте реальные учётные записи, чеки и персональные данные.
  • Статусы печати wait → done|fail и статусы ОФД (pending|sent|rejected) моделируются локально; ofd_receipt_url указывает на страницу мока.
  • Контракт восстановлен по опубликованным материалам и интеграции проекта; сверяйте поведение с актуальной документацией Ferma перед изменением production-интеграции.

Байтовые квоты состояния (PD-02) ​

EnvДефолтНазначение
MOCK_SANDBOX_MAX_STATE_BYTESпусто: 8388608 (8 МиБ) при maxDocuments > 0, иначе без лимитабайты JSON одной песочницы; 0 выключает
MOCK_SANDBOX_GLOBAL_MAX_STATE_BYTES67108864 (64 МиБ)сумма байт JSON всех песочниц рантайма, включая default; 0 выключает

Значения задаются в целых байтах. Пусто сохраняет дефолты. Квота per-sandbox не ограничивает память контейнера; общий бюджет меньше mem_limit: 128m, но не равен RSS. Превышение глобального бюджета на записи или байтовых квот при импорте /mock/state → HTTP 400, errorCode: 400, kind: 'bytes', code: sandbox_state_budget_exceeded / sandbox_state_quota_exceeded, с откатом отклонённого состояния. Правила/история/журнал остаются volatile. Подробности и границы — документация ядра.