{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Приёмочный контракт интеграции с Инвойсбокс API",
  "description": "Машиночитаемый список проверок, после которых интеграцию можно считать готовой к бою. Источник — чеклист запуска https://docs.invoicebox.ru/go-live/. Каждая проверка формулируется как утверждение, которое интеграция обязана доказать тестом.",
  "version": "2026-08-11",
  "checks": [
    {
      "id": "auth-token-works",
      "assert": "GET /v3/security/api/auth/auth с боевым токеном возвращает 200 и непустой data.userId",
      "note": "200 с userId: null — это анонимный ответ, а не успешная авторизация",
      "goLiveItem": "secrets"
    },
    {
      "id": "create-returns-same-number",
      "assert": "Создание заказа возвращает тот же merchantOrderId, что был отправлен, и HTTPS-ссылку paymentUrl",
      "goLiveItem": "idempotency"
    },
    {
      "id": "timeout-recovery-no-duplicate",
      "assert": "Повтор после таймаута или 5xx не создаёт второй заказ: сначала выборка по merchantOrderId, создание — только при пустом ответе",
      "note": "Проверяется в том числе двумя конкурентными воркерами: без замка на стороне магазина оба увидят пустую выборку",
      "goLiveItem": "idempotency"
    },
    {
      "id": "server-uniqueness-enabled",
      "assert": "Серверная проверка уникальности merchantOrderId включена: повторный POST с использованным номером возвращает merchant_order_id_duplicate",
      "note": "Включается тумблером «Проверять уникальность номера заказа» в личном кабинете app.invoicebox.ru (Мои продажи → Магазины → Общая информация); для боевого запуска обязательна",
      "goLiveItem": "idempotency"
    },
    {
      "id": "webhook-happy-path",
      "assert": "Уведомление completed с валидной подписью, совпадающими merchantId, валютой и суммой меняет локальный статус заказа ровно один раз",
      "goLiveItem": "signature"
    },
    {
      "id": "webhook-duplicate-ignored",
      "assert": "Повторная доставка того же уведомления не повторяет бизнес-эффект и возвращает success",
      "goLiveItem": "webhook-idempotency"
    },
    {
      "id": "webhook-out-of-order",
      "assert": "Уведомление со «старым» статусом после терминального не откатывает локальное состояние",
      "note": "Ключ дедупликации — заголовок X-Event-Id: у всех попыток доставки одного уведомления он один, а поле id в теле — идентификатор ЗАКАЗА. Переход применяйте только вперёд по паре id + status: подпись считается от тела и заголовки не покрывает",
      "goLiveItem": "webhook-idempotency"
    },
    {
      "id": "webhook-bad-signature",
      "assert": "Неверная подпись или несовпадающая сумма возвращают HTTP 200 с {\"status\":\"error\",\"code\":...} по документированному перечню кодов",
      "goLiveItem": "signature"
    },
    {
      "id": "reconciliation-catches-missed",
      "assert": "После намеренно пропущенного уведомления регулярная сверка выборкой находит расхождение и поднимает алерт",
      "note": "Сверка — с водяным знаком и нахлёстом; см. раздел «Если повторы исчерпаны» на странице уведомлений",
      "goLiveItem": "webhook-idempotency"
    },
    {
      "id": "money-ops-need-human",
      "assert": "Возвраты и отмены оплаченных заказов не выполняются без подтверждения человека",
      "goLiveItem": "errors"
    },
    {
      "id": "no-demo-credentials-in-prod",
      "assert": "Демо-токен и демо-магазин отсутствуют в боевой сборке и конфигурации",
      "goLiveItem": "secrets"
    },
    {
      "id": "no-secrets-in-logs-decimal-math",
      "assert": "Токены и персональные данные не попадают в логи; денежная арифметика в тестах и коде — Decimal/BigDecimal, не двоичный float",
      "goLiveItem": "logging"
    }
  ]
}
