Skip to content

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

vtb-mock — ВТБ SMB E-commerce API ​

Четвёртый внешний банк-адаптер Stage 2 (issue #87). Реализован по официальной документации SMB E-commerce API и callback/тестирования ВТБ. Это SMB API интернет-эквайринга, не legacy RBS (vtb.rbsuat.com, *.do): JSON REST base /openapi/smb/efcp/e-commerce/v1/, OAuth 2.0 client_credentials (POST /passport/oauth2/token) → Authorization: Bearer + X-IBM-Client-Id (client_id в нижнем регистре, без @ext.vtb.ru), суммы — десятичные рубли {value, code: "RUB"}.

Быстрый старт ​

bash
npm run start:vtb          # http://localhost:3106
# или общий локальный стенд:
docker compose up -d vtb-mock

Node ≥ 18, zero npm dependencies. Синтетические дефолты — vtbmocktest / mockpass123; это не тестовые креды банка. Фактические env значения при запуске скрыты в логах. Банк выдаёт реальные тестовые client_id/client_secret подключённому merchant отдельно; не помещайте их в код, тесты, fixtures или общий compose-файл.

Поддерживаемый SMB-поток ​

МаршрутНазначение
POST /passport/oauth2/tokenOAuth 2.0 client_credentials → {access_token, token_type, expires_in} (≈170 с; обновление по expires_in)
POST /openapi/smb/efcp/e-commerce/v1/ordersсоздать ордер (уникальный merchant orderId, orderName, amount в десятичных рублях) → payUrl; повтор orderId идемпотентен (X-Mock-Replayed)
GET /openapi/smb/efcp/e-commerce/v1/orders/{orderId}ордер + транзакции платежей/возвратов; status.value на своём уровне
POST /openapi/smb/efcp/e-commerce/v1/orders/preauthордер с предавторизацией (twoStageParams обязателен); доступность — MOCK_VTB_PREAUTH («отдельное разрешение банка»)
POST /openapi/smb/efcp/e-commerce/v1/orders/complete/{orderId}завершить холд суммой ≤ авторизации (частичное подтверждение поддержано)
POST /openapi/smb/efcp/e-commerce/v1/orders/{orderId}/cancelотмена неоплаченного ордера/preauth до complete, опциональный cancelReason
POST /openapi/smb/efcp/e-commerce/v1/refundsполный/частичный возврат (refundId, paymentId, amount); refundId уникален — повтор это ошибка, не replay
GET /openapi/smb/efcp/e-commerce/v1/refunds/{refundId}статус возврата
GET /pay/{orderId}, POST /pay/{orderId}/payтестовая hosted-страница оплаты (payUrl) с синтетическими [FORK]-картами
GET /health, GET /openapi.jsonhealthcheck и OpenAPI 3.1
GET /admin, GET /admin/journal, POST /admin/actionконсоль ордеров/возвратов/очереди callback и журнала; действия защищены same-origin + CSRF
/mock/time, /mock/scenarios, /mock/state, /mock/resetвиртуальные часы, хаос/сценарии и состояние песочницы
/sandboxes*, /mock/sandboxesRFC-001 sandbox/control-plane API

Legacy RBS (vtb.rbsuat.com, /payment/rest/, register.do/paymentorder.do, userName/password, form-urlencoded) не реализуется и не является альтернативной базой для /openapi/smb/…. СБП-QR стенды (qr-efcp.bankingapi.ru) и ветка sbpParams — вне области.

OAuth и заголовки ​

Токен выдаётся form-urlencoded POST /passport/oauth2/token (grant_type=client_credentials); ошибки token endpoint — RFC 6749 §5.2 (invalid_client, unsupported_grant_type). TTL токена (дефолт 170 с) живёт на виртуальных часах: POST /mock/time {"advance":"+171s"} истекает токен, клиент запрашивает новый по expires_in. Бизнес-запросы несут Authorization: Bearer <token> и X-IBM-Client-Id — строго client_id в нижнем регистре без @ext.vtb.ru (суффикс/регистр → invalid_client). Merchant-Authorization опционален (одна политика: для одного ресурса не обязателен).

Суммы, статусы и идемпотентность ​

amount.value — десятичные рубли (10.56), не копейки: принимается число или цифровая строка максимум с двумя знаками после точки, code строго RUB. Статусы живут строго на своём уровне status.value: ордер CREATED/PAID/REFUNDED/PARTIALLY_REFUNDED/CANCELED, операция (платёж/возврат) NEW/CONFIRMED/RECONCILED — не смешивать. Ордер по умолчанию живёт 20 минут (expire возвращается с timezone и миллисекундами; своё — при создании); по истечении незакрытый ордер переходит в CANCELED ([VERIFY]: явного статуса истечения в срезе нет). Повтор orderId в POST /orders идемпотентен и возвращает существующий ордер; повторный refundId — provider-shaped ошибка duplicate_refund_id (409) с DX-подсказкой.

Callback SMB ​

Исходящее уведомление — HTTP POST JSON {"type":"PAYMENT|REFUND","object":{…}} на MOCK_VTB_CALLBACK_URL (один URL на ресурс). Payload сохраняется и доставляется как есть: в карточном примере платежа поле статуса — Status (заглавная), в примере возврата — status (строчные); мок не нормализует регистр ключа. Сценарий status-lowercase отдаёт СБП-вариант status у платежа (для проверки парсеров), reconciled — RECONCILED вместо CONFIRMED.

Подписи, обязательного ACK-кода и расписания ретраев в публичных SMB-доках нет — мок их не выдумывает и не переносит legacy-правила RBS. Локально доставкой считается любой HTTP 2xx ([FORK] — договорённость стенда, не утверждение протокола); график повторов задаётся MOCK_VTB_CALLBACK_RETRY_MS × MOCK_VTB_CALLBACK_MAX_ATTEMPTS (дефолт 3 × 60 с). Уведомление — не источник истины: после callback выполняйте GET /orders/{orderId} либо GET /refunds/{refundId}. В отсутствии callback URL очередь не создаётся.

Ошибки и протокольные оговорки ​

Бизнес-ошибки — HTTP 400/401/404/409/429 + тело {error, error_description, field, hint, doc}; коды invalid_request, invalid_token, invalid_client, invalid_amount, order_not_found, payment_not_found, refund_not_found, duplicate_refund_id, amount_exceeds_limit, invalid_order_state, preauth_disabled и др. Публичный SMB-срез фиксирует HTTP-статусы и примеры, но не полную схему тела бизнес-ошибок — envelope и словарь кодов это явный [FORK] мока (по образцу errorCode-форка Сбера), а не production-каталог ВТБ. Точно так же [FORK]/[VERIFY] являются: синтетические карты взамен истёкшей официальной (4111 1111 1111 1111, 12/24), форма twoStageParams/paymentData/paymentCode, объект {"value": …} внутри callback-статуса, mock-поле returnUrl, пометка voided у снятого холда (статуса reversal в словаре операций нет).

Сценарии, часы и диагностика ​

X-Mock-Scenario//mock/scenarios поддерживают fail, hang, latency, http-500, http-504, webhook-drop/no-callback, webhook-duplicate/callback-duplicate, status-lowercase, reconciled. POST /mock/time принимает {"advance":"+20m"} (истечение ордера), {"advance":"+171s"} (истечение токена) и {"advance":"+60s"} (retry callback). Сценарии, история, токены, журнал и состояние изолируются по X-Mock-Sandbox. Журнал redacts чувствительные поля; PAN не пишется в журнал и не сохраняется в ордере — только маска first6**last4. В hosted-режиме SSRF-проверка исходящих callback включена по умолчанию; SSRF_BLOCKED — терминальный отказ без повторов.

Переменные окружения ​

ПеременнаяDefaultНазначение
MOCK_HOST / MOCK_PORT0.0.0.0 / 3106адрес и порт HTTP-сервера
MOCK_VTB_CLIENT_IDvtbmocktestсинтетический client_id (в X-IBM-Client-Id — lowercase, без @ext.vtb.ru)
MOCK_VTB_CLIENT_SECRETmockpass123синтетический client_secret
MOCK_VTB_TOKEN_TTL_SECS170срок access_token в секундах; поддерживает длительность (2m); TTL живёт на виртуальных часах
MOCK_VTB_PUBLIC_BASE_URLhttp://localhost:${MOCK_VTB_HOST_PORT:-3106}внешний base URL, используемый в payUrl
MOCK_VTB_CALLBACK_URLпустоcallback URL ресурса (один на ресурс); без него очередь не создаётся
MOCK_VTB_CALLBACK_RETRY_MS60000 (60s)интервал retry; число в миллисекундах либо длительность (60s, 1m)
MOCK_VTB_CALLBACK_MAX_ATTEMPTS3максимум доставок; значение меньше 1 нормализуется в 1
MOCK_VTB_ORDER_TTL_SECS1200срок нового ордера (20 минут); поле expire запроса переопределяет его для конкретного ордера
MOCK_VTB_PREAUTHtrueразрешить POST /orders/preauth в локальном стенде (upstream — отдельное разрешение банка)
MOCK_STATE_FILEпустосохранение доменного состояния/очереди между рестартами
MOCK_SANDBOX_FILE<MOCK_STATE_FILE>.sandboxes.jsonбаза RFC-001 реестра песочниц
MOCK_SANDBOX_SWEEP_MS60000интервал sweep TTL/retention, когда worker включён
MOCK_BLOCK_PRIVATE_WEBHOOKSfalse (hosted → true)запрет исходящих callback в private/reserved адреса; SSRF-отказ терминален
MOCK_VIRTUAL_CLOCKтекущее времяначальное значение виртуальных часов
MOCK_ADMIN_TOKEN / MOCK_OPERATOR_TOKENпустозащита управляющих /admin* и /mock/*
MOCK_HOSTEDfalsetrue включает hosted fail-closed для управляющих поверхностей и default SSRF-защиту
MOCK_TENANCYавтоsandbox-модель RFC-001 (off, namespaces, owned)
MOCK_SANDBOX_TOKENпустоowner-token default sandbox в owned-модели; при MOCK_HOSTED=1 без него default не обслуживается (MS-12)
MOCK_SANDBOX_TTL_MS / MOCK_SANDBOX_MAXowned: 24h / owned: 100аренда и число песочниц
MOCK_SANDBOX_MAX_DOCUMENTSowned: 5000квота ордеров + возвратов + callback-документов на sandbox
MOCK_SANDBOX_RETENTION_MS0retention терминальных callback-записей; ордеры не удаляются
MOCK_SANDBOX_HOST_SUFFIXпустоопциональный hostname-формат sandbox URL
MOCK_SANDBOX_CREATE_LIMITowned: 20rate-limit создания sandbox на IP

Протокольные файлы и тесты ​

Байтовые квоты состояния (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. Подробности и границы — документация ядра.