📚 BusinessPlatform Docs

Velvio × BusinessPlatform — серверный контракт v1

Обновление 15 октября 2026: магазин использует общий Commerce API, /api/v1/commerce/projects/{projectId}. Внешний клиент владеет оплатой; прежние /payment-settings и /checkout первого адаптера возвращают 410. Профиль/каталог старого адаптера сохраняются для совместимости. Описание ниже фиксирует прежний этап; актуальные DTO вариантов, доставки, заказа и результата оплаты находятся в новом контракте.

Velvio создаёт сайт и интерфейс владельца. BusinessPlatform хранит бизнес и каталог, выполняет бизнес-операции. Здесь описаны реализованный первый этап и границы его готовности.

Проект: BusinessPlatform / Velvio · Создано: 7 октября 2026, 14:15 +03:00 · Обновлено: 15 октября 2026, 21:59 +03:00 · Статус: первый адаптер сохранён для совместимости; текущий магазин — общий Commerce API выпуска 20261015-212542.

Основание — действующее ТЗ владельца. Документ дополняет ТЗ результатами реализации; следующие этапы сохраняются. Источник: docs/int-velvio.md; код: Services/Integrations/Velvio/; миграция: 037_velvio_integration.sql.

Уточнение владельца 7 октября 2026: API BusinessPlatform — общий для внешних приложений; Velvio является первым потребителем. Этот документ описывает фактически развёрнутый первый адаптер с Velvio-specific маршрутами и настройками. Обобщение идентичности клиента, прав на бизнес, внешних привязок и доставки событий ещё не реализовано. Архитектурное решение и критерии: раздел 14 руководства BP. Планы записи/заказов/платежей сохраняются как возможности общего BP API, а не отдельные правила для Velvio.

1. Граница продуктов и готовность

БраузерСайт / кабинет Velvio
→
Velvio BFFПользователь, проект, CAPTCHA, лимиты
→
BusinessPlatformJWT, scopes, привязка projectId
→
PostgreSQLДанные + ответ + outbox

JWT и подпись вебхуков используются только серверами. Браузер не получает приватные ключи BP/Velvio. BP не копирует аккаунт Velvio и не предоставляет iframe кабинета. Доступ пользователя к проекту проверяет Velvio до вызова BP; сервисный JWT имеет доступ ко всем зарегистрированным проектам в пределах scope.

Область Сейчас
Привязка проекта Один внешний projectId → один новый бизнес BP
Авторизация Отдельная схема RSA JWT RS256; read/write/provision
Профиль Чтение и обновление ограниченного DTO
Товары, услуги, категории CRUD, архивирование, tenant isolation, пагинация
Идемпотентность Изменение, событие и сохранённый ответ — одна транзакция
События integration.provisioned, profile.changed, catalog.changed; HMAC и повторная доставка
Мастера, ресурсы, расписание Ещё не доступны через контракт Velvio
Записи, holds, ожидание Ещё не доступны через контракт Velvio
Заказы, оплата, возвраты Ещё не доступны через контракт Velvio; существующие платёжные настройки BP сохраняются
Сквозная работа двух продуктов Нужны Velvio BFF, issuer, receiver и совместная приёмка

GET /capabilities возвращает эти возможности машинным способом. Velvio скрывает недоступные действия, а не вызывает старые public API с внутренними IDs. Новый API развёрнут в ядре 20261007-145000, но отключён: публичный HTTPS маршрут возвращает 503 integration_disabled. Basic Auth кабинета на этот маршрут не распространяется. Миграция 037 зарегистрирована атомарно; данные существующих бизнесов сохранены.

2. Доступ и формат запросов

База URL: /api/v1/integrations/velvio/projects/{projectId}. Это UUID проекта Velvio, не businessId BP. Бизнес разрешается через integration_tenants; его ID не возвращается в DTO. Подключать произвольный существующий бизнес через тело запроса нельзя.

Authorization: Bearer <short-lived-service-JWT>
X-Request-Id: <UUID>
Idempotency-Key: <unique-command-key>

Idempotency-Key обязателен для команд: один заголовок, 1–128 печатных ASCII-символов без пробелов. POST/PUT: application/json; DELETE: пустое тело. Максимум 64 KiB, глубина JSON 16. JSON camelCase; неизвестные поля и повторные имена свойств отклоняются. Суммы/проценты — максимум два десятичных знака. Даты ответов — ISO 8601 UTC.

