openapi: 3.1.0
info:
  title: Syncra Merchant API V2
  version: "2.0.0"
  description: |
    P2P-эквайринговый шлюз — Merchant API (канонический контракт V2).

    Покрывает аутентификацию, управление API-ключами, жизненный цикл мерчанта,
    PayIn/PayOut-сделки, балансы, пропуски белого списка, доставку вебхуков,
    крипто-выводы, диспуты (апелляции), банки и выдачу TRC-20-адресов
    для депозитов.

    ## Соглашения
    - Все денежные суммы — **int64 в минорных единицах валюты** (копейки для RUB,
      центы для USD/USDT, сатоши для BTC). Без float и decimal.
    - Все временные метки — **RFC 3339** UTC (например `2026-08-10T12:34:56Z`).
    - Все идентификаторы — **строковые UUID** (v4), если не указано иное.
    - Доступность валют и платёжных методов зависит от конфигурации тенанта.
    - Конвенция обёрток ответа (с 2026-08-24): ОДИНОЧНЫЙ ресурс отдаётся
      как proto-объект верхнего уровня (`{"deal": {...}}`); КАЖДЫЙ list-эндпоинт
      возвращает каноническую обёртку `{"data": [...], "next_page_token": ""}`
      (AIP-158) — имена массивов per-entity и `total_count` из внутреннего
      gRPC-контракта никогда не показываются на HTTP. `GET /balance` —
      единственное исключение (проекция баланса, а не листинг:
      `{"wallets": [...]}`).
    - `tenant_id` / `merchant_id` в телах запросов инжектятся API-шлюзом
      из аутентифицированного контекста мерчанта; клиентам следует их опускать.

    ## Аутентификация
    Каждый запрос (кроме `POST /auth`) ОБЯЗАН нести:
      - `X-Merchant-Token: <64-char hex>` — публичный токен мерчанта.
      - `X-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>`
        где `hex_hmac = HMAC-SHA256(webhook_secret, "<unix_seconds>.<raw_body>")`.
    Метка времени защищает от replay-атак (максимальное расхождение 5 минут
    плюс 60 секунд допуска в будущее).
  contact:
    name: Syncra Support
    url: https://docs.syncra.money
    email: support@syncra.money
  license:
    name: Proprietary

servers:
  - url: https://api.syncra.money
    description: Продакшен
  - url: http://localhost:8080
    description: Локальная разработка

tags:
  - name: Authentication
    x-displayName: Аутентификация
    description: Проверка токена и HMAC-подписи (вызывается API-шлюзом).
  - name: API Keys
    x-displayName: API-ключи
    description: Выпуск и ротация учётных данных мерчанта.
  - name: Merchants
    x-displayName: Мерчанты
    description: Жизненный цикл мерчанта (kill-switch, включение/отключение, адрес депозитов).
  - name: Management
    x-displayName: Профиль мерчанта
    description: Профиль мерчанта для самообслуживания и лимиты (HMAC или JWT кабинета).
  - name: Payments
    x-displayName: Платежи
    description: Создание PayIn-сделок (эквайринг) и PayOut-выплат.
  - name: Deals
    x-displayName: Сделки
    description: Получение и листинг сделок.
  - name: Finance
    x-displayName: Финансы
    description: Балансы и крипто-выводы (USDT).
  - name: Passes
    x-displayName: Пропуски
    description: Управление пропусками белого списка (обход каскада).
  - name: Callbacks
    x-displayName: Доставка вебхуков
    description: Журнал доставок вебхуков.
  - name: Disputes
    x-displayName: Диспуты
    description: Апелляции (диспуты) и сообщения в чате апелляции.
  - name: Reference
    x-displayName: Справочники
    description: Справочник банков и платёжных методов.
  - name: Webhooks
    x-displayName: Вебхуки
    description: Исходящие события вебхуков, доставляемые на эндпоинты мерчанта.

security:
  - MerchantToken: []
    MerchantSignature: []
    MerchantTimestamp: []