JWT: RS256, RSA ≥2048 бит, известный kid; iss=velvio, aud=business-platform, sub=velvio-api, непустой UUID jti, iat, exp. Срок exp−iat ≤300 секунд, рассогласование часов ≤30 секунд. Приватный ключ хранит Velvio, BP получает только публичный. В scope перечисляются права через пробел:

Scope Доступ
velvio:read GET профиль, каталог, capabilities
velvio:write PUT профиль; POST/PUT/DELETE каталог
velvio:provision PUT базового URL проекта

Cookie, WidgetToken, MCP/AIRouter service token и ключ витрины не заменяют этот JWT. Вне Development требуется HTTPS. Перед включением проверить доверенный reverse proxy и закрыть прямой HTTP-порт ядра: общая текущая настройка ForwardedHeaders доверяет заголовкам прокси, поэтому сетевое ограничение nginx/порта обязательно. Публичный CORS новому API не добавлен.

Ошибки и ответный X-Request-Id:

{"error":{"code":"idempotency_conflict","message":"Key was already used for a different request.","requestId":"00000000-0000-4000-8000-000000000001","details":{}}}

400 — поля/заголовки; 401 — JWT; 403 — scope/suspended; 404 — проект/сущность не доступны; 409 — конфликт; 413 — тело; 415 — Content-Type; 503 — отключено/недоступно; 504 — deadline. Чужой ID возвращает 404. SQL, секреты и сообщения исключений не раскрываются. Velvio переводит error.code в свои сообщения.

3. Проект и профиль

PUT / со scope provision создаёт бизнес (201) или синхронизирует существующий (200):

{"name":"Салон Пример","businessType":"services","timeZone":"Europe/Moscow"}

Типы: services/retail/hybrid/booking/events. Флаги модулей — Business.ApplyDefaultFlags. Тип существующего бизнеса не меняется: 409 business_type_conflict. Новые бизнесы имеют isPublished=false, RUB и slug velvio-{projectId без дефисов}; публикация в агрегаторе BP не включается. Настройка расписания относится к следующему этапу.

GET /profile: projectId, name, businessType, timeZone, currency, status, isPublished, description, address, city, phone, email, createdAt, updatedAt. Секретные settings и ключи оплаты не выдаются.

PUT /profile — полный редактируемый DTO, не JSON Patch; nullable-поля передаются явно:

{"name":"Салон Пример","description":"Услуги","address":"Улица, 1","city":"Москва","phone":"+70000000000","email":null,"timeZone":"Europe/Moscow"}

Статусы active/suspended/deleted пока меняет оператор контролируемо через DbInspector; HTTP lifecycle ещё нет. Suspended блокирует чтение, команды и старые idempotency-ответы; события сохраняются, доставка приостановлена. Deleted возвращает 404, не пересоздаётся provisioning и не удаляет исторические данные.

4. Каталог

Метод и путь после projectId Результат
GET /catalog/items или /catalog/categories {items:[...],nextCursor:UUID или null}
GET /catalog/items/{id} или /catalog/categories/{id} Сущность, включая архивную
POST /catalog/items или /catalog/categories Создание, 201
PUT /catalog/items/{id} или /catalog/categories/{id} Замена редактируемого DTO, 200
DELETE /catalog/items/{id} или /catalog/categories/{id} Архивирование, 200; строка сохранена

Список: limit 1..100 (50 по умолчанию), cursor=последний id предыдущей страницы. Порядок id↑, архив исключён, business_id проверяется на каждой странице. Это текущее состояние, не snapshot: конкурентные вставки требуют повторного прохода или события-инвалидации.

{"name":"Букет","itemType":"product","price":2500,"categoryId":null,"description":null,"stockQuantity":10,"discountPercent":0,"status":"active","sortOrder":0}
{"name":"Стрижка","itemType":"service","price":1200,"durationMinutes":60,"categoryId":null,"status":"active"}

Обязательны name/itemType/price. Статусы active/draft/archived, is_active согласован со статусом. Service требует durationMinutes 1..1440; price ≥0, discountPercent 0..100, stockQuantity ≥0 или null. Бизнес должен поддерживать тип позиции. Тип существующей позиции неизменяем. DTO пока не управляет изображениями, вариантами, составными услугами, SKU и маркировкой; PUT сохраняет эти дополнительные поля старого каталога.

Категория: {"name":"Услуги","parentId":null,"sortOrder":0}. ParentId/categoryId должны принадлежать проекту и не быть архивными. Циклы запрещены. Архив категории с неархивными позициями/дочерними категориями — 409 category_in_use; сначала перенести или архивировать содержимое. Эти гарантии реализованы в новом контракте; старые API BP требуют отдельного аудита на те же свойства.

5. Повторы и транзакции

Ключ уникален в provider+projectId и связан с точными байтами тела, методом и путём. Пробелы JSON и другой URL меняют hash: Velvio повторяет исходные байты без пересериализации. JWT/X-Request-Id можно обновить.

Успешный повтор возвращает прежний статус и неизменное тело, даже если сущность потом изменена. Idempotency-Replayed:true отличает повтор. Другой запрос с этим ключом — 409. Ошибки/откатившиеся транзакции не сохраняют успешную квитанцию.

Advisory lock PostgreSQL сериализует команды проекта, включая provisioning. Изменение, событие и ответ коммитятся атомарно; stores, открывающие отдельные соединения, внутри транзакции не вызываются. Падение до commit откатывает всё; потеря HTTP-ответа после commit разрешается повтором.

GET deadline 5 секунд, команда 15. После timeout/503/потери соединения повторять с прежним ключом; новое изменение — новый ключ. Квитанции автоматически не удаляются: нет скрытого TTL, обрывающего гарантию. Политику хранения нужно согласовать до большого потока.

6. События

Таблица Хранение
integration_tenants projectId ↔ businessId, статус, time zone
integration_idempotency hash, HTTP-статус, точные JSON-байты ответа
integration_outbox payload, sequence/eventId, lease, попытки, ack
integration_webhook_deliveries Время/номер попытки, HTTP-статус, успех; без тела ответа и секретов

Интеграция использует PostgreSQL outbox. Redis-корзины и RabbitMQ-уведомления BP сохраняют свои задачи, для этого контура они не нужны.

Формат: {eventId,projectId,type,entityId,version:1,occurredAt,data}. Version — версия схемы, не ревизия сущности. События не содержат контакты клиентов. catalog.changed.data: collection=catalog_items/categories, operation=insert/update/delete. SQL-триггеры охватывают и изменения каталога через старый кабинет BP. profile.changed пока создаёт только новый API. Событий записей/заказов/платежей ещё нет.

POST идёт на один заранее настроенный URL Velvio, не принимаемый от проекта/браузера. HTTPS, без redirect; 5 секунд на отправку. Только Development допускает HTTP loopback.

X-Velvio-Event-Id: <eventId>
X-Velvio-Timestamp: <Unix seconds>
X-Velvio-Signature: sha256=<lowercase hex HMAC>

Подпись: HMAC-SHA256(secret, UTF8(timestamp + "." + rawBody)). Receiver проверяет timestamp (рекомендуемое окно 5 минут), подпись constant-time до разбора JSON и соответствие eventId в теле/заголовке. Сохранение eventId и изменение проекции — одна транзакция. Любой 2xx означает durable ack: сначала сохранить, затем ответить.

Доставка как минимум один раз. Повтор сохраняет eventId и байты тела; timestamp/подпись новые. Backoff 2,4,8…600 секунд, без лимита попыток. Lease 30 секунд восстанавливается после падения; workers используют SKIP LOCKED. Worker не обгоняет уже видимые pending-события проекта; строгая ревизия при любых конкурентных старых каналах не гарантируется. Velvio обрабатывает событие как инвалидацию и перечитывает актуальные данные. Проблемный проект не блокирует другие.

Без URL события накапливаются. До включения нужны мониторинг числа/возраста pending, attempts, 401/403 receiver, зависших lease и политика хранения. Недоступность receiver не отменяет сохранённые бизнес-команды.

7. Настройка, выпуск и проверка

Защищённая runtime-конфигурация, без действующих credentials в репозитории:

{"Integrations":{"Velvio":{"Enabled":false,"KeyId":"velvio-key-1","PublicKeyPem":"<RSA PUBLIC KEY PEM>","WebhookUrl":"https://<velvio-server>/internal/business-platform/events","WebhookSecret":"<separate random secret: at least 32 UTF-8 bytes>"}}}

Enabled=true без корректного RSA-ключа запрещает старт. Неверные webhook URL/secret тоже отклоняются. JWT-ключ и HMAC secret различаются. Одновременная ротация нескольких JWT-ключей пока не реализована; согласовать смену с пяти минутным сроком токенов.

  1. Согласовать DTO с Velvio; подготовить серверный JWT issuer и durable receiver.
  2. Backup businessplatform, проверка на staging. Через DbInspector применить только 037, без migrate-all/demo seed: четыре таблицы, categories.is_archived, два триггера.
  3. Развернуть образ с отключённой интеграцией, проверить старые каналы, сеть/nginx и runtime secrets.
  4. Включить и проверить реальный Velvio BFF: проект → профиль → категория → позиция → повтор → webhook.
  5. Следующие этапы: мастера/ресурсы/расписание, конкурентное бронирование/holds, заказы/платежи/возвраты. Capabilities включать после проверки соответствующего пути.