paths:
  # ─── Authentication ─────────────────────────────────────────────────────────
  /api/v1/p2p/merchant/auth:
    post:
      operationId: authenticateMerchant
      summary: Проверка учётных данных мерчанта
      description: |
        Проверяет токен мерчанта и подпись HMAC-SHA256. Вызывается auth-интерцептором
        API-шлюза на каждый запрос. Публичные клиенты НЕ вызывают этот эндпоинт
        напрямую — это каноническая точка входа верификации. При успехе возвращает
        резолвнутые `tenant_id` и `merchant_id`.
      tags: [Authentication]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthenticateMerchantRequest'
      responses:
        '200':
          description: Учётные данные проверены
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthenticateMerchantResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── API Key Management ─────────────────────────────────────────────────────
  /api/v1/p2p/merchant/api-keys:
    post:
      operationId: issueApiKeys
      summary: Выпуск новой пары API-ключей
      description: |
        Генерирует для мерчанта новую пару токен + секрет для подписи вебхуков.
        Предыдущие учётные данные немедленно аннулируются. `secret` показывается
        ровно ОДИН раз — храните его надёжно.
      tags: [API Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssueAPIKeysRequest'
      responses:
        '200':
          description: Новые учётные данные выпущены
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssueAPIKeysResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/api-keys/rotate:
    post:
      operationId: rotateApiKeys
      summary: Ротация API-ключей
      description: |
        Атомарно заменяет существующие учётные данные. Возвращает новый токен и секрет.
        Grace-период ротации (подтверждённое поведение): исходящая пара остаётся
        валидной ограниченное окно (по умолчанию 15 минут, настраивается оператором
        через `MERCHANT_KEY_GRACE_PERIOD`, `0` отключает). Grace строго парный —
        в течение окна принимается только ПОЛНАЯ старая пара (старый токен + старый
        секрет); смешанные комбинации возвращают 401. Исходящие вебхуки подписываются
        НОВЫМ секретом немедленно.
      tags: [API Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateAPIKeysRequest'
      responses:
        '200':
          description: Учётные данные ротированы
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateAPIKeysResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Merchant Lifecycle ─────────────────────────────────────────────────────
  /api/v1/p2p/merchant/{merchant_id}/disable:
    post:
      operationId: disableMerchant
      summary: Активация kill-switch мерчанта
      description: |
        Устанавливает `disabled_at` на записи мерчанта. Все последующие попытки
        аутентификации этого мерчанта возвращают `PERMISSION_DENIED`.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisableMerchantRequest'
      responses:
        '200':
          description: Мерчант отключён
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/{merchant_id}/enable:
    post:
      operationId: enableMerchant
      summary: Снятие kill-switch мерчанта
      description: Обнуляет `disabled_at`, снова разрешая аутентификацию.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnableMerchantRequest'
      responses:
        '200':
          description: Мерчант включён
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchants/{merchant_id}/kill-switch:
    post:
      operationId: toggleMerchantKillSwitch
      summary: Переключение kill-switch мерчанта (только админ, без тела)
      description: |
        Переключает состояние kill-switch — toggle без тела (каждый вызов инвертирует
        disabled ↔ enabled); полей запроса вида `active` или `reason` НЕ существует.
        Только для админов: требуется JWT-сессия оператора платформы (роль
        `admin`/`platform_admin`); HMAC-пары и сессии кабинета мерчанта
        получают 403. Админский алиас:
        `POST /api/v1/p2p/admin/merchants/{merchant_id}/kill-switch`.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      responses:
        '200':
          description: Kill-switch переключён
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToggleKillSwitchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Требуется роль админа (HMAC/сессии мерчанта отклоняются).

  /api/v1/p2p/merchants/{merchant_id}/deposit-address:
    post:
      operationId: generateMerchantDepositAddressLegacy
      summary: Выдача (или ротация) TRC-20-адреса депозитов мерчанта (legacy-путь)
      description: |
        Legacy-алиас с административной имперсонацией канонического
        `POST /api/v1/p2p/merchant/wallet/deposit-address` (тот же обработчик, тот же
        ответ). Позволяет оператору действовать от имени мерчанта. При HMAC-аутентификации
        `merchant_id` в пути ДОЛЖЕН совпадать с claims токена;
        вызывающие с JWT мерчанта могут обращаться только к собственному мерчанту
        (защита от IDOR). Занимает следующий индекс вывода
        BIP44, выводит адрес через tron-wallet-api и сохраняет адрес + индекс
        обратно в запись мерчанта. Ранее выданные адреса остаются в вечном наборе
        блок-поллинга — депозиты, отправленные на заменённый адрес, по-прежнему
        обнаруживаются и зачисляются.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateDepositAddressRequest'
      responses:
        '200':
          description: Адрес депозитов сгенерирован
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateDepositAddressResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Payments (PayIn / PayOut) ──────────────────────────────────────────────
  /api/v1/p2p/merchant/deals/payin:
    post:
      operationId: createPayIn
      summary: Создание PayIn-сделки (эквайринг)
      description: |
        Создаёт новую сделку входящего платежа с опциональным матчингом пропусков
        белого списка. Идемпотентно: дубликат `idempotency_key` возвращает
        закешированный результат. Возвращает публичный `payment_url`, на который
        мерчанту следует редиректить клиента.
      tags: [Payments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePayInRequest'
      responses:
        '200':
          description: PayIn-сделка создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayInResponse'
              example:
                deal:
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  status: "ESCROW_LOCKED"
                  amount: "150000"
                  currency: "RUB"
                  created_at: "2026-08-10T12:00:00Z"
                  client_id: "player_42"
                  deal_type: "PAYIN"
                  updated_at: "2026-08-10T12:00:00Z"
                  payment_url: "https://checkout.syncra.money/pay/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/NoTraderAvailable'

  /api/v1/p2p/merchant/deals/payout:
    post:
      operationId: createPayOut
      summary: Создание PayOut-сделки (выплата)
      description: |
        Инициирует выплату на переданные целевые реквизиты. Проверяет баланс
        мерчанта и размещает USDT-холд (курс SELL) перед созданием сделки. Далее
        следует синхронный каскадный авто-матчинг: при наличии свободного трейдера
        сделка возвращается в `MATCHED`; иначе — в `UNASSIGNED`, и worker повторного
        матчинга продолжает подбор, пока SLA назначения не истечёт сделку
        (`EXPIRED`, холд освобождается).
        Возвращает сделку с заполненным `target_requisite` получателя и
        `currency` как эхом из запроса (ISO-код фиатной валюты выплаты).
      tags: [Payments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePayOutRequest'
      responses:
        '200':
          description: PayOut-сделка создана
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayOutResponse'
              example:
                deal:
                  deal_id: "8bd2590f-2345-6789-abcd-ef1234567890"
                  status: "MATCHED"
                  amount: "500000"
                  currency: "RUB"
                  created_at: "2026-08-10T12:05:00Z"
                  client_id: "player_42"
                  deal_type: "PAYOUT"
                  updated_at: "2026-08-10T12:05:00Z"
                  target_requisite: "2202201234567890"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/NoTraderAvailable'

  # ─── Deals ──────────────────────────────────────────────────────────────────
  /api/v1/p2p/merchant/deals/{deal_id}:
    get:
      operationId: retrieveDeal
      summary: Получение сделки по ID
      description: Возвращает детали одной сделки в рамках мерчанта.
      tags: [Deals]
      parameters:
        - $ref: '#/components/parameters/DealIdPath'
      responses:
        '200':
          description: Детали сделки
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveDealResponse'
              example:
                deal:
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  status: "PAYMENT_VERIFIED"
                  amount: "150000"
                  currency: "RUB"
                  created_at: "2026-08-10T12:00:00Z"
                  client_id: "player_42"
                  deal_type: "PAYIN"
                  updated_at: "2026-08-10T12:06:42Z"
                  payment_url: "https://checkout.syncra.money/pay/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/p2p/merchant/deals/{deal_id}/cancel:
    post:
      operationId: cancelDeal
      summary: Отмена PayIn-сделки
      description: |
        Явно отменяет депозитную (PayIn) сделку до верификации платежа.
        Разрешено только из `INITIALIZED`, `ESCROW_LOCKED` или
        `PAYMENT_NOTIFIED`; эскроу-холды снимаются автоматически, разбивка
        комиссии обнуляется, а матчи помечаются `FAILED`. Повторная отмена уже
        `CANCELLED`-сделки возвращает её без изменений (идемпотентно).
        Клиенты отменяют через публичный эндпоинт checkout
        `POST /api/v1/p2p/checkout/{hash}/cancel`.
      tags: [Deals]
      parameters:
        - $ref: '#/components/parameters/DealIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Необязательная произвольная причина отмены (только в аудит-лог).
                  maxLength: 512
      responses:
        '200':
          description: Сделка отменена (или уже отменена — идемпотентно)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveDealResponse'
              example:
                deal:
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  status: "CANCELLED"
                  amount: "150000"
                  currency: "RUB"
                  created_at: "2026-08-10T12:00:00Z"
                  client_id: "player_42"
                  deal_type: "PAYIN"
                  updated_at: "2026-08-10T12:04:10Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/p2p/merchant/deals/{deal_id}/amount:
    post:
      operationId: updateDealAmount
      summary: Изменение фиатовой суммы живой сделки (оператор)
      description: |
        Меняет фиатовую сумму сделки, ещё не достигшей
        `PAYMENT_VERIFIED` (`INITIALIZED`, `ESCROW_LOCKED`,
        `PAYMENT_NOTIFIED`, `SOFT_DISPUTED` — сумма изменяема до
        авторизации). Атомарно в одной транзакции:
        USDT-эквивалент пересчитывается по собственному фиксированному курсу сделки,
        каждый эскроу-холд (order-level и per-match) пересоздаётся под новую
        сумму, пишется append-only аудит-строка и ставится в очередь событие
        вебхука `deal.amount_changed` (только V2). Идемпотентно по
        `idempotency_key` (тот же ключ + та же сумма → replay; тот же ключ +
        другая сумма → 409). Оптимистичная блокировка через
        `expected_amount_version` (If-Match): конкурентное изменение отказывает
        с 409.
      tags: [Deals]
      parameters:
        - $ref: '#/components/parameters/DealIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount_fiat, reason, idempotency_key]
              properties:
                amount_fiat:
                  type: integer
                  format: int64
                  description: Новая фиатовая сумма в минорных единицах (копейки).
                reason:
                  type: string
                  description: Обоснование оператора, фиксируется дословно в append-only аудите.
                  maxLength: 512
                idempotency_key:
                  type: string
                  format: uuid
                  description: Ключ идемпотентности действия оператора.
                expected_amount_version:
                  type: integer
                  format: int64
                  description: |
                    Версия amount_version, которую оператор видел последней (If-Match). 0 или
                    не задано — не проверять; несовпадение → 409, перечитать сделку.
      responses:
        '200':
          description: Сумма сделки изменена (или идемпотентный replay)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveDealResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Конфликт — несовпадение версии/ключа или сделка уже за PAYMENT_VERIFIED.

  /api/v1/p2p/appeals/{appeal_id}/settlement:
    post:
      operationId: createAppealSettlement
      summary: Зачёт полученных средств EXPIRED-сделки как НОВОЙ сделки (оператор)
      description: |
        Settle-as-received: для EXPIRED-родителя, фактически получившего
        средства (`received_amount` > 0), резолв апелляции создаёт НОВУЮ
        COMPLETED-сделку-сеттлмент на полученную сумму по курсу,
        зафиксированному в момент резолва. Истёкший оригинал остаётся EXPIRED
        навсегда (терминальные статусы неизменяемы). Сеттлмент несёт
        `parent_deal_id` / `origin_appeal_id`; одна апелляция сеттлится максимум
        один раз (UNIQUE-констрейнт — повторный вызов возвращает существующий
        сеттлмент). Эмитит штатную последовательность вебхуков
        `deal.created` … `deal.completed` для сеттлмента.
      tags: [Disputes]
      parameters:
        - name: appeal_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [idempotency_key]
              properties:
                received_amount_override:
                  type: integer
                  format: int64
                  description: Ручное переопределение received_amount родителя; при задании обязательна `reason` (аудит-флаг).
                reason:
                  type: string
                  description: Обязательна, когда задан received_amount_override.
                  maxLength: 512
                idempotency_key:
                  type: string
                  format: uuid
                  description: Ключ идемпотентности действия оператора; дополнительно гарантируется констрейнтом UNIQUE(origin_appeal_id).
      responses:
        '200':
          description: Сеттлмент создан (или идемпотентный replay существующего)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveDealResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Конфликт — родитель не EXPIRED, апелляция уже разрешена или received_amount равен нулю.

  /api/v1/p2p/merchant/deals:
    get:
      operationId: listDeals
      summary: Листинг сделок мерчанта
      description: |
        Возвращает сделки мерчанта с фильтрацией по типу, статусу и дате.
        Листинг — UNION записей PayIn (`p2p_orders`) и PayOut
        (`p2p_payouts`) в одном хронологическом списке. Пагинация
        keyset-based через `page_token`. Область видимости всегда выводится из
        подписанного токена мерчанта (claims приоритетны); поле запроса
        `merchant_id` носит информационный характер и молча игнорируется.
      tags: [Deals]
      parameters:
        - name: deal_type
          in: query
          schema:
            $ref: '#/components/schemas/DealType'
          description: Фильтр по направлению сделки.
        - name: statuses
          in: query
          schema:
            type: array
            items:
              anyOf:
                - $ref: '#/components/schemas/OrderStatus'
                - $ref: '#/components/schemas/PayOutStatus'
          style: form
          explode: true
          description: Фильтр по одному или нескольким статусам сделки (OrderStatus для PayIn, PayOutStatus для PayOut — UNION-листинг принимает оба набора).
        - name: created_after
          in: query
          schema:
            type: string
            format: date-time
          description: Только сделки, созданные не раньше этой метки времени (RFC 3339, включительно).
        - name: created_before
          in: query
          schema:
            type: string
            format: date-time
          description: Только сделки, созданные не позже этой метки времени (RFC 3339, включительно).
        - $ref: '#/components/parameters/PageSizeQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: Список сделок
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDealsResponse'
              example:
                data:
                  - deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                    status: "COMPLETED"
                    amount: "150000"
                    currency: "RUB"
                    created_at: "2026-08-10T12:00:00Z"
                    client_id: "player_42"
                    deal_type: "PAYIN"
                    updated_at: "2026-08-10T12:10:00Z"
                    payment_url: "https://checkout.syncra.money/pay/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
                  - deal_id: "8bd2590f-2345-6789-abcd-ef1234567890"
                    status: "MATCHED"
                    amount: "500000"
                    currency: "RUB"
                    created_at: "2026-08-10T12:05:00Z"
                    client_id: "player_43"
                    deal_type: "PAYOUT"
                    updated_at: "2026-08-10T12:05:00Z"
                    target_requisite: "2202201234567890"
                next_page_token: "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0xMFQxMjowNTowMFoifQ=="
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Finance ────────────────────────────────────────────────────────────────
  /api/v1/p2p/merchant/balance:
    get:
      operationId: getBalance
      summary: Получение балансов мерчанта
      description: |
        Возвращает реальные кошельки мерчанта из money-сервиса — по одной строке
        на фактический кошелёк с собственной валютой и `wallet_id` (без синтетических
        строк). `frozen` отражает активные холды.
      tags: [Finance]
      responses:
        '200':
          description: Балансы мерчанта
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'
              example:
                wallets:
                  - wallet_id: "f0e1d2c3-b4a5-6789-abcd-ef1234567890"
                    currency: "RUB"
                    available: "12500000"
                    frozen: "500000"
                    total: "13000000"
                  - wallet_id: "9b2fcbd6-b6f4-4f17-a94d-b7309f84bde5"
                    currency: "USDT"
                    available: "2500000"
                    frozen: "0"
                    total: "2500000"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/wallet/withdraw:
    post:
      operationId: withdrawCrypto
      summary: Вывод USDT на TRC-20-адрес
      description: |
        Инициирует вывод USDT на переданный TRC-20-адрес. Синхронный
        `status` — COMPLETED (авто-одобрение: on-chain-транзакция отправлена
        в broadcast) или PENDING_MANUAL (очередь одобрения админом — политика
        MANUAL_ONLY или сумма выше действующего авто-лимита). Недостаточный
        баланс — failed precondition → HTTP 400 (не 402). Идемпотентность
        гарантируется на стороне сервера (детерминированный ключ из merchant + address
        + amount + текущая комиссия).
      tags: [Finance]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCryptoWithdrawRequest'
      responses:
        '200':
          description: Вывод выполнен (COMPLETED) или поставлен в очередь на одобрение (PENDING_MANUAL)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CryptoWithdrawResponse'
              example:
                status: "COMPLETED"
                transaction_id: "0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
                amount_usdt: "1000"
                tron_address: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
                created_at: "2026-08-10T13:00:00Z"
                fee_usdt_cents: "150"
                net_usdt_cents: "99850"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/wallet/deposit-address:
    get:
      operationId: getMerchantDepositAddress
      summary: Получение текущего TRC-20-адреса депозитов мерчанта
      description: |
        Только чтение: возвращает текущий активный TRC-20-адрес депозитов
        мерчанта, используемый для входящего USDT-обеспечения (пополнение
        операционного баланса мерчанта — выплаты, выводы и комиссии; клиенты в
        PayIn-сделках платят по реквизитам трейдеров, а НЕ на этот адрес).
        `tenant_id` / `merchant_id` в запросе носят информационный характер — при HMAC-аутентификации
        область видимости всегда резолвится из подписанного токена.
        Возвращает `404 Not Found` (problem+json), если адрес ещё
        не выдан.
      tags: [Finance]
      responses:
        '200':
          description: Текущий адрес депозитов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetDepositAddressResponse'
              example:
                trc20_address: "TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: |
            Адрес депозитов ещё не выдан — выполните POST /wallet/deposit-address,
            чтобы сгенерировать его.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                type: "about:blank"
                title: "Not Found"
                status: 404
                detail: "deposit address not provisioned yet — POST /wallet/deposit-address to generate one"
                instance: "/api/v1/p2p/merchant/wallet/deposit-address"

    post:
      operationId: generateMerchantDepositAddress
      summary: Выдача (или ротация) TRC-20-адреса депозитов мерчанта
      description: |
        Генерирует (первый вызов) или ротирует (повторные вызовы) TRC-20-адрес
        депозитов мерчанта для входящего USDT-обеспечения. Область видимости
        резолвится из аутентифицированного контекста мерчанта (канонический путь,
        без `merchant_id` в пути). Атомарно занимает следующий индекс вывода
        BIP44, выводит адрес через tron-wallet-api и сохраняет адрес + индекс
        в запись мерчанта. Семантика ротации: новый адрес немедленно заменяет
        старый в записи мерчанта, при этом каждый ранее выданный адрес навсегда
        остаётся в вечном наборе блок-поллинга платформы — USDT, отправленные
        на заменённый адрес, по-прежнему обнаруживаются и зачисляются.
        Legacy-алиас с административной имперсонацией:
        `POST /api/v1/p2p/merchants/{merchant_id}/deposit-address`.
      tags: [Finance]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateDepositAddressRequest'
      responses:
        '200':
          description: Адрес депозитов сгенерирован (или ротирован)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateDepositAddressResponse'
              example:
                trc20_address: "TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a"
                wallet_derivation_index: 17
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Passes (Whitelist) ─────────────────────────────────────────────────────
  /api/v1/p2p/merchant/passes:
    post:
      operationId: createPass
      summary: Создание пропуска белого списка
      description: |
        Создаёт новый пропуск белого списка в статусе `ACTIVE`. Пропуска обходят
        стандартную каскадную маршрутизацию, когда все непустые поля совпадают
        с входящей сделкой.
      tags: [Passes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePassRequest'
      responses:
        '200':
          description: Пропуск создан
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePassResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      operationId: listPasses
      summary: Листинг пропусков белого списка
      description: Возвращает пропуски, отфильтрованные по мерчанту.
      tags: [Passes]
      parameters:
        - $ref: '#/components/parameters/PageSizeCompactQuery'
      responses:
        '200':
          description: Список пропусков
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListPassesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Callbacks ──────────────────────────────────────────────────────────────
  /api/v1/p2p/merchant/callbacks:
    get:
      operationId: listCallbacks
      summary: Листинг записей доставки вебхуков
      description: |
        Возвращает записи доставки вебхуков мерчанта с опциональной фильтрацией
        по сделке. Привязка-алиас: `GET /api/v1/p2p/callbacks`.
      tags: [Callbacks]
      parameters:
        - name: deal_id
          in: query
          schema:
            type: string
            format: uuid
          description: Фильтр записей доставки по ID сделки.
        - $ref: '#/components/parameters/PageSizeCompactQuery'
      responses:
        '200':
          description: Список записей доставки вебхуков
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCallbacksResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Appeals (Disputes) ─────────────────────────────────────────────────────
  /api/v1/p2p/merchant/appeals:
    post:
      operationId: createAppeal
      summary: Открытие диспута (апелляции)
      description: |
        Открывает диспут по конкретной сделке. Мерчант может приложить
        файлы-доказательства (по их UUID).
      tags: [Disputes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAppealRequest'
      responses:
        '200':
          description: Диспут создан
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateAppealResponse'
              example:
                appeal:
                  appeal_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  status: "OPEN"
                  reason: "Funds not credited within 30 minutes"
                  resolution_note: ""
                  created_at: "2026-08-10T12:45:00Z"
                  resolved_at: ""
                  updated_at: "2026-08-10T12:45:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    get:
      operationId: listAppeals
      summary: Листинг диспутов
      description: Возвращает все диспуты мерчанта с опциональной фильтрацией по статусу.
      tags: [Disputes]
      parameters:
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/AppealStatus'
          description: Фильтр по статусу апелляции.
        - $ref: '#/components/parameters/PageSizeCompactQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: Список диспутов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAppealsResponse'
              example:
                data:
                  - appeal_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab"
                    deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                    status: "OPEN"
                    reason: "Funds not credited within 30 minutes"
                    resolution_note: ""
                    created_at: "2026-08-10T12:45:00Z"
                    resolved_at: ""
                    updated_at: "2026-08-10T12:45:00Z"
                  - appeal_id: "d2e3f4a5-6789-0abc-def1-234567890abc"
                    deal_id: "8bd2590f-2345-6789-abcd-ef1234567890"
                    status: "IN_PROGRESS"
                    reason: "Wrong recipient name"
                    resolution_note: ""
                    created_at: "2026-08-09T15:20:00Z"
                    resolved_at: ""
                    updated_at: "2026-08-10T09:10:00Z"
                next_page_token: ""
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/appeals/{appeal_id}:
    get:
      operationId: retrieveAppeal
      summary: Получение диспута по ID
      tags: [Disputes]
      parameters:
        - $ref: '#/components/parameters/AppealIdPath'
      responses:
        '200':
          description: Детали диспута
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveAppealResponse'
              example:
                appeal:
                  appeal_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  status: "IN_PROGRESS"
                  reason: "Funds not credited within 30 minutes"
                  resolution_note: ""
                  created_at: "2026-08-10T12:45:00Z"
                  resolved_at: ""
                  updated_at: "2026-08-10T13:00:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/p2p/merchant/appeals/{appeal_id}/messages:
    post:
      operationId: sendAppealMessage
      summary: Отправка сообщения в диспуте
      description: Отправляет текстовое сообщение (опционально с файловыми вложениями) в чат диспута.
      tags: [Disputes]
      parameters:
        - $ref: '#/components/parameters/AppealIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendAppealMessageRequest'
      responses:
        '200':
          description: Сообщение отправлено
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendAppealMessageResponse'
              example:
                message:
                  message_id: "e3f4a5b6-7890-abcd-ef12-34567890abcd"
                  appeal_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab"
                  sender_role: "MERCHANT"
                  body: "Please check the attached bank transfer screenshot."
                  file_ids:
                    - "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  created_at: "2026-08-10T13:05:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    get:
      operationId: listAppealMessages
      summary: Листинг сообщений чата диспута
      tags: [Disputes]
      parameters:
        - $ref: '#/components/parameters/AppealIdPath'
        - name: page_size
          in: query
          schema: { type: integer, format: int32 }
          description: Зарезервировано контрактом — текущий релиз возвращает всю историю сообщений (пагинация пока не применяется).
        - name: page_token
          in: query
          schema: { type: string }
          description: Зарезервировано контрактом — см. page_size выше (next_page_token остаётся пустым).
      responses:
        '200':
          description: История сообщений чата
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListAppealMessagesResponse'
              example:
                data:
                  - message_id: "e3f4a5b6-7890-abcd-ef12-34567890abcd"
                    appeal_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab"
                    sender_role: "MERCHANT"
                    body: "Please check the attached bank transfer screenshot."
                    file_ids:
                      - "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    created_at: "2026-08-10T13:05:00Z"
                  - message_id: "f4a5b6c7-8901-bcde-f123-4567890abcde"
                    appeal_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab"
                    sender_role: "ADMIN"
                    body: "Received. Reviewing the evidence, will respond within 1 hour."
                    file_ids: []
                    created_at: "2026-08-10T13:10:00Z"
                next_page_token: ""
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ─── Banks & Payment Methods ────────────────────────────────────────────────
  /api/v1/p2p/merchant/banks:
    get:
      operationId: listBanks
      summary: Листинг доступных банков и платёжных методов
      description: |
        Канонический справочник банков тенанта (bank-routing). `bank_name` —
        МАШИННЫЙ код банка платформы (не отображаемое имя): его передают в
        `target_bank` заявок и по нему рисуют 1click-кнопки. Доступность
        (payin_available / payout_available) вычисляется по фактическому
        покрытию — «мёртвая кнопка» невозможна.
      tags: [Reference]
      parameters:
        - name: country
          in: query
          schema:
            type: string
          description: Код страны ISO 3166-1 alpha-2 (например `RU`).
          example: RU
        - name: currency
          in: query
          schema:
            type: string
          description: Код валюты ISO 4217 (например `RUB`).
          example: RUB
        - name: method
          in: query
          schema:
            type: string
          description: |
            UUID или слаг метода — «банки, доступные для КОНКРЕТНОГО метода»
            (источник кнопок для `sbp_rub`, `iban_try`, ...). Без параметра —
            весь справочник тенанта.
          example: sbp_rub
        - name: direction
          in: query
          schema:
            type: string
            enum: [PAY_IN, PAY_OUT]
          description: |
            Направление: `PAY_IN` (кнопки депозита) или `PAY_OUT` (банки
            выплаты). Без параметра — обе стороны; различайте по флагам
            payin_available / payout_available.
          example: PAY_IN
      responses:
        '200':
          description: Список банков
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListBanksResponse'
              example:
                data:
                  - bank_name: "sber"
                    display_name: "Сбербанк"
                    country: "RU"
                    methods: ["SBP"]
                    payin_available: true
                    payout_available: false
                  - bank_name: "tbank"
                    display_name: "Т-Банк"
                    country: "RU"
                    methods: ["SBP", "BANK_CARD"]
                    payin_available: true
                    payout_available: false
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/payment-methods:
    get:
      operationId: listMerchantPaymentMethods
      summary: Листинг активных платёжных методов тенанта
      description: |
        Возвращает платёжные методы в статусе ACTIVE, настроенные для тенанта
        аутентифицированного мерчанта: `id` (UUID), `slug`, `name`, `requisite_types`,
        `currency_code` и `status`.

        Это **канонический каталог методов**: единая ссылка на метод —
        UUID (`id`) или слаг (`slug`) — принимается и
        `POST /deals/payin` (`payment_method_id`), и `POST /deals/payout`
        (`payment_method`); оба формата резолвятся в одну и ту же строку.
        Валюта метода должна совпадать с валютой сделки (PayOut отклоняет
        несовпадение с HTTP 400). `GET /banks` НЕ возвращает идентификаторы
        методов — его поле `methods` содержит только значения `RequisiteType`.
      tags: [Reference]
      responses:
        '200':
          description: Список активных платёжных методов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListMerchantPaymentMethodsResponse'
              example:
                data:
                  - id: "550e8400-e29b-41d4-a716-446655440000"
                    slug: "express-havale"
                    name: "Havale Express (TRY)"
                    requisite_types:
                      - "BANK_TRANSFER"
                    currency_code: "TRY"
                    status: "ACTIVE"
                  - id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
                    slug: "sbp_rub"
                    name: "SBP"
                    requisite_types:
                      - "SBP"
                    currency_code: "RUB"
                    status: "ACTIVE"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/currencies:
    get:
      operationId: listMerchantCurrencies
      summary: Листинг активных валют тенанта
      description: |
        Справочник валют тенанта аутентифицированного мерчанта
        (HMAC claims или JWT кабинета): UUID, ISO 4217 alpha-код и отображаемое
        имя каждой ACTIVE-строки `p2p_currencies`. Используется для форматирования
        сумм выплат и выбора валюты в CreateMerchantPayOut.
      tags: [Reference]
      responses:
        '200':
          description: Список активных валют
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListMerchantCurrenciesResponse'
              example:
                data:
                  - id: "9a3f1c2e-1234-4abc-9def-56789abcdef0"
                    code: "RUB"
                    name: "Российский рубль"
                    status: "ACTIVE"
                  - id: "4ac148fe-19a3-45bb-b992-019fac55b72c"
                    code: "TRY"
                    name: "Türk lirası"
                    status: "ACTIVE"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/deals/export:
    get:
      operationId: exportMerchantDeals
      summary: Экспорт сделок в CSV (канонический путь)
      description: |
        Отдаёт сделки мерчанта — PayIn (orders) И PayOut (payouts),
        UNION по контракту ListMerchantDeals — как CSV-документ RFC 4180.
        Фильтры один в один совпадают с листингом; без пагинации (ограничение
        10 000 строк данных, `truncated: true` при достижении). `Accept: text/csv`
        возвращает сырой документ с
        `Content-Disposition: attachment; filename="deals-YYYYMMDD.csv"`;
        любой другой Accept получает JSON-обёртку
        (`csv`/`file_name`/`row_count`/`truncated`).
        Legacy-зеркало `GET /api/v1/p2p/deals/export` (без префикса /merchant)
        продолжает работать ради wire-compat и устарело — стройте новые
        интеграции на этом каноническом пути.
      tags: [Payments]
      parameters:
        - name: deal_type
          in: query
          schema: { type: string, enum: [PAYIN, PAYOUT] }
          description: Фильтр по направлению; пусто/отсутствует = оба (UNION).
        - name: statuses
          in: query
          schema: { type: array, items: { type: string } }
          description: Повторяемые имена статусов БД (`statuses=COMPLETED&statuses=EXPIRED`).
        - name: created_after
          in: query
          schema: { type: string, format: date-time }
          description: Нижняя граница created_at (RFC3339, включительно).
        - name: created_before
          in: query
          schema: { type: string, format: date-time }
          description: Верхняя граница created_at (RFC3339, включительно).
      responses:
        '200':
          description: "CSV-документ (сырой при Accept: text/csv, иначе JSON-обёртка)"
          content:
            text/csv:
              schema: { type: string }
              example: |
                id,date,type,amount,currency,status,counterparty
                9512d99a-3bbc-4a32-acae-d6ad8bb7e32f,2026-08-23T19:04:28Z,PAYIN,1000.00,RUB,EXPIRED,
            application/json:
              schema:
                $ref: '#/components/schemas/ExportDealsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/deals/turnover:
    get:
      operationId: turnoverMerchantDeals
      summary: Ежедневный оборот сделок (график кабинета)
      description: |
        Одна агрегатная строка на (UTC-день × deal_type × валюта) в ограниченном
        окне дат, суммируется по той же поверхности UNION ALL, что и листинг
        (orders `amount` для PAYIN, payouts `amount_fiat` для PAYOUT).
        `date_from`/`date_to` ОБЯЗАТЕЛЬНЫ, RFC3339; окно ограничено
        92 днями (свыше — InvalidArgument). Несостоявшиеся сделки
        исключаются (orders EXPIRED/SPAM_REJECTED, payouts FAILED/EXPIRED).
        Строки упорядочены по (date ASC, deal_type ASC, currency ASC); дни
        без сделок отсутствуют.
      tags: [Payments]
      parameters:
        - name: date_from
          in: query
          required: true
          schema: { type: string, format: date-time }
          description: Начало окна (RFC3339, включительно).
        - name: date_to
          in: query
          required: true
          schema: { type: string, format: date-time }
          description: Конец окна (RFC3339, включительно; не раньше date_from).
        - name: deal_type
          in: query
          schema: { type: string, enum: [PAYIN, PAYOUT] }
          description: Необязательный фильтр направления; пусто = оба.
      responses:
        '200':
          description: Агрегированные строки оборота
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TurnoverResponse'
              example:
                data:
                  - { date: "2026-08-23", deal_type: "PAYIN", amount_minor: "100000", currency: "RUB" }
                  - { date: "2026-08-23", deal_type: "PAYOUT", amount_minor: "96971", currency: "RUB" }
                next_page_token: "" 
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/callbacks/{callback_id}/retry:
    post:
      operationId: retryMerchantCallback
      summary: Повторная постановка доставки вебхука в очередь (ручной retry)
      description: |
        Ставит в очередь немедленную повторную доставку ОДНОЙ из собственных
        доставок вебхуков вызывающего. Подходят только записи с терминальным
        провалом (статус ERROR или RETRY); PENDING/SUCCESS отклоняются
        с 400 FAILED_PRECONDITION. Rate limit: не более одного ручного retry
        на запись за 60 секунд (иначе 429 RESOURCE_EXHAUSTED). Чужой callback_id —
        403, неизвестный — 404. Перезаписанная строка возвращается со статусом
        PENDING, очищенными полями ответа и СОХРАНЁННЫМ retry_count (в отличие
        от админского retry, который сбрасывает счётчик попыток). Двойная
        аутентификация: HMAC-пара или JWT кабинета. Тело — пустой JSON-объект.
      tags: [Webhooks]
      parameters:
        - name: callback_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              example: {}
      responses:
        '200':
          description: Доставка вебхука снова в очереди
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetryCallbackResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Запись доставки принадлежит другому мерчанту (PERMISSION_DENIED).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          description: Кулдаун ручного retry (60 с на запись; RESOURCE_EXHAUSTED).

  /api/v1/p2p/merchant/wallet/withdraw-fee:
    get:
      operationId: estimateMerchantWithdrawFee
      summary: Расчёт комиссии вывода до отправки запроса
      description: |
        Read-only расчёт комиссии вывода, которая будет применена к
        аутентифицированному мерчанту ДО отправки: переопределение для конкретного
        контрагента, если задано, иначе база тенанта — резолвится через ТОТ ЖЕ
        policy resolver, что использует платёжный путь, поэтому расчёт не может
        разойтись с фактически списанной комиссией. Необязательный `amount_usdt_cents`
        (центы USDT; обратите внимание — в запросе создания единицы целые USDT)
        также даёт чистую сумму.
      tags: [Finance]
      parameters:
        - name: amount_usdt_cents
          in: query
          required: false
          schema: { type: integer, format: int64 }
          description: Необязательная сумма вывода брутто в центах USDT (> 0 включает net_amount_usdt_cents).
          example: 100000
      responses:
        '200':
          description: Расчёт комиссии
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WithdrawFeeResponse'
              example:
                base_fee_usdt_cents: "150"
                override_applied: false
                total_fee_usdt_cents: "150"
                net_amount_usdt_cents: "99850"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/wallet/deposits:
    get:
      operationId: listMerchantIncomingDeposits
      summary: Входящие TRC-20-депозиты со статусами кастодии
      description: |
        Входящие TRC-20-депозиты мерчанта: зачисление леджера плюс
        жизненный цикл принимающего адреса от DepositSweepWorker
        (CREDITED / SWEEP_PENDING / SWEEP_FAILED / SWEEP_STUCK / SWEPT).
        Keyset-пагинация (AIP-158): page_size 1–100 (по умолчанию 20),
        page_token/next_page_token.
      tags: [Finance]
      parameters:
        - $ref: '#/components/parameters/PageSizeCompactQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: Список депозитов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDepositsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/wallet/withdrawals:
    get:
      operationId: listMerchantWithdrawals
      summary: История выводов (ручная очередь, объединённая с исполненными списаниями)
      description: |
        История выводов мерчанта: очередь ручного одобрения, объединённая
        с исполненными списаниями леджера, дедуплицированная по on-chain
        хэшу транзакции. Статусы: PENDING (ожидает одобрения), APPROVED
        (одобрен, ещё не в broadcast), COMPLETED (broadcast,
        есть tx-хэш), CANCELLED (отклонён оператором). Та же keyset-пагинация,
        что у депозитов.
      tags: [Finance]
      parameters:
        - $ref: '#/components/parameters/PageSizeCompactQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: Список выводов
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWithdrawalsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/my-merchant:
    get:
      operationId: getMyMerchant
      summary: Собственный профиль мерчанта, лимиты и баннер kill-switch
      description: |
        Самостоятельное чтение СОБСТВЕННОГО профиля мерчанта: идентификация,
        представление api-credentials, webhook URL, redirect-URL, IP whitelist,
        лимиты сделок (deal_min_amount / deal_max_amount / daily_deal_amount_limit —
        присутствуют только когда заданы) и поля баннера
        kill_switch / kill_switch_reason. Двойная аутентификация: JWT кабинета
        (Authorization: Bearer, роль merchant) ИЛИ HMAC-пара мерчанта — область
        видимости резолвится из предъявленных учётных данных.
      tags: [Management]
      security:
        - bearerAuth: []
        - MerchantToken: []
          MerchantSignature: []
      responses:
        '200':
          description: Профиль мерчанта
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyMerchantResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      operationId: updateMyMerchant
      summary: Обновление собственного профиля и лимитов сделок (JWT кабинета)
      description: |
        Частичное обновление собственного профиля мерчанта: `name`, `webhook_url`,
        `withdrawal_address`, `success_redirect_url`, `fail_redirect_url`,
        `checkout_brand_name`, `finance_chat_id`, `appeal_chat_id`,
        `ip_whitelist` и три лимита сделок. Семантика указателей лимитов:
        отсутствует = без изменений, `0` = снят (NULL), `>0` = задан (в минорных
        единицах). Пара min/max проверяется на согласованность. Статус, балансы и
        комиссии — только для админов. Аутентификация: JWT кабинета.
      tags: [Management]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateMyMerchantRequest'
            example:
              deal_min_amount: "100000"
              daily_deal_amount_limit: "10000000"
      responses:
        '200':
          description: Обновлённый профиль мерчанта (полный объект)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyMerchantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

# ─── Webhooks (outbound) ──────────────────────────────────────────────────────
webhooks:
  deal.created:
    post:
      summary: Сделка создана
      description: Эмитируется, когда новая сделка переходит в состояние `INITIALIZED`.
      tags: [Webhooks]
      x-webhook-event: "deal.created"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_created:
                summary: "PayIn создан"
                value:
                  event: "deal.created"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "INITIALIZED"
                  timestamp: "2026-08-10T12:00:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
        '2xx':
          description: Любой ответ 2xx считается успехом.
  deal.escrow_locked:
    post:
      summary: Эскроу сделки зафиксирован
      description: Эмитируется при фиксации обеспечительного эскроу (`ESCROW_LOCKED`).
      tags: [Webhooks]
      x-webhook-event: "deal.escrow_locked"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_escrow_locked:
                summary: "Эскроу-залог зафиксирован"
                value:
                  event: "deal.escrow_locked"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "ESCROW_LOCKED"
                  timestamp: "2026-08-10T12:00:15Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.payment_notified:
    post:
      summary: Плательщик уведомил об оплате
      description: Эмитируется, когда плательщик уведомляет об оплате (`PAYMENT_NOTIFIED`).
      tags: [Webhooks]
      x-webhook-event: "deal.payment_notified"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_payment_notified:
                summary: "Покупатель уведомил об оплате"
                value:
                  event: "deal.payment_notified"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "PAYMENT_NOTIFIED"
                  timestamp: "2026-08-10T12:05:20Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.amount_changed:
    post:
      summary: Сумма сделки изменена
      description: |
        Эмитируется, когда оператор изменяет сумму живой сделки до
        `PAYMENT_VERIFIED` (операция Amend). `amount` несёт НОВУЮ сумму,
        `old_amount` — прежнюю, `amount_version` — монотонный счётчик правок
        (новая сделка стартует с 1, каждая правка +1). Эскроу-холды
        пересоздаются под новую сумму, USDT-эквивалент пересчитывается по
        курсу самой сделки; статус сделки при этом не меняется. Только для
        мерчантов V2 — legacy-конверт V1 не имеет callbackType под это
        событие и не доставляет его.
      tags: [Webhooks]
      x-webhook-event: "deal.amount_changed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_amount_changed:
                summary: "Оператор изменил сумму живой сделки"
                value:
                  event: "deal.amount_changed"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 200000
                  old_amount: 150000
                  amount_version: 2
                  currency: "RUB"
                  status: "PAYMENT_NOTIFIED"
                  timestamp: "2026-09-03T18:12:44Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'

  deal.payment_verified:
    post:
      summary: Платёж верифицирован
      description: Эмитируется при верификации платежа (`PAYMENT_VERIFIED`).
      tags: [Webhooks]
      x-webhook-event: "deal.payment_verified"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_payment_verified:
                summary: "Оплата верифицирована"
                value:
                  event: "deal.payment_verified"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "PAYMENT_VERIFIED"
                  timestamp: "2026-08-10T12:06:42Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.completed:
    post:
      summary: Сделка завершена
      description: Эмитируется, когда сделка достигает терминального состояния `COMPLETED`.
      tags: [Webhooks]
      x-webhook-event: "deal.completed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_completed:
                summary: "Сделка завершена"
                value:
                  event: "deal.completed"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "COMPLETED"
                  timestamp: "2026-08-10T12:10:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.expired:
    post:
      summary: Сделка истекла
      description: Эмитируется по таймауту сделки (`EXPIRED`).
      tags: [Webhooks]
      x-webhook-event: "deal.expired"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_expired:
                summary: "Сделка истекла по таймауту"
                value:
                  event: "deal.expired"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "EXPIRED"
                  timestamp: "2026-08-10T12:30:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.cancelled:
    post:
      summary: Сделка отменена
      description: Эмитируется, когда клиент или мерчант явно отменяет PayIn-сделку до верификации платежа (`CANCELLED`).
      tags: [Webhooks]
      x-webhook-event: "deal.cancelled"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_cancelled:
                summary: "Сделка явно отменена до подтверждения платежа"
                value:
                  event: "deal.cancelled"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "CANCELLED"
                  timestamp: "2026-08-10T12:04:10Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.appealed:
    post:
      summary: По сделке открыт диспут
      description: Эмитируется при открытии диспута по сделке (`APPEALED`).
      tags: [Webhooks]
      x-webhook-event: "deal.appealed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_appealed:
                summary: "По сделке открыт диспут"
                value:
                  event: "deal.appealed"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "APPEALED"
                  timestamp: "2026-08-10T12:45:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.refund_pending:
    post:
      summary: Возврат в процессе (транзит к отмене)
      description: Транзит-уведомление. Эмитируется при входе сделки в `REFUND_PENDING` (отклонённая апелляция / third-party-флаг); за ним всегда следует `deal.cancelled` — итоговый исход для мерчанта CANCELLED. Пейлоад не изменяется.
      tags: [Webhooks]
      x-webhook-event: "deal.refund_pending"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_refund_pending:
                summary: "Инициирован возврат средств"
                value:
                  event: "deal.refund_pending"
                  deal_id: "7ac148fe-1234-5678-9abc-def012345678"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 150000
                  currency: "RUB"
                  status: "REFUND_PENDING"
                  timestamp: "2026-08-10T13:00:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.failed:
    post:
      summary: Сделка провалена
      description: Эмитируется при провале PayOut-сделки (`FAILED`).
      tags: [Webhooks]
      x-webhook-event: "deal.failed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_failed:
                summary: "PayOut сделка провалена"
                value:
                  event: "deal.failed"
                  deal_id: "8bd2590f-2345-6789-abcd-ef1234567890"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 500000
                  currency: "RUB"
                  status: "FAILED"
                  timestamp: "2026-08-10T13:15:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  p2p.finance.withdrawal_completed:
    post:
      summary: Крипто-вывод завершён
      description: |
        Эмитируется, когда вывод USDT достигает терминального состояния
        `APPROVED` (broadcast on-chain прошёл, леджер списан).
      tags: [Webhooks]
      x-webhook-event: "p2p.finance.withdrawal_completed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              withdrawal_completed:
                summary: "USDT-вывод завершён в on-chain"
                value:
                  event: "p2p.finance.withdrawal_completed"
                  deal_id: "9ce3601c-3456-789a-bcde-f23456789012"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 50000
                  currency: "USDT"
                  status: "APPROVED"
                  timestamp: "2026-08-10T13:30:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  p2p.finance.withdrawal_failed:
    post:
      summary: Крипто-вывод провален / отменён
      description: |
        Эмитируется, когда вывод USDT отклонён оператором или отменён
        до broadcast (терминальный `CANCELLED`); зарезервированная сумма
        возвращается на баланс мерчанта. `deal_id` несёт UUID вывода;
        on-chain-хэша на этом пути не существует.
      tags: [Webhooks]
      x-webhook-event: "p2p.finance.withdrawal_failed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              withdrawal_failed:
                summary: "USDT-вывод отклонён оператором"
                value:
                  event: "p2p.finance.withdrawal_failed"
                  deal_id: "9ce3601c-3456-789a-bcde-f23456789012"
                  merchant_id: "2505069e-32db-4f4f-940c-9ad11e5d0d56"
                  tenant_id: "aaa00000-0000-0000-0000-000000000001"
                  amount: 50000
                  currency: "USDT"
                  status: "CANCELLED"
                  timestamp: "2026-08-10T13:35:00Z"
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  payment_methods.updated:
    post:
      summary: Изменение каталога платёжных методов (broadcast только для V2)
      description: |
        Эмитируется, когда администратор создаёт платёжный метод или обновляет
        его каталог-видимые поля (`name`, `status`, `requisite_types`,
        `is_cross_border`). Broadcast каждому ACTIVE V2-мерчанту
        тенанта с непустым webhook_url (legacy V1-мерчанты не получают
        его никогда — V1-обёртка не имеет catalog callbackType). Обновления
        без изменений ничего не эмитируют. Обёртка — отдельная каталог-форма:
        поля сделки (`amount`, `currency`, `deal_id`) отсутствуют. Это
        УВЕДОМЛЕНИЕ, а не источник данных: при получении перечитайте
        GET /api/v1/p2p/merchant/payment-methods.
      tags: [Webhooks]
      x-webhook-event: "payment_methods.updated"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CatalogEventPayload'
            examples:
              payment_methods_updated:
                summary: "Метод каталога отключён админом"
                value:
                  event: "payment_methods.updated"
                  merchant_id: "8f870a31-b88d-4cb0-a882-628d08cbda88"
                  tenant_id: "550e8400-e29b-41d4-a716-446655440000"
                  timestamp: "2026-08-27T12:00:00Z"
                  data:
                    changed:
                      - payment_method_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                        slug: "sbp_rub"
                        status: "DISABLED"
                        name: "SBP"
                        before:
                          status: "ACTIVE"
                          name: "SBP"
                          requisite_types: ["SBP"]
      responses:
        '200':
          description: Вебхук получен
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "JWT сессии кабинета (заголовок Authorization, схема Bearer) — используется эндпоинтом /api/v1/p2p/my-merchant."
    MerchantToken:
      type: apiKey
      in: header
      name: X-Merchant-Token
      description: Публичный токен мерчанта (64 hex-символа), выдаётся при онбординге.
    MerchantSignature:
      type: apiKey
      in: header
      name: X-Signature
      description: |
        Подпись HMAC-SHA256 (единая схема для всех эндпоинтов).
        Формат: `t=<unix_seconds>,v1=<hex_hmac_sha256>`
        где `hex_hmac = HMAC-SHA256(webhook_secret, "<unix_seconds>.<raw_body>")`.
        Окно replay: 5 минут (допускается дрейф 60 секунд в будущее).
    MerchantTimestamp:
      type: apiKey
      in: header
      name: X-Timestamp
      description: >-
        Unix-время отправки запроса в секундах. Окно подписи: 5 минут назад /
        60 секунд вперёд.

  headers:
    XTraceId:
      description: |
        Корреляционный trace ID (заголовок ответа). Присутствует на каждом
        ошибочном ответе; генерируется API-шлюзом, если апстрим его
        не предоставил. Указывайте ИМЕННО это значение в обращениях в поддержку.
        Раскрыт браузерным клиентам через CORS `Access-Control-Expose-Headers`.
      schema:
        type: string
      example: "4f7c1a9b8e2d"

  parameters:
    MerchantIdPath:
      name: merchant_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Целевой UUID мерчанта.
    DealIdPath:
      name: deal_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID сделки.
    AppealIdPath:
      name: appeal_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID апелляции.
    PageSizeQuery:
      name: page_size
      in: query
      schema:
        type: integer
        format: int32
        minimum: 1
        maximum: 250
        default: 50
      description: |
        Максимальное число возвращаемых результатов (семантика листинга сделок:
        по умолчанию 50, максимум 250). Компактные листинги (callbacks, passes,
        appeals, deposits, withdrawals) используют 20 / 100 — см.
        переопределения параметров у конкретных эндпоинтов.
    PageSizeCompactQuery:
      name: page_size
      in: query
      schema:
        type: integer
        format: int32
        minimum: 1
        maximum: 100
        default: 20
      description: Максимальное число возвращаемых результатов (по умолчанию 20, максимум 100).
    PageTokenQuery:
      name: page_token
      in: query
      schema:
        type: string
      description: Непрозрачный токен пагинации, возвращённый предыдущей страницей.

  responses:
    BadRequest:
      description: |
        Ошибка валидации (gRPC INVALID_ARGUMENT) или недопустимое состояние
        (FAILED_PRECONDITION): недостаточный баланс, дневной лимит сделок,
        активная апелляция, вебхук не настроен, retry недоступен.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Аутентификация не пройдена (токен или подпись отсутствуют/неверны).
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Запрошенный ресурс не найден.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Конкурентное резервирование пропуска белого списка (gRPC ABORTED) — единственный путь 409 в API; повтор безопасен.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NoTraderAvailable:
      description: Нет доступного трейдера для каскадной маршрутизации — повторите позже.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    # ─── Common / Errors (RFC 9457) ───────────────────────────────────────────
    EmptyResponse:
      type: object
      description: |
        Операция успешно завершена — grpc-gateway рендерит proto-сообщение
        EMPTY как пустой JSON-объект `{}` (без поля `ok`).

    ErrorResponse:
      type: object
      description: |
        Ошибка в формате RFC 9457 problem details. Примеры автогенерируются
        fumadocs из схемы (жёстко зашитых инстансов нет). Тело НЕ несёт
        trace-полей — корреляционный ID передаётся в заголовке ответа
        `X-Trace-Id` (см. components/headers/XTraceId).
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri-reference
          description: URI, идентифицирующий тип ошибки.
        title:
          type: string
          description: Краткое человекочитаемое описание ошибки.
        status:
          type: integer
          format: int32
          description: HTTP-код статуса.
        detail:
          type: string
          description: Человекочитаемое пояснение для конкретного случая.
        instance:
          type: string
          format: uri-reference
          description: URI, идентифицирующий конкретное возникновение ошибки.
        errors:
          type: array
          description: Ошибки валидации на уровне полей.
          items:
            $ref: '#/components/schemas/FieldError'

    FieldError:
      type: object
      required: [field, message]
      properties:
        field:
          type: string
          description: JSON-путь невалидного поля.
          example: "amount"
        message:
          type: string
          description: Описание ошибки уровня поля.
          example: "must be greater than 0"
        code:
          type: string
          description: Стабильный машиночитаемый код ошибки.
          example: "POSITIVE_REQUIRED"

    # ─── Enums (canonical V2 values) ──────────────────────────────────────────
    OrderStatus:
      type: string
      description: |
        Каноническое состояние жизненного цикла заказа V2 (FSA). Точно
        совпадает с `domain.OrderStatus` и proto-enum `OrderStatus`.
      enum:
        - INITIALIZED
        - ESCROW_LOCKED
        - PAYMENT_NOTIFIED
        - PAYMENT_VERIFIED
        - COMPLETED
        - EXPIRED
        - CANCELLED
        - APPEALED
        - REFUND_PENDING
        - SPAM_REJECTED
        - SOFT_DISPUTED

    DealType:
      type: string
      description: Направление P2P-сделки.
      enum:
        - PAYIN
        - PAYOUT

    AppealStatus:
      type: string
      description: Состояние жизненного цикла апелляции (диспута).
      enum:
        - OPEN
        - IN_PROGRESS
        - REOPENED
        - SATISFIED
        - REJECTED

    PayOutStatus:
      type: string
      description: Состояние жизненного цикла PayOut.
      enum:
        - INITIALIZED
        - MATCHED
        - UNASSIGNED
        - PROCESSING
        - COMPLETED
        - FAILED
        - EXPIRED

    WithdrawalStatus:
      type: string
      description: Состояние жизненного цикла крипто-вывода (USDT).
      enum:
        - PENDING
        - APPROVED
        - CANCELLED

    PassStatus:
      type: string
      description: Состояние жизненного цикла пропуска белого списка.
      enum:
        - ACTIVE
        - RESERVED
        - USED
        - EXPIRED

    RequisiteType:
      type: string
      description: Классификация платёжного инструмента трейдера.
      enum:
        - BANK_CARD
        - E_WALLET
        - SBP
        - SIM
        - BANK_TRANSFER

    AmountMatchStatus:
      type: string
      description: |
        Качество совпадения ожидаемой и фактически полученной сумм платежа.
        Значения — строки в нижнем регистре.
      enum:
        - full
        - partial
        - overpaid

    CallbackDeliveryStatus:
      type: string
      description: Жизненный цикл записи доставки вебхука.
      enum:
        - PENDING
        - SUCCESS
        - RETRY
        - ERROR

    SenderRole:
      type: string
      description: Роль отправителя сообщения в чате диспута.
      enum:
        - MERCHANT
        - TRADER
        - ADMIN

    # ─── Authentication ───────────────────────────────────────────────────────
    AuthenticateMerchantRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
          description: UUID тенанта (инжектится API-шлюзом из JWT claim).
        token:
          type: string
          description: Публичный токен мерчанта (64 hex-символа).
          example: "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0"
        body:
          type: string
          description: Сырое тело запроса для пересчёта подписи.
        signature:
          type: string
          description: Hex-подпись HMAC-SHA256 по телу с ключом `webhook_secret`.

    AuthenticateMerchantResponse:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid

    # ─── API Keys ────────────────────────────────────────────────────────────
    IssueAPIKeysRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста.
        merchant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста.

    IssueAPIKeysResponse:
      type: object
      required: [token, secret]
      properties:
        token:
          type: string
          description: Новый API-токен (64 hex-символа, НЕ хранится в открытом виде).
        secret:
          type: string
          description: Секрет для подписи вебхуков (32 байта). Показывается ровно ОДИН раз.

    RotateAPIKeysRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid

    RotateAPIKeysResponse:
      type: object
      required: [token, secret]
      properties:
        token:
          type: string
        secret:
          type: string

    # ─── Merchant Lifecycle ──────────────────────────────────────────────────
    DisableMerchantRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid

    EnableMerchantRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid

    ToggleKillSwitchRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid

    ToggleKillSwitchResponse:
      type: object
      properties:
        merchant_id:
          type: string
          format: uuid
        disabled:
          type: boolean
          description: true, когда kill-switch активен.
        disabled_at:
          type: string
          format: date-time
          description: RFC 3339-метка времени отключения; отсутствует, когда включён.

    GenerateDepositAddressRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid

    GenerateDepositAddressResponse:
      type: object
      required: [trc20_address]
      properties:
        trc20_address:
          type: string
          description: TRC-20-адрес депозитов.
          example: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
        wallet_derivation_index:
          type: integer
          format: int32
          description: Индекс вывода BIP44, использованный для получения адреса.

    GetDepositAddressResponse:
      type: object
      required: [trc20_address]
      properties:
        trc20_address:
          type: string
          description: |
            Текущий активный TRC-20-адрес депозитов. Повторяет форму ответа
            Generate БЕЗ индекса вывода (read-only представление).
          example: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
        updated_at:
          type: string
          format: date-time
          description: RFC 3339-метка времени последней генерации/ротации (updated_at записи мерчанта).

    # ─── PayIn / PayOut ──────────────────────────────────────────────────────
    CreatePayInRequest:
      type: object
      required: [amount, currency, idempotency_key, payment_method_id]
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста; в клиентских запросах опускайте.
        merchant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста; в клиентских запросах опускайте.
        amount:
          type: integer
          format: int64
          description: Сумма в минорных единицах валюты (например копейки для RUB).
          example: 150000
        currency:
          type: string
          description: Код валюты ISO 4217 (например `RUB`).
          example: RUB
        idempotency_key:
          type: string
          description: |
            Уникальный ключ запроса. Повторные запросы с тем же ключом возвращают
            закешированный результат вместо создания новой сделки.
          example: "payin_order_12345"
        payment_method_id:
          type: string
          description: |
            ОБЯЗАТЕЛЬНАЯ единая ссылка на метод — UUID метода (канонический
            быстрый путь по PK) или слаг каталога (`sbp_rub`, `card_rub`, ...).
            Оба резолвятся в одну и ту же строку через
            `GET /api/v1/p2p/merchant/payment-methods`; неизвестные ссылки
            отклоняются с HTTP 400 со списком допустимых слагов. Пустое значение
            отклоняется (каскадная маршрутизация привязана к методу). `GET /banks`
            НЕ возвращает идентификаторы методов — его поле `methods` содержит
            только значения `RequisiteType`.
          example: "550e8400-e29b-41d4-a716-446655440000"
        currency_id:
          type: string
          format: uuid
          description: Необязательный UUID валюты; обязателен для матчинга пропусков белого списка.
        client_id:
          type: string
          description: Идентификатор клиента (клиента) в системе мерчанта.
          example: "player_42"
        target_bank:
          type: string
          description: |
            Канонический код банка реквизита (bank-routing, опционально):
            значение `bank_name` из GET /banks?method=... — кнопка «1click
            Альфа» = метод + `target_bank: alfa`. Не передан — маршрутизация
            по методу как раньше (byte-identical). Пустая строка = ошибка;
            неизвестный/отключённый/непривязанный к методу код = 400 с
            перечнем допустимых. Replay-семантика: повтор idempotency_key
            возвращает исходную сделку; другой банк — новый ключ.
          example: "alfa"
        payer_bank:
          type: string
          description: Необязательная подсказка по банку плательщика для каскадной маршрутизации.
        description:
          type: string
          description: Необязательное описание от мерчанта, показывается на платёжной странице.
        callback_url:
          type: string
          format: uri
          description: |
            Переопределение callback URL на уровне сделки (только PayIn). Иерархия
            доставки: per-deal `callback_url` → `merchant.webhook_url`; когда оба
            пусты, для сделки не создаётся вебхук. Запросы PayOut не имеют
            переопределения per-deal — вебхуки выплат всегда идут на
            `merchant.webhook_url`.
        is_client_commission:
          type: boolean
          default: false
          description: Если true, комиссию платит клиент (а не мерчант).

    CreatePayOutRequest:
      type: object
      required: [amount, currency, wallet_id, idempotency_key, target_requisite, payment_method]
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста; в клиентских запросах опускайте.
        merchant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста; в клиентских запросах опускайте.
        amount:
          type: integer
          format: int64
          description: Сумма выплаты в минорных единицах валюты.
          example: 500000
        currency:
          type: string
          description: Код валюты ISO 4217.
          example: RUB
        wallet_id:
          type: string
          format: uuid
          description: |
            UUID **USDT-кошелька** мерчанта для списания — холд размещается
            в USDT (конвертация по курсу SELL на момент создания),
            а НЕ на фиатном кошельке. Используйте `wallet_id` USDT-строки из
            `GET /balance`.
        idempotency_key:
          type: string
          description: Уникальный ключ операции.
          example: "payout_order_99812"
        target_requisite:
          type: string
          description: Реквизиты получателя (номер карты, телефон СБП, IBAN).
          example: "2202201234567890"
        target_name:
          type: string
          description: Полное имя получателя для сверки.
          example: "Ivan Ivanovich I."
        target_bank:
          type: string
          description: |
            Банк получателя: слаг (например `ziraat`, `garanti`) или человекочитаемое
            имя. Передаётся провайдеру как банк выплаты и используется для
            внутрибанковского каскадного матчинга (TRY/Havale). Пусто = без подсказки
            банка (выплаты на карту/по СБП).
          example: "ziraat"
        requisite_details:
          type: object
          additionalProperties:
            type: string
          description: |
            Именованные реквизиты получателя помимо `target_requisite` /
            `target_name`. Допустимые ключи (enum): `iban`, `account_number`,
            `document_number`, `bank_name`, `phone_number`. Неизвестные ключи
            отклоняются с HTTP 400 (InvalidArgument) со списком допустимых.
            Необязательно: отсутствует/пусто = без дополнительных реквизитов. Значения —
            собственные реквизиты мерчанта и возвращаются без маскировки.
          example:
            iban: "TR33 0006 1005 1978 6457 8413 26"
            document_number: "12345678901"
        payment_method:
          type: string
          description: |
            ОБЯЗАТЕЛЬНАЯ единая ссылка на метод — **слаг** каталога
            (`sbp_rub`, `card_rub`, `mobcom_rub`, `express-havale`, ...;
            поле `slug` из
            `GET /api/v1/p2p/merchant/payment-methods`, идентично во всех
            окружениях) или **UUID** метода (поле `id`); оба
            резолвятся в одну и ту же строку. НЕ uppercase-enum `RequisiteType`
            (это информационные `requisite_types`, в запросах не передаются).
            Неизвестная/отсутствующая ссылка или метод, обслуживающий другую
            валюту сделки, отклоняется с HTTP 400 (кросс-проверка валюты;
            ошибка содержит список допустимых слагов). PayIn использует тот же
            единый контракт через `payment_method_id`.
          example: "card_rub"

    MerchantDealInfo:
      type: object
      description: |
        Каноническое M4-представление сделки, возвращаемое merchant API. Точно
        повторяет proto-сообщение `MerchantDealInfo`.
      properties:
        deal_id:
          type: string
          format: uuid
          description: UUID сделки.
        status:
          $ref: '#/components/schemas/OrderStatus'
          description: |
            Канонический статус — `OrderStatus` для PayIn-сделок, `PayOutStatus`
            для PayOut-сделок (например `MATCHED` сразу после синхронного
            авто-матчинга при создании).
        amount:
          type: integer
          format: int64
          description: Сумма сделки в минорных единицах валюты.
        currency:
          type: string
          description: |
            Код валюты ISO 4217 (эхо валюты запроса; для PayOut —
            фиатная валюта выплаты).
        created_at:
          type: string
          format: date-time
          description: RFC 3339-метка времени создания.
        client_id:
          type: string
          description: Идентификатор клиента (клиента).
        deal_type:
          $ref: '#/components/schemas/DealType'
        updated_at:
          type: string
          format: date-time
          description: RFC 3339-метка времени последнего обновления.
        payment_url:
          type: string
          format: uri
          description: |
            URL hosted-payform для редиректа клиента. Заполняется только для
            PayIn-сделок; для PayOut — пустая строка.
        target_requisite:
          type: string
          description: |
            Реквизиты получателя (эхо значения запроса, без маскировки).
            Заполняется только для PayOut-сделок; для PayIn — пустая строка.

    PayInResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        deal:
          $ref: '#/components/schemas/MerchantDealInfo'

    PayOutResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        deal:
          $ref: '#/components/schemas/MerchantDealInfo'

    RetrieveDealResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        deal:
          $ref: '#/components/schemas/MerchantDealInfo'

    ListDealsResponse:
      type: object
      description: |
        Каноническая list-обёртка (CONVENTIONS §2.2, применяется egress-маршализатором
        HTTP с 2026-08-24): только `data` + `next_page_token`. Per-entity
        имя поля proto (`deals`) и `total_count` существуют во внутреннем
        gRPC-контракте, но НИКОГДА не показываются на HTTP.
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MerchantDealInfo'
        next_page_token:
          type: string
          description: Непрозрачный токен следующей страницы (пустой, когда результатов больше нет).

    # ─── Balance ─────────────────────────────────────────────────────────────
    MerchantWalletBalance:
      type: object
      properties:
        wallet_id:
          type: string
          format: uuid
          description: UUID кошелька money-сервиса (каждая строка — реальный кошелёк; TRC-20-адрес депозитов — отдельная сущность, см. /wallet/deposit-address).
        currency:
          type: string
          description: Код валюты (конфигурируется тенантом).
        available:
          type: integer
          format: int64
          description: Доступный баланс (в минорных единицах валюты).
        frozen:
          type: integer
          format: int64
          description: Замороженный / удерживаемый баланс.
        total:
          type: integer
          format: int64
          description: Итоговый баланс (`available + frozen`).

    BalanceResponse:
      type: object
      description: |
        grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки
        `data`). `wallets` — реальные кошельки мерчанта из money-сервиса.
      properties:
        wallets:
          type: array
          items:
            $ref: '#/components/schemas/MerchantWalletBalance'

    # ─── Passes ──────────────────────────────────────────────────────────────
    CreatePassRequest:
      type: object
      required: [amount]
      description: Обязательно только `amount` — все остальные фильтры необязательны (пусто = любой).
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Инжектится из auth-контекста.
        merchant_id:
          type: string
          format: uuid
          description: Необязательный UUID мерчанта; пусто = любой мерчант.
        provider_id:
          type: string
          format: uuid
          description: Необязательный UUID провайдера; пусто = любой провайдер.
        amount:
          type: integer
          format: int64
          description: Заранее одобренная сумма в минорных единицах валюты.
        currency_id:
          type: string
          format: uuid
          description: Необязательный UUID валюты; пусто = любая валюта.
        payment_method_id:
          type: string
          format: uuid
          description: "Необязательный UUID платёжного метода, привязывающий пропуск к одному методу; пусто = любой метод. Только UUID — в отличие от сделок, слаг-ссылки на пути пропусков не резолвятся."
        requisite_type:
          $ref: '#/components/schemas/RequisiteType'
        expires_at:
          type: string
          format: date-time
          description: Необязательный срок действия RFC 3339; пусто = бессрочно.

    Pass:
      type: object
      properties:
        pass_id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid
          description: Пусто, когда пропуск применяется ко всем мерчантам.
        provider_id:
          type: string
          format: uuid
          description: "Всегда пусто в ответах для мерчантов (TD-080: провайдеры остаются анонимными)."
        amount:
          type: integer
          format: int64
        currency_id:
          type: string
          format: uuid
        payment_method_id:
          type: string
          format: uuid
        requisite_type:
          $ref: '#/components/schemas/RequisiteType'
        status:
          $ref: '#/components/schemas/PassStatus'
        expires_at:
          type: string
          format: date-time
          description: Срок действия RFC 3339 или пусто, когда бессрочно.
        used_deal_id:
          type: string
          format: uuid
          description: UUID сделки, использовавшей этот пропуск, или пусто.
        created_at:
          type: string
          format: date-time

    CreatePassResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        pass:
          $ref: '#/components/schemas/Pass'

    ListPassesResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Pass'

    # ─── Callbacks ───────────────────────────────────────────────────────────
    Callback:
      type: object
      description: Запись доставки вебхука.
      properties:
        callback_id:
          type: string
          format: uuid
        deal_id:
          type: string
          format: uuid
          description: UUID сделки (пусто для колбэков выплат/выводов).
        merchant_id:
          type: string
          format: uuid
        callback_url:
          type: string
          format: uri
        status:
          $ref: '#/components/schemas/CallbackDeliveryStatus'
        retry_count:
          type: integer
          format: int32
        response_code:
          type: integer
          format: int32
          description: HTTP-код статуса, возвращённый эндпоинтом мерчанта.
        response_body:
          type: string
          description: Тело ответа, снятое с эндпоинта мерчанта.
        next_retry_at:
          type: string
          format: date-time
          description: RFC 3339-метка времени следующего ретрая или пусто в терминальном состоянии.
        created_at:
          type: string
          format: date-time

    ListCallbacksResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Callback'

    # ─── Crypto Withdrawal ───────────────────────────────────────────────────
    CreateCryptoWithdrawRequest:
      type: object
      required: [amount_usdt, tron_address]
      properties:
        amount_usdt:
          type: integer
          format: int64
          description: |
            Целые USDT (integer, не float): `1000` = 1000 USDT.
            Внутренне конвертируется в центы USDT (×100) для хранения.
          example: 1000
        tron_address:
          type: string
          description: TRC-20-адрес назначения (base58check, 34 символа; неверный формат → 400).
          example: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
        idempotency_key:
          type: string
          description: |
            Присутствует в контракте, но НЕ требуется/не проверяется — платформа
            гарантирует идемпотентность вывода на стороне сервера (детерминированный
            ключ из merchant + address + amount + текущей комиссии), поэтому
            идентичный повтор сходится к той же on-chain-транзакции. Значение
            игнорируется.

    CryptoWithdrawResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        status:
          type: string
          enum: [COMPLETED, PENDING_MANUAL]
          description: |
            Синхронный исход ЭТОГО запроса: COMPLETED (авто-одобрено —
            on-chain-транзакция отправлена в broadcast, transaction_id несёт
            хэш) или PENDING_MANUAL (направлено в очередь одобрения админом).
            DOMAIN-статус WithdrawalStatus (PENDING/APPROVED/CANCELLED — строки
            выводов, листинги, события вебхуков) — другая поверхность.
        transaction_id:
          type: string
          description: On-chain TX-хэш (задаётся только при COMPLETED; пусто при PENDING_MANUAL).
        amount_usdt:
          type: integer
          format: int64
          description: Эхо запрошенной суммы (целые USDT).
        tron_address:
          type: string
        created_at:
          type: string
          format: date-time
        fee_usdt_cents:
          type: integer
          format: int64
          description: Комиссия вывода в центах USDT — фактически списанная (авто-путь) или комиссия, которую спишет путь ручного одобрения.
        net_usdt_cents:
          type: integer
          format: int64
          description: "Чистая on-chain-сумма, которую получает получатель — amount_usdt*100 минус fee_usdt_cents."

    # ─── Appeals (Disputes) ──────────────────────────────────────────────────
    CreateAppealRequest:
      type: object
      required: [deal_id, reason]
      properties:
        deal_id:
          type: string
          format: uuid
          description: UUID сделки, по которой открывается диспут.
        reason:
          type: string
          description: Произвольная причина открытия диспута.
          example: "Funds not credited within 30 minutes"
        file_ids:
          type: array
          items:
            type: string
            format: uuid
          description: UUID загруженных файлов-доказательств.

    AppealInfo:
      type: object
      properties:
        appeal_id:
          type: string
          format: uuid
        deal_id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/AppealStatus'
        reason:
          type: string
        resolution_note:
          type: string
          description: Резолюция, устанавливаемая при закрытии диспута.
        created_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
          description: RFC 3339-метка времени резолюции или пусто, пока открыт.
        updated_at:
          type: string
          format: date-time

    CreateAppealResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        appeal:
          $ref: '#/components/schemas/AppealInfo'

    RetrieveAppealResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        appeal:
          $ref: '#/components/schemas/AppealInfo'

    ListAppealsResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AppealInfo'
        next_page_token:
          type: string

    SendAppealMessageRequest:
      type: object
      required: [body]
      properties:
        body:
          type: string
          description: Текст сообщения.
        file_ids:
          type: array
          items:
            type: string
            format: uuid
          description: UUID прикреплённых файлов.

    AppealMessageInfo:
      type: object
      properties:
        message_id:
          type: string
          format: uuid
        appeal_id:
          type: string
          format: uuid
        sender_role:
          $ref: '#/components/schemas/SenderRole'
        body:
          type: string
        file_ids:
          type: array
          items:
            type: string
            format: uuid
        created_at:
          type: string
          format: date-time

    SendAppealMessageResponse:
      type: object
      description: grpc-gateway рендерит proto-поля на верхнем уровне (без обёртки `data`).
      properties:
        message:
          $ref: '#/components/schemas/AppealMessageInfo'

    ListAppealMessagesResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AppealMessageInfo'
        next_page_token:
          type: string

    # ─── Banks & Payment Methods ─────────────────────────────────────────────
    BankInfo:
      type: object
      properties:
        bank_name:
          type: string
          description: |
            Канонический МАШИННЫЙ код банка платформы (bank-routing) —
            значение для `target_bank` заявок. Не отображаемое имя.
          example: "sber"
        display_name:
          type: string
          description: Человекочитаемое имя.
          example: "Сбербанк"
        country:
          type: string
          description: Код страны ISO 3166-1 alpha-2.
          example: "RU"
        methods:
          type: array
          items:
            $ref: '#/components/schemas/RequisiteType'
          description: Поддерживаемые типы реквизитов (при ?method= — типы этого метода).
        payin_available:
          type: boolean
          description: Банк реально обслуживает депозиты прямо сейчас (покрытие живое).
        payout_available:
          type: boolean
          description: Банк реально обслуживает выплаты прямо сейчас (покрытие живое).

    ListBanksResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/BankInfo'

    MerchantPaymentMethodInfo:
      type: object
      description: |
        Одна ACTIVE-строка платёжного метода тенанта мерчанта, соединённая
        с её валютой. `id` — UUID, передаваемый как `payment_method_id` в
        `CreatePayInRequest`; `slug` — значение, принимаемое полем
        `payment_method` в `CreatePayOutRequest`.
      properties:
        id:
          type: string
          format: uuid
          description: UUID платёжного метода (`payment_method_id` для PayIn).
          example: "550e8400-e29b-41d4-a716-446655440000"
        slug:
          type: string
          description: Слаг платёжного метода (`payment_method` для PayOut).
          example: "express-havale"
        name:
          type: string
          description: Человекочитаемое имя метода.
          example: "Havale Express (TRY)"
        requisite_types:
          type: array
          items:
            $ref: '#/components/schemas/RequisiteType'
          description: Типы реквизитов, которые обслуживает метод.
        currency_code:
          type: string
          description: Код валюты ISO 4217 (join из `p2p_currencies`).
          example: "TRY"
        status:
          type: string
          enum: [ACTIVE]
          description: Всегда `ACTIVE` — эндпоинт фильтрует неактивные строки.

    ListMerchantPaymentMethodsResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MerchantPaymentMethodInfo'

    # ─── Currencies / Export / Turnover / Wallet history / My Merchant ────────
    MerchantCurrencyInfo:
      type: object
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
          description: Alpha-код ISO 4217 (RUB, TRY, ...).
        name:
          type: string
          description: Отображаемое имя в кабинете мерчанта.
        status:
          type: string
          description: Всегда ACTIVE (эндпоинт фильтрует).

    ListMerchantCurrenciesResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MerchantCurrencyInfo'

    ExportDealsResponse:
      type: object
      description: "JSON-обёртка CSV-экспорта (сырой документ отдаётся при Accept: text/csv)."
      properties:
        csv:
          type: string
          description: Полный CSV-документ RFC 4180 (заголовок `id,date,type,amount,currency,status,counterparty`, CRLF, поля в кавычках; суммы в МАЖОРНЫХ единицах).
        file_name:
          type: string
          description: deals-YYYYMMDD.csv (UTC-дата экспорта).
        row_count:
          type: integer
          description: Строки данных без заголовка.
        truncated:
          type: boolean
          description: true = достигнут лимит 10 000 строк, строки обрезаны.

    TurnoverRow:
      type: object
      properties:
        date:
          type: string
          description: Календарный UTC-день created_at ("YYYY-MM-DD").
        deal_type:
          type: string
          enum: [PAYIN, PAYOUT]
        amount_minor:
          type: integer
          format: int64
          description: Суммированная сумма сделок в минорной единице валюты.
        currency:
          type: string
          description: Код валюты ISO 4217.

    TurnoverResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TurnoverRow'

    RetryCallbackResponse:
      type: object
      properties:
        callback:
          $ref: '#/components/schemas/Callback'

    WithdrawFeeResponse:
      type: object
      properties:
        base_fee_usdt_cents:
          type: integer
          format: int64
          description: Базовая комиссия тенанта (p2p_system_config.withdrawal_fee_usdt_cents).
        override_applied:
          type: boolean
          description: true = база заменена переопределением для конкретного мерчанта.
        total_fee_usdt_cents:
          type: integer
          format: int64
          description: "Фактически списываемая комиссия — переопределение, если задано, иначе база."
        net_amount_usdt_cents:
          type: integer
          format: int64
          description: amount − total_fee; 0, когда сумма не передана. Может быть ≤ 0, когда комиссия покрывает всю сумму (такой вывод отклоняется платёжным путём).

    DepositEntrySchema:
      type: object
      properties:
        block_timestamp:
          type: string
          format: date-time
          description: credited_at леджера — метка времени on-chain-блока плюс глубина финальности в 19 подтверждений.
        amount_usdt_cents:
          type: integer
          format: int64
          description: Зачисленная сумма в центах USDT.
        tx_hash:
          type: string
          description: Хэш on-chain-транзакции TRON.
        trc20_address:
          type: string
          description: TRC-20-адрес получения, на который поступил депозит.
        status:
          type: string
          enum: [CREDITED, SWEEP_PENDING, SWEEP_FAILED, SWEEP_STUCK, SWEPT]
          description: Зачисление леджера + кастодиальный жизненный цикл DepositSweepWorker.

    ListDepositsResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/DepositEntrySchema'
        next_page_token:
          type: string
          description: Непрозрачный keyset-курсор; пустая строка = последняя страница.

    WithdrawalEntrySchema:
      type: object
      properties:
        created_at:
          type: string
          format: date-time
          description: Время запроса вывода (ручная очередь) или время списания леджера (авто-путь).
        amount_usdt_cents:
          type: integer
          format: int64
          description: Запрошенная сумма брутто в центах USDT.
        fee_usdt_cents:
          type: integer
          format: int64
          description: Комиссия вывода в центах USDT (0 для истории до введения комиссии).
        net_usdt_cents:
          type: integer
          format: int64
          description: Чистая on-chain-сумма = брутто − комиссия.
        tx_hash:
          type: string
          description: Хэш on-chain-транзакции TRON; пусто до broadcast.
        status:
          type: string
          enum: [PENDING, APPROVED, COMPLETED, CANCELLED]
          description: PENDING — ожидает одобрения админом; APPROVED — одобрен, ещё не в broadcast; COMPLETED — broadcast (есть tx-хэш); CANCELLED — отклонён/отменён оператором.
        tron_address:
          type: string
          description: Адрес назначения в сети TRON.

    ListWithdrawalsResponse:
      type: object
      description: Каноническая list-обёртка (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WithdrawalEntrySchema'
        next_page_token:
          type: string
          description: Непрозрачный keyset-курсор; пустая строка = последняя страница.

    MyMerchantResponse:
      type: object
      properties:
        merchant:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            email:
              type: string
            status:
              type: string
            kill_switch:
              type: boolean
              description: Kill-switch под управлением оператора (переключение только админом; мерчант видит состояние, но никогда не переключает его через API).
            kill_switch_reason:
              type: string
              description: Причина kill-switch платформы, отображаемая баннером кабинета; пусто, когда выключен.
            balance_usdt:
              type: string
            escrow_usdt:
              type: string
            trc20_address:
              type: string
            project_url:
              type: string
            checkout_brand_name:
              type: string
            success_redirect_url:
              type: string
            fail_redirect_url:
              type: string
            finance_chat_id:
              type: string
            appeal_chat_id:
              type: string
            ip_whitelist:
              type: array
              items:
                type: string
            api_token:
              type: string
            withdrawal_address:
              type: string
            moderation_status:
              type: string
            api_version:
              type: string
            webhook_url:
              type: string
            created_at:
              type: string
              format: date-time
            updated_at:
              type: string
              format: date-time
            deal_min_amount:
              type: string
              description: Собственный минимум на сделку (минорные единицы); отсутствует = не задан (proto3 optional).
            deal_max_amount:
              type: string
              description: Собственный максимум на сделку (минорные единицы); отсутствует = не задан.
            daily_deal_amount_limit:
              type: string
              description: Собственный дневной (UTC) лимит объёма PayIn+PayOut (минорные единицы); отсутствует = не задан.
        auto_withdrawal_mode:
          type: string
          enum: [AUTO_UP_TO_LIMIT, MANUAL_ONLY]
        auto_withdrawal_effective_limit_usdt_cents:
          type: string

    UpdateMyMerchantRequest:
      type: object
      description: "Частичное обновление — передавайте только изменённые поля. Указатели лимитов — отсутствует = без изменений, 0 = снят, >0 = задан."
      properties:
        name:
          type: string
        webhook_url:
          type: string
          format: uri
        withdrawal_address:
          type: string
        success_redirect_url:
          type: string
        fail_redirect_url:
          type: string
        checkout_brand_name:
          type: string
        finance_chat_id:
          type: string
        appeal_chat_id:
          type: string
        ip_whitelist:
          type: string
          description: JSON-массив IP-адресов в виде строки.
        deal_min_amount:
          type: string
        deal_max_amount:
          type: string
        daily_deal_amount_limit:
          type: string

    # ─── Webhook Payload ─────────────────────────────────────────────────────
    WebhookAck:
      type: object
      description: Тело подтверждения, которое мерчанты возвращают, чтобы принять доставку вебхука.
      properties:
        received:
          type: boolean
          description: Установите `true`, чтобы подтвердить получение вебхука.
          example: true

    CatalogMethodBefore:
      type: object
      description: Снимок каталог-видимых полей метода до обновления (только события update).
      properties:
        status:
          type: string
        name:
          type: string
        requisite_types:
          type: array
          items:
            type: string
        is_cross_border:
          type: boolean

    CatalogMethodChange:
      type: object
      description: Состояние AFTER одного изменённого платёжного метода плюс необязательный снимок before.
      required: [payment_method_id, slug, status, name]
      properties:
        payment_method_id:
          type: string
          format: uuid
        slug:
          type: string
        status:
          type: string
        name:
          type: string
        before:
          $ref: '#/components/schemas/CatalogMethodBefore'

    CatalogEventPayload:
      type: object
      description: |
        Выделенная обёртка каталог-broadcast `payment_methods.updated`
        (НЕ сделочная CallbackEventPayload — amount/currency/deal_id
        бессмысленны для строки каталога и отсутствуют).
      required: [event, merchant_id, tenant_id, timestamp, data]
      properties:
        event:
          type: string
          enum: [payment_methods.updated]
        merchant_id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        timestamp:
          type: string
          format: date-time
        data:
          type: object
          required: [changed]
          properties:
            changed:
              type: array
              items:
                $ref: '#/components/schemas/CatalogMethodChange'

    CallbackEventPayload:
      type: object
      description: |
        Каноническое тело вебхука для мерчанта, отправляемое при событиях
        жизненного цикла. Подписывается тем же алгоритмом HMAC-SHA256, что и
        входящие запросы (заголовок `X-Signature`).
      required: [event, deal_id, merchant_id, tenant_id, amount, currency, status, timestamp]
      properties:
        event:
          type: string
          description: Идентификатор события (например `deal.completed`).
          example: "deal.completed"
        deal_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        amount:
          type: integer
          format: int64
          description: Сумма сделки в минорных единицах валюты.
        old_amount:
          type: integer
          format: int64
          description: Сумма сделки ДО изменения — присутствует только в `deal.amount_changed`.
        amount_version:
          type: integer
          format: int64
          description: Монотонный счётчик правок суммы — присутствует только в `deal.amount_changed` (новая сделка стартует с 1, каждая правка +1).
        currency:
          type: string
          description: Код валюты ISO 4217.
        status:
          $ref: '#/components/schemas/OrderStatus'
        timestamp:
          type: string
          format: date-time
          description: RFC 3339-метка времени события.
        received_amount:
          type: integer
          format: int64
          description: Фактически полученная сумма (отличается от `amount` при partial/overpaid).
        amount_match_status:
          $ref: '#/components/schemas/AmountMatchStatus'
        provider_metadata:
          type: object
          description: Провайдер-специфичные passthrough-данные (deeplinks, URL платёжных страниц и т.п.).
          additionalProperties: true
        fee_breakdown:
          type: object
          description: |
            Картина мерча по сделке — только `deal.completed` (PayIn), только
            V2. Все суммы — int64 в USDT-центах (в JSON передаются строками,
            правило proto3 JSON). Внутренние издержки платформы не
            раскрываются: наружу идут только gross-сумма, комиссия мерча и
            его нетто; промо-поля присутствуют только при применённом промо.
          properties:
            gross_usdt_cents:
              type: integer
              format: int64
              description: Общий USDT-эквивалент платежа.
            merchant_commission_usdt_cents:
              type: integer
              format: int64
              description: Комиссия мерча по сделке (gross − merchant_cut); считается от фактической суммы платежа.
            merchant_cut_usdt_cents:
              type: integer
              format: int64
              description: Нетто мерча до промо-бонуса.
            promo_bonus_usdt_cents:
              type: integer
              format: int64
              description: Бонус, начисленный поверх нетто — только при применённом промо.
            promo_id:
              type: string
              format: uuid
              description: Идентификатор применённого правила акции — только при применённом промо.
            promo_sponsor_type:
              type: string
              enum: [PLATFORM, MERCHANT, PROVIDER]
              description: Спонсор бонуса — только при применённом промо.