Важно для оператора: одиночная команда DbInspector migrate выполняет SQL, но не обновляет schema_migrations. При ручном выполнении SQL включать запись filename 037_velvio_integration.sql в существующий трекер в ту же транзакцию. В выпуске 20261007-145000 DDL и трекер записаны атомарно через контролируемый SQL; не отмечать миграцию до выполнения. При прерывании сначала проверить четыре таблицы, колонку и триггеры, а не запускать SQL повторно вслепую. Изолированный fixture не является рабочим трекером релизов.

Для отката приложения отключить интеграцию и оставить таблицы/квитанции/outbox. Старый образ не использует новые таблицы; default категории безопасен. Снятие триггеров — отдельная контролируемая операция после остановки команд; не удалять накопленные данные ради отката.

Проверки tools/VelvioChecks: реальные PostgreSQL 17, TestServer, RSA JWT, SQL; старые seeds/транспорты не стартуют. Harness допускает только пустую БД bp_velvio_test* на loopback/postgres-test. schema.sql — структура шести существующих таблиц без строк/credentials. Подготовка: два явных вызова DbInspector — schema.sql, затем 037. GitHub Actions содержит отдельный job с временной PostgreSQL.

07.10.2026 прошли 59 интеграционных сценариев и 35 прежних regression checks: 12 одновременных provisioning/созданий позиции, scopes, чужие IDs, атомарный rollback после принудительного сбоя, HMAC/retry/lease. Сборка всех проектов прошла. Уязвимая транзитивная OpenAPI 2.4.1 закреплена на исправленной 2.7.5; Swagger проверен. Удалённый CI ещё не запускался. Рабочая БД и приложения этой подготовкой не менялись.

Общий импорт каталога — 17 октября 2026

Импорт принадлежит общему /api/v1/commerce/projects/{projectId}, а не пространству имён Velvio. BFF передаёт multipart source/file (100 MiB) или YML URL, получает непосредственно Job DTO с HTTP 202, опрашивает GET до ready и отдельно подтверждает apply. Список возвращает {items,total,offset,limit}. Права — строго commerce:imports:read / commerce:imports:write; commerce:import не поддерживается. Уже существующие общие commerce:read/write покрывают соответствующие операции.

Поддерживаемые source: yandex_yml, yandex_market_xlsx, wildberries_xlsx, ozon_xlsx (включая бинарный .xls), shopify_csv. Нового CSV Velvio нет: шаблон совместим с Shopify. Нужен только существующий серверный JWT; приватные storage credentials остаются на BP. Изображения скачивает worker в хранилище BP, dry-run каталог не меняет. Применение только явное, режимы update_existing/create_only; отсутствующие продукты/варианты не удаляются.

Точные пути, DTO, состояния, ограничения форматов, конфигурация и проверки описаны в контракте импорта. Миграция 040 и включение CommerceImports выполняются при отдельном выпуске; наличие этого описания само по себе не подтверждает deploy. Фоновая очередь и история используют PostgreSQL, без новой зависимости от Redis/RabbitMQ. Доставка HTTP-шлюза, upload/progress/report и пользовательская приёмка интерфейса Velvio остаются на стороне Velvio.

Развёрнуто 17.10.2026: импорт включён в выпуске 20261017-095952, код 4f4faa6; миграция 040 применена. 113 проверок импорта, 227 Commerce, 59 Velvio, 35 regression и живой smoke из 37 запросов прошли. Собственные изображения доступны по HTTPS, storage — постоянный FileSystem volume BP. HTTP-шлюз и пользовательский путь Velvio проверяются отдельно. См. квитанции catalog-import-*.json в docs/development.

8. Источники

Экспорт общего каталога: GET /api/v1/commerce/projects/{projectId}/exports/shopify.csv и /exports/yandex.yml?baseUrl=https%3A%2F%2Fshop.example. Синхронные UTF-8 файлы attachment, scope commerce:read или commerce:imports:read. Без baseUrl YML не содержит выдуманных ссылок. CSV использует общий Shopify заголовок и стандартный product metafield для обратимого переноса расширенных полей BP. Подробности и ограничения — контракт экспорта.