openapi: 3.1.0
info:
  title: Syncra Merchant API V2
  version: "2.0.0"
  description: |
    P2P acquiring gateway — Merchant API (the canonical V2 contract).

    Covers authentication, API key management, the merchant lifecycle,
    PayIn/PayOut deals, balances, whitelist passes, webhook delivery,
    crypto withdrawals, disputes (appeals), banks, and TRC-20 deposit
    address issuance.

    ## Conventions
    - All monetary amounts are **int64 in minor currency units** (kopecks for RUB,
      cents for USD/USDT, satoshis for BTC). No float or decimal.
    - All timestamps are **RFC 3339** UTC (for example `2026-08-10T12:34:56Z`).
    - All identifiers are **string UUIDs** (v4) unless stated otherwise.
    - Currency and payment method availability depends on the tenant configuration.
    - Response wrapper convention (since 2026-08-24): a SINGLE resource is
      returned as a top-level proto object (`{"deal": {...}}`); EVERY list
      endpoint returns the canonical wrapper
      `{"data": [...], "next_page_token": ""}` (AIP-158) — per-entity array
      names and `total_count` from the internal gRPC contract are never
      exposed over HTTP. `GET /balance` is the only exception (a balance
      projection, not a listing: `{"wallets": [...]}`).
    - `tenant_id` / `merchant_id` in request bodies are injected by the API
      gateway from the authenticated merchant context; clients should omit them.

    ## Authentication
    Every request (except `POST /auth`) MUST carry:
      - `X-Merchant-Token: <64-char hex>` — the merchant's public token.
      - `X-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>`
        where `hex_hmac = HMAC-SHA256(webhook_secret, "<unix_seconds>.<raw_body>")`.
    The timestamp protects against replay attacks (a maximum clock skew of
    5 minutes plus a 60-second allowance into the future).
  contact:
    name: Syncra Support
    url: https://docs.syncra.money
    email: support@syncra.money
  license:
    name: Proprietary

servers:
  - url: https://api.syncra.money
    description: Production
  - url: http://localhost:8080
    description: Local development

tags:
  - name: Authentication
    x-displayName: Authentication
    description: Token and HMAC signature verification (invoked by the API gateway).
  - name: API Keys
    x-displayName: API keys
    description: Issuing and rotating merchant credentials.
  - name: Merchants
    x-displayName: Merchants
    description: Merchant lifecycle (kill switch, enable/disable, deposit address).
  - name: Management
    x-displayName: Merchant profile
    description: Self-service merchant profile and limits (HMAC or cabinet JWT).
  - name: Payments
    x-displayName: Payments
    description: Creating PayIn deals (acquiring) and PayOut payouts.
  - name: Deals
    x-displayName: Deals
    description: Retrieving and listing deals.
  - name: Finance
    x-displayName: Finance
    description: Balances and crypto withdrawals (USDT).
  - name: Passes
    x-displayName: Passes
    description: Managing whitelist passes (cascade bypass).
  - name: Callbacks
    x-displayName: Webhook delivery
    description: Webhook delivery log.
  - name: Disputes
    x-displayName: Disputes
    description: Appeals (disputes) and dispute chat messages.
  - name: Reference
    x-displayName: Reference
    description: Bank and payment method reference.
  - name: Webhooks
    x-displayName: Webhooks
    description: Outbound webhook events delivered to merchant endpoints.

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

paths:
  # ─── Authentication ─────────────────────────────────────────────────────────
  /api/v1/p2p/merchant/auth:
    post:
      operationId: authenticateMerchant
      summary: Verify merchant credentials
      description: |
        Verifies the merchant token and the HMAC-SHA256 signature. Invoked by
        the API gateway auth interceptor on every request. Public clients do
        NOT call this endpoint directly — it is the canonical verification
        entry point. On success, returns the resolved `tenant_id` and
        `merchant_id`.
      tags: [Authentication]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthenticateMerchantRequest'
      responses:
        '200':
          description: Credentials verified
          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: Issue a new API key pair
      description: |
        Generates a new token + secret pair for the merchant, used for webhook
        signing. The previous credentials are revoked immediately. `secret` is
        shown exactly ONCE — store it securely.
      tags: [API Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssueAPIKeysRequest'
      responses:
        '200':
          description: New credentials issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssueAPIKeysResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/api-keys/rotate:
    post:
      operationId: rotateApiKeys
      summary: Rotate API keys
      description: |
        Atomically replaces the existing credentials. Returns a new token and secret.
        Rotation grace period (confirmed behavior): the outgoing pair remains
        valid for a limited window (15 minutes by default, configurable by the
        operator via `MERCHANT_KEY_GRACE_PERIOD`; `0` disables it). Grace is
        strictly paired — during the window only the FULL old pair (old token +
        old secret) is accepted; mixed combinations return 401. Outbound webhooks
        are signed with the NEW secret immediately.
      tags: [API Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateAPIKeysRequest'
      responses:
        '200':
          description: Credentials rotated
          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: Activate the merchant kill switch
      description: |
        Sets `disabled_at` on the merchant record. All subsequent authentication
        attempts for this merchant return `PERMISSION_DENIED`.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisableMerchantRequest'
      responses:
        '200':
          description: Merchant disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/p2p/merchant/{merchant_id}/enable:
    post:
      operationId: enableMerchant
      summary: Deactivate the merchant kill switch
      description: Clears `disabled_at`, allowing authentication again.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnableMerchantRequest'
      responses:
        '200':
          description: Merchant enabled
          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: Toggle the merchant kill switch (admin only, no body)
      description: |
        Toggles the kill switch state — a bodyless toggle (each call inverts
        disabled ↔ enabled); request fields like `active` or `reason` do NOT
        exist. Admin only: requires a platform operator JWT session (role
        `admin`/`platform_admin`); HMAC pairs and merchant cabinet sessions
        get 403. Admin alias:
        `POST /api/v1/p2p/admin/merchants/{merchant_id}/kill-switch`.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      responses:
        '200':
          description: Kill switch toggled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToggleKillSwitchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Admin role required (merchant HMAC/cabinet sessions are rejected).

  /api/v1/p2p/merchants/{merchant_id}/deposit-address:
    post:
      operationId: generateMerchantDepositAddressLegacy
      summary: Issue (or rotate) the merchant TRC-20 deposit address (legacy path)
      description: |
        Legacy alias with administrative impersonation of the canonical
        `POST /api/v1/p2p/merchant/wallet/deposit-address` (same handler, same
        response). Lets the operator act on the merchant's behalf. With HMAC
        authentication, `merchant_id` in the path MUST match the token claims;
        merchant JWT callers can only access their own merchant
        (IDOR protection). Takes the next BIP44 derivation index,
        derives the address via tron-wallet-api, and stores the address + index
        back into the merchant record. Previously issued addresses remain in
        the eternal block-polling set — deposits sent to a replaced address
        are still detected and credited.
      tags: [Merchants]
      parameters:
        - $ref: '#/components/parameters/MerchantIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateDepositAddressRequest'
      responses:
        '200':
          description: Deposit address generated
          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: Create a PayIn deal (acquiring)
      description: |
        Creates a new incoming payment deal with optional whitelist pass
        matching. Idempotent: a duplicate `idempotency_key` returns the
        cached result. Returns the public `payment_url` that the merchant
        should redirect the client to.
      tags: [Payments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePayInRequest'
      responses:
        '200':
          description: PayIn deal created
          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: Create a PayOut deal (payout)
      description: |
        Initiates a payout to the provided target payment details. Checks the
        merchant balance and places a USDT hold (SELL rate) before creating
        the deal. Next comes synchronous cascade auto-matching: if a free
        trader is available, the deal is returned as `MATCHED`; otherwise as
        `UNASSIGNED`, and the re-matching worker keeps looking until the
        assignment SLA expires the deal (`EXPIRED`, the hold is released).
        Returns the deal with the recipient's `target_requisite` filled in and
        `currency` as an echo of the request (the ISO code of the payout fiat
        currency).
      tags: [Payments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePayOutRequest'
      responses:
        '200':
          description: PayOut deal created
          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: Retrieve a deal by ID
      description: Returns the details of a single deal within the merchant scope.
      tags: [Deals]
      parameters:
        - $ref: '#/components/parameters/DealIdPath'
      responses:
        '200':
          description: Deal details
          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: Cancel a PayIn deal
      description: |
        Explicitly cancels a deposit (PayIn) deal before the payment is
        verified. Allowed only from `INITIALIZED`, `ESCROW_LOCKED`, or
        `PAYMENT_NOTIFIED`; escrow holds are released automatically, the
        fee breakdown is zeroed, and matches are marked `FAILED`. Cancelling
        an already-`CANCELLED` deal again returns it unchanged (idempotent).
        Clients cancel via the public checkout endpoint
        `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: Optional free-form cancellation reason (audit log only).
                  maxLength: 512
      responses:
        '200':
          description: Deal cancelled (or already cancelled — idempotent)
          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: Change the fiat amount of a live deal (operator)
      description: |
        Changes the fiat amount of a deal that has not yet reached
        `PAYMENT_VERIFIED` (`INITIALIZED`, `ESCROW_LOCKED`,
        `PAYMENT_NOTIFIED`, `SOFT_DISPUTED` — the amount is editable until
        authorization). Atomically, in a single transaction:
        the USDT equivalent is recalculated at the deal's own fixed rate,
        every escrow hold (order-level and per-match) is recreated for the
        new amount, an append-only audit row is written, and a
        `deal.amount_changed` webhook event is queued (V2 only). Idempotent
        by `idempotency_key` (same key + same amount → replay; same key +
        different amount → 409). Optimistic locking via
        `expected_amount_version` (If-Match): a concurrent change fails
        with 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: The new fiat amount in minor units (kopecks).
                reason:
                  type: string
                  description: The operator's justification, recorded verbatim in the append-only audit.
                  maxLength: 512
                idempotency_key:
                  type: string
                  format: uuid
                  description: Idempotency key for the operator action.
                expected_amount_version:
                  type: integer
                  format: int64
                  description: |
                    The amount_version the operator last saw (If-Match). 0 or
                    omitted — do not check; a mismatch → 409, re-read the deal.
      responses:
        '200':
          description: Deal amount changed (or an idempotent 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: Conflict — version/key mismatch or the deal is already past PAYMENT_VERIFIED.

  /api/v1/p2p/appeals/{appeal_id}/settlement:
    post:
      operationId: createAppealSettlement
      summary: Settle received funds of an EXPIRED deal as a NEW deal (operator)
      description: |
        Settle-as-received: for an EXPIRED parent that actually received
        funds (`received_amount` > 0), resolving the appeal creates a NEW
        COMPLETED settlement deal for the received amount at the rate
        fixed at resolution time. The expired original stays EXPIRED
        forever (terminal statuses are immutable). The settlement carries
        `parent_deal_id` / `origin_appeal_id`; one appeal can be settled at
        most once (UNIQUE constraint — a repeated call returns the existing
        settlement). Emits the regular webhook sequence
        `deal.created` … `deal.completed` for the settlement.
      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: Manual override of the parent's received_amount; when set, `reason` is mandatory (audit flag).
                reason:
                  type: string
                  description: Required when received_amount_override is set.
                  maxLength: 512
                idempotency_key:
                  type: string
                  format: uuid
                  description: Idempotency key for the operator action; additionally guaranteed by the UNIQUE(origin_appeal_id) constraint.
      responses:
        '200':
          description: Settlement created (or an idempotent replay of the existing one)
          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: Conflict — the parent is not EXPIRED, the appeal is already resolved, or received_amount is zero.

  /api/v1/p2p/merchant/deals:
    get:
      operationId: listDeals
      summary: List merchant deals
      description: |
        Returns the merchant's deals filtered by type, status, and date.
        The listing is a UNION of PayIn (`p2p_orders`) and PayOut
        (`p2p_payouts`) records in a single chronological list. Keyset-based
        pagination via `page_token`. The scope is always derived from the
        signed merchant token (claims take priority); the `merchant_id`
        request field is informational and silently ignored.
      tags: [Deals]
      parameters:
        - name: deal_type
          in: query
          schema:
            $ref: '#/components/schemas/DealType'
          description: Filter by deal direction.
        - name: statuses
          in: query
          schema:
            type: array
            items:
              anyOf:
                - $ref: '#/components/schemas/OrderStatus'
                - $ref: '#/components/schemas/PayOutStatus'
          style: form
          explode: true
          description: Filter by one or more deal statuses (OrderStatus for PayIn, PayOutStatus for PayOut — the UNION listing accepts both sets).
        - name: created_after
          in: query
          schema:
            type: string
            format: date-time
          description: Only deals created at or after this timestamp (RFC 3339, inclusive).
        - name: created_before
          in: query
          schema:
            type: string
            format: date-time
          description: Only deals created at or before this timestamp (RFC 3339, inclusive).
        - $ref: '#/components/parameters/PageSizeQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: List of deals
          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: Retrieve merchant balances
      description: |
        Returns the merchant's real wallets from the money service — one row
        per actual wallet with its own currency and `wallet_id` (no synthetic
        rows). `frozen` reflects active holds.
      tags: [Finance]
      responses:
        '200':
          description: Merchant balances
          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: Withdraw USDT to a TRC-20 address
      description: |
        Initiates a USDT withdrawal to the provided TRC-20 address. The
        synchronous `status` is COMPLETED (auto-approval: the on-chain
        transaction has been sent to broadcast) or PENDING_MANUAL (admin
        approval queue — MANUAL_ONLY policy or an amount above the current
        auto-limit). Insufficient balance — failed precondition → HTTP 400
        (not 402). Idempotency is guaranteed server-side (a deterministic
        key of merchant + address + amount + current fee).
      tags: [Finance]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCryptoWithdrawRequest'
      responses:
        '200':
          description: Withdrawal completed (COMPLETED) or queued for approval (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: Retrieve the merchant's current TRC-20 deposit address
      description: |
        Read-only: returns the merchant's current active TRC-20 deposit
        address used for incoming USDT collateral (topping up the
        merchant's operating balance — payouts, withdrawals, and fees; in
        PayIn deals clients pay to the traders' payment details, NOT to
        this address). `tenant_id` / `merchant_id` in the request are
        informational — with HMAC authentication the scope is always
        resolved from the signed token.
        Returns `404 Not Found` (problem+json) if the address has not
        been issued yet.
      tags: [Finance]
      responses:
        '200':
          description: Current deposit address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetDepositAddressResponse'
              example:
                trc20_address: "TYHCiW3XN5sP9cWJ5T4Dfs9Y18h46t9w1a"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: |
            The deposit address has not been issued yet — call
            POST /wallet/deposit-address to generate one.
          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: Issue (or rotate) the merchant TRC-20 deposit address
      description: |
        Generates (first call) or rotates (subsequent calls) the merchant's
        TRC-20 deposit address for incoming USDT collateral. The scope is
        resolved from the authenticated merchant context (the canonical
        path, without `merchant_id` in the path). Atomically takes the next
        BIP44 derivation index, derives the address via tron-wallet-api, and
        stores the address + index in the merchant record. Rotation
        semantics: the new address immediately replaces the old one in the
        merchant record, while every previously issued address forever
        remains in the platform's eternal block-polling set — USDT sent
        to a replaced address is still detected and credited.
        Legacy alias with administrative impersonation:
        `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: Deposit address generated (or rotated)
          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: Create a whitelist pass
      description: |
        Creates a new whitelist pass in the `ACTIVE` status. Passes bypass
        the standard cascade routing when all non-empty fields match
        the incoming deal.
      tags: [Passes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePassRequest'
      responses:
        '200':
          description: Pass created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatePassResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      operationId: listPasses
      summary: List whitelist passes
      description: Returns the passes filtered by merchant.
      tags: [Passes]
      parameters:
        - $ref: '#/components/parameters/PageSizeCompactQuery'
      responses:
        '200':
          description: List of passes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListPassesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Callbacks ──────────────────────────────────────────────────────────────
  /api/v1/p2p/merchant/callbacks:
    get:
      operationId: listCallbacks
      summary: List webhook delivery records
      description: |
        Returns the merchant's webhook delivery records with optional
        filtering by deal. Bound alias: `GET /api/v1/p2p/callbacks`.
      tags: [Callbacks]
      parameters:
        - name: deal_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter delivery records by deal ID.
        - $ref: '#/components/parameters/PageSizeCompactQuery'
      responses:
        '200':
          description: List of webhook delivery records
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListCallbacksResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ─── Appeals (Disputes) ─────────────────────────────────────────────────────
  /api/v1/p2p/merchant/appeals:
    post:
      operationId: createAppeal
      summary: Open a dispute (appeal)
      description: |
        Opens a dispute on a specific deal. The merchant can attach
        evidence files (by their UUIDs).
      tags: [Disputes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAppealRequest'
      responses:
        '200':
          description: Dispute created
          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: List disputes
      description: Returns all the merchant's disputes with optional status filtering.
      tags: [Disputes]
      parameters:
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/AppealStatus'
          description: Filter by appeal status.
        - $ref: '#/components/parameters/PageSizeCompactQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: List of disputes
          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: Retrieve a dispute by ID
      tags: [Disputes]
      parameters:
        - $ref: '#/components/parameters/AppealIdPath'
      responses:
        '200':
          description: Dispute details
          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: Send a message in a dispute
      description: Sends a text message (optionally with file attachments) to the dispute chat.
      tags: [Disputes]
      parameters:
        - $ref: '#/components/parameters/AppealIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendAppealMessageRequest'
      responses:
        '200':
          description: Message sent
          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: List dispute chat messages
      tags: [Disputes]
      parameters:
        - $ref: '#/components/parameters/AppealIdPath'
        - name: page_size
          in: query
          schema: { type: integer, format: int32 }
          description: Reserved by the contract — the current release returns the full message history (pagination is not applied yet).
        - name: page_token
          in: query
          schema: { type: string }
          description: Reserved by the contract — see page_size above (next_page_token stays empty).
      responses:
        '200':
          description: Chat message history
          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: List available banks and payment methods
      description: |
        The tenant's canonical bank reference (bank routing). `bank_name` is
        the platform's MACHINE bank code (not a display name): pass it in
        `target_bank` on requests and use it to render 1click buttons.
        Availability (payin_available / payout_available) is computed from
        actual coverage — a "dead button" is impossible.
      tags: [Reference]
      parameters:
        - name: country
          in: query
          schema:
            type: string
          description: ISO 3166-1 alpha-2 country code (for example `RU`).
          example: RU
        - name: currency
          in: query
          schema:
            type: string
          description: ISO 4217 currency code (for example `RUB`).
          example: RUB
        - name: method
          in: query
          schema:
            type: string
          description: |
            A method UUID or slug — "banks available for a SPECIFIC method"
            (the source of buttons for `sbp_rub`, `iban_try`, ...). Without
            the parameter — the tenant's full reference.
          example: sbp_rub
        - name: direction
          in: query
          schema:
            type: string
            enum: [PAY_IN, PAY_OUT]
          description: |
            Direction: `PAY_IN` (deposit buttons) or `PAY_OUT` (payout
            banks). Without the parameter — both sides; tell them apart
            by the payin_available / payout_available flags.
          example: PAY_IN
      responses:
        '200':
          description: List of banks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListBanksResponse'
              example:
                data:
                  - bank_name: "sber"
                    display_name: "Sberbank"
                    country: "RU"
                    methods: ["SBP"]
                    payin_available: true
                    payout_available: false
                  - bank_name: "tbank"
                    display_name: "T-Bank"
                    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: List the tenant's active payment methods
      description: |
        Returns payment methods in the ACTIVE status configured for the
        authenticated merchant's tenant: `id` (UUID), `slug`, `name`,
        `requisite_types`, `currency_code`, and `status`.

        This is the **canonical method catalog**: a single method reference —
        a UUID (`id`) or a slug (`slug`) — is accepted by both
        `POST /deals/payin` (`payment_method_id`) and `POST /deals/payout`
        (`payment_method`); both formats resolve to the same row.
        The method currency must match the deal currency (PayOut rejects
        a mismatch with HTTP 400). `GET /banks` does NOT return method
        identifiers — its `methods` field contains only `RequisiteType` values.
      tags: [Reference]
      responses:
        '200':
          description: List of active payment methods
          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: List the tenant's active currencies
      description: |
        The currency reference of the authenticated merchant's tenant
        (HMAC claims or cabinet JWT): the UUID, ISO 4217 alpha code, and
        display name of every ACTIVE `p2p_currencies` row. Used for
        formatting payout amounts and choosing the currency in
        CreateMerchantPayOut.
      tags: [Reference]
      responses:
        '200':
          description: List of active currencies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListMerchantCurrenciesResponse'
              example:
                data:
                  - id: "9a3f1c2e-1234-4abc-9def-56789abcdef0"
                    code: "RUB"
                    name: "Russian ruble"
                    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: Export deals to CSV (canonical path)
      description: |
        Returns the merchant's deals — PayIn (orders) AND PayOut (payouts),
        UNION over the ListMerchantDeals contract — as an RFC 4180 CSV
        document. The filters match the listing one-to-one; no pagination
        (a 10,000 data row limit, `truncated: true` when reached).
        `Accept: text/csv` returns the raw document with
        `Content-Disposition: attachment; filename="deals-YYYYMMDD.csv"`;
        any other Accept gets the JSON wrapper
        (`csv`/`file_name`/`row_count`/`truncated`).
        The legacy mirror `GET /api/v1/p2p/deals/export` (without the
        /merchant prefix) keeps working for wire-compat and is deprecated —
        build new integrations on this canonical path.
      tags: [Payments]
      parameters:
        - name: deal_type
          in: query
          schema: { type: string, enum: [PAYIN, PAYOUT] }
          description: Filter by direction; empty/missing = both (UNION).
        - name: statuses
          in: query
          schema: { type: array, items: { type: string } }
          description: Repeatable DB status names (`statuses=COMPLETED&statuses=EXPIRED`).
        - name: created_after
          in: query
          schema: { type: string, format: date-time }
          description: Lower bound of created_at (RFC3339, inclusive).
        - name: created_before
          in: query
          schema: { type: string, format: date-time }
          description: Upper bound of created_at (RFC3339, inclusive).
      responses:
        '200':
          description: "CSV document (raw with Accept: text/csv, otherwise the JSON wrapper)"
          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: Daily deal turnover (dashboard chart)
      description: |
        One aggregate row per (UTC day × deal_type × currency) within a
        bounded date window, summed over the same UNION ALL surface as the
        listing (orders `amount` for PAYIN, payouts `amount_fiat` for
        PAYOUT). `date_from`/`date_to` are REQUIRED, RFC3339; the window is
        capped at 92 days (beyond that — InvalidArgument). Unsuccessful
        deals are excluded (orders EXPIRED/SPAM_REJECTED, payouts
        FAILED/EXPIRED). Rows are ordered by (date ASC, deal_type ASC,
        currency ASC); days without deals are absent.
      tags: [Payments]
      parameters:
        - name: date_from
          in: query
          required: true
          schema: { type: string, format: date-time }
          description: Start of the window (RFC3339, inclusive).
        - name: date_to
          in: query
          required: true
          schema: { type: string, format: date-time }
          description: End of the window (RFC3339, inclusive; not earlier than date_from).
        - name: deal_type
          in: query
          schema: { type: string, enum: [PAYIN, PAYOUT] }
          description: Optional direction filter; empty = both.
      responses:
        '200':
          description: Aggregated turnover rows
          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: Queue a webhook delivery retry (manual retry)
      description: |
        Queues an immediate re-delivery of ONE of the caller's own webhook
        deliveries. Only records with a terminal failure qualify (status
        ERROR or RETRY); PENDING/SUCCESS are rejected with
        400 FAILED_PRECONDITION. Rate limit: at most one manual retry
        per record per 60 seconds (otherwise 429 RESOURCE_EXHAUSTED).
        Another merchant's callback_id — 403; unknown — 404. The rewritten
        row is returned with status PENDING, cleared response fields, and
        the PRESERVED retry_count (unlike the admin retry, which resets
        the attempt counter). Dual authentication: HMAC pair or cabinet
        JWT. The body is an empty JSON object.
      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: Webhook delivery re-queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetryCallbackResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The delivery record belongs to another merchant (PERMISSION_DENIED).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          description: Manual retry cooldown (60 s per record; RESOURCE_EXHAUSTED).

  /api/v1/p2p/merchant/wallet/withdraw-fee:
    get:
      operationId: estimateMerchantWithdrawFee
      summary: Estimate the withdrawal fee before sending a request
      description: |
        Read-only estimate of the withdrawal fee that will be applied to
        the authenticated merchant BEFORE submission: a counterparty-specific
        override if one is set, otherwise the tenant base — resolved through
        the SAME policy resolver the payment path uses, so the estimate can
        never diverge from the fee actually charged. The optional
        `amount_usdt_cents` (USDT cents; note that the create request takes
        whole units) also yields the net amount.
      tags: [Finance]
      parameters:
        - name: amount_usdt_cents
          in: query
          required: false
          schema: { type: integer, format: int64 }
          description: Optional gross withdrawal amount in USDT cents (> 0 enables net_amount_usdt_cents).
          example: 100000
      responses:
        '200':
          description: Fee estimate
          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: Incoming TRC-20 deposits with custody statuses
      description: |
        The merchant's incoming TRC-20 deposits: ledger credit plus the
        receiving address lifecycle from DepositSweepWorker
        (CREDITED / SWEEP_PENDING / SWEEP_FAILED / SWEEP_STUCK / SWEPT).
        Keyset pagination (AIP-158): page_size 1–100 (default 20),
        page_token/next_page_token.
      tags: [Finance]
      parameters:
        - $ref: '#/components/parameters/PageSizeCompactQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: List of deposits
          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: Withdrawal history (manual queue merged with executed debits)
      description: |
        The merchant's withdrawal history: the manual approval queue merged
        with executed ledger debits, deduplicated by the on-chain
        transaction hash. Statuses: PENDING (awaiting approval), APPROVED
        (approved, not yet in broadcast), COMPLETED (broadcast,
        tx hash present), CANCELLED (rejected by the operator). The same
        keyset pagination as deposits.
      tags: [Finance]
      parameters:
        - $ref: '#/components/parameters/PageSizeCompactQuery'
        - $ref: '#/components/parameters/PageTokenQuery'
      responses:
        '200':
          description: List of withdrawals
          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: Own merchant profile, limits, and kill switch banner
      description: |
        Self-service read of the merchant's OWN profile: identification,
        the api-credentials representation, webhook URL, redirect URLs,
        IP whitelist, deal limits (deal_min_amount / deal_max_amount /
        daily_deal_amount_limit — present only when set) and the
        kill_switch / kill_switch_reason banner fields. Dual
        authentication: cabinet JWT (Authorization: Bearer, merchant role)
        OR the merchant HMAC pair — the scope is resolved from the
        presented credentials.
      tags: [Management]
      security:
        - bearerAuth: []
        - MerchantToken: []
          MerchantSignature: []
      responses:
        '200':
          description: Merchant profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyMerchantResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      operationId: updateMyMerchant
      summary: Update your own profile and deal limits (cabinet JWT)
      description: |
        Partial update of the merchant's own profile: `name`, `webhook_url`,
        `withdrawal_address`, `success_redirect_url`, `fail_redirect_url`,
        `checkout_brand_name`, `finance_chat_id`, `appeal_chat_id`,
        `ip_whitelist`, and the three deal limits. Limit pointer semantics:
        missing = unchanged, `0` = cleared (NULL), `>0` = set (in minor
        units). The min/max pair is checked for consistency. Status,
        balances, and fees are admin-only. Authentication: cabinet 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: Updated merchant profile (the full object)
          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: Deal created
      description: Emitted when a new deal enters the `INITIALIZED` state.
      tags: [Webhooks]
      x-webhook-event: "deal.created"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_created:
                summary: "PayIn created"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
        '2xx':
          description: Any 2xx response counts as success.
  deal.escrow_locked:
    post:
      summary: Deal escrow locked
      description: Emitted when the collateral escrow is locked (`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: "Escrow collateral locked"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.payment_notified:
    post:
      summary: Payer reported the payment
      description: Emitted when the payer reports having paid (`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: "The payer reported the payment"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.amount_changed:
    post:
      summary: Deal amount changed
      description: |
        Emitted when an operator changes the amount of a live deal before
        `PAYMENT_VERIFIED` (the Amend operation). `amount` carries the NEW
        amount, `old_amount` the previous one, `amount_version` the monotonic
        edit counter (a new deal starts at 1, every edit increments it).
        Escrow holds are recreated for the new amount and the USDT
        equivalent is recalculated at the deal's own rate; the deal status
        does not change. V2 merchants only — the V1 adapter has no
        callbackType for this event and never delivers it.
      tags: [Webhooks]
      x-webhook-event: "deal.amount_changed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_amount_changed:
                summary: "Operator changed the live deal amount"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'

  deal.payment_verified:
    post:
      summary: Payment verified
      description: Emitted when the payment is verified (`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: "Payment verified"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.completed:
    post:
      summary: Deal completed
      description: Emitted when a deal reaches the terminal `COMPLETED` state.
      tags: [Webhooks]
      x-webhook-event: "deal.completed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_completed:
                summary: "Deal completed"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.expired:
    post:
      summary: Deal expired
      description: Emitted on deal timeout (`EXPIRED`).
      tags: [Webhooks]
      x-webhook-event: "deal.expired"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_expired:
                summary: "Deal timed out"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.cancelled:
    post:
      summary: Deal cancelled
      description: Emitted when the client or the merchant explicitly cancels a PayIn deal before the payment is verified (`CANCELLED`).
      tags: [Webhooks]
      x-webhook-event: "deal.cancelled"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_cancelled:
                summary: "Deal explicitly cancelled before payment verification"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.appealed:
    post:
      summary: A dispute was opened on the deal
      description: Emitted when a dispute is opened on a deal (`APPEALED`).
      tags: [Webhooks]
      x-webhook-event: "deal.appealed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_appealed:
                summary: "A dispute was opened on the deal"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.refund_pending:
    post:
      summary: Refund in progress (transit to cancellation)
      description: A transit notification. Emitted when a deal enters `REFUND_PENDING` (a rejected appeal / third-party flag); it is always followed by `deal.cancelled` — the final outcome for the merchant is CANCELLED. The payload does not change.
      tags: [Webhooks]
      x-webhook-event: "deal.refund_pending"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_refund_pending:
                summary: "Refund initiated"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  deal.failed:
    post:
      summary: Deal failed
      description: Emitted when a PayOut deal fails (`FAILED`).
      tags: [Webhooks]
      x-webhook-event: "deal.failed"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackEventPayload'
            examples:
              deal_failed:
                summary: "PayOut deal failed"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  p2p.finance.withdrawal_completed:
    post:
      summary: Crypto withdrawal completed
      description: |
        Emitted when a USDT withdrawal reaches the terminal state
        `APPROVED` (the on-chain broadcast succeeded, the ledger is debited).
      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 withdrawal completed 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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  p2p.finance.withdrawal_failed:
    post:
      summary: Crypto withdrawal failed / cancelled
      description: |
        Emitted when a USDT withdrawal is rejected by the operator or
        cancelled before broadcast (the terminal `CANCELLED`); the reserved
        amount is returned to the merchant balance. `deal_id` carries the
        withdrawal UUID; there is no on-chain hash on this path.
      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 withdrawal rejected by the operator"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'
  payment_methods.updated:
    post:
      summary: Payment method catalog changed (V2 broadcast only)
      description: |
        Emitted when an administrator creates a payment method or updates
        its catalog-visible fields (`name`, `status`, `requisite_types`,
        `is_cross_border`). Broadcast to every ACTIVE V2 merchant of the
        tenant with a non-empty webhook_url (legacy V1 merchants never
        receive it — the V1 wrapper has no catalog callbackType).
        No-op updates emit nothing. The wrapper is a separate catalog form:
        the deal fields (`amount`, `currency`, `deal_id`) are absent. This
        is a NOTIFICATION, not a source of data: on receiving it, re-fetch
        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: "A catalog method disabled by an admin"
                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: Webhook received
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAck'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "Merchant cabinet session JWT (Authorization header, Bearer scheme) — used by the /api/v1/p2p/my-merchant endpoint."
    MerchantToken:
      type: apiKey
      in: header
      name: X-Merchant-Token
      description: The merchant's public token (64 hex characters), issued at onboarding.
    MerchantSignature:
      type: apiKey
      in: header
      name: X-Signature
      description: |
        HMAC-SHA256 signature (a single scheme for all endpoints).
        Format: `t=<unix_seconds>,v1=<hex_hmac_sha256>`
        where `hex_hmac = HMAC-SHA256(webhook_secret, "<unix_seconds>.<raw_body>")`.
        Replay window: 5 minutes (a 60-second drift into the future is allowed).
    MerchantTimestamp:
      type: apiKey
      in: header
      name: X-Timestamp
      description: >-
        Unix time of the request in seconds. Signature window: 5 minutes back /
        60 seconds forward.

  headers:
    XTraceId:
      description: |
        Correlation trace ID (a response header). Present on every error
        response; generated by the API gateway if the upstream did not
        provide one. Quote EXACTLY this value when contacting support.
        Exposed to browser clients via the CORS
        `Access-Control-Expose-Headers` header.
      schema:
        type: string
      example: "4f7c1a9b8e2d"

  parameters:
    MerchantIdPath:
      name: merchant_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The target merchant UUID.
    DealIdPath:
      name: deal_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The deal UUID.
    AppealIdPath:
      name: appeal_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The appeal UUID.
    PageSizeQuery:
      name: page_size
      in: query
      schema:
        type: integer
        format: int32
        minimum: 1
        maximum: 250
        default: 50
      description: |
        The maximum number of results returned (deal listing semantics:
        50 by default, 250 at most). Compact listings (callbacks, passes,
        appeals, deposits, withdrawals) use 20 / 100 — see the per-endpoint
        parameter overrides.
    PageSizeCompactQuery:
      name: page_size
      in: query
      schema:
        type: integer
        format: int32
        minimum: 1
        maximum: 100
        default: 20
      description: The maximum number of results returned (20 by default, 100 at most).
    PageTokenQuery:
      name: page_token
      in: query
      schema:
        type: string
      description: An opaque pagination token returned by the previous page.

  responses:
    BadRequest:
      description: |
        A validation error (gRPC INVALID_ARGUMENT) or an invalid state
        (FAILED_PRECONDITION): insufficient balance, the daily deal limit,
        an active appeal, no webhook configured, retry unavailable.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Authentication failed (token or signature missing/invalid).
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: The requested resource was not found.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: A concurrent whitelist pass reservation (gRPC ABORTED) — the only 409 path in the API; safe to retry.
      headers:
        X-Trace-Id:
          $ref: '#/components/headers/XTraceId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NoTraderAvailable:
      description: No trader available for cascade routing — try again later.
      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: |
        The operation completed successfully — grpc-gateway renders the
        EMPTY proto message as an empty JSON object `{}` (no `ok` field).

    ErrorResponse:
      type: object
      description: |
        An error in the RFC 9457 problem details format. Examples are
        auto-generated by fumadocs from the schema (no hard-coded
        instances). The body carries NO trace fields — the correlation ID
        travels in the `X-Trace-Id` response header
        (see components/headers/XTraceId).
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri-reference
          description: A URI identifying the error type.
        title:
          type: string
          description: A short human-readable description of the error.
        status:
          type: integer
          format: int32
          description: The HTTP status code.
        detail:
          type: string
          description: A human-readable explanation specific to this occurrence.
        instance:
          type: string
          format: uri-reference
          description: A URI identifying the specific error occurrence.
        errors:
          type: array
          description: Field-level validation errors.
          items:
            $ref: '#/components/schemas/FieldError'

    FieldError:
      type: object
      required: [field, message]
      properties:
        field:
          type: string
          description: The JSON path of the invalid field.
          example: "amount"
        message:
          type: string
          description: A field-level error description.
          example: "must be greater than 0"
        code:
          type: string
          description: A stable machine-readable error code.
          example: "POSITIVE_REQUIRED"

    # ─── Enums (canonical V2 values) ──────────────────────────────────────────
    OrderStatus:
      type: string
      description: |
        The canonical V2 order lifecycle state (FSA). Exactly matches
        `domain.OrderStatus` and the `OrderStatus` proto enum.
      enum:
        - INITIALIZED
        - ESCROW_LOCKED
        - PAYMENT_NOTIFIED
        - PAYMENT_VERIFIED
        - COMPLETED
        - EXPIRED
        - CANCELLED
        - APPEALED
        - REFUND_PENDING
        - SPAM_REJECTED
        - SOFT_DISPUTED

    DealType:
      type: string
      description: The P2P deal direction.
      enum:
        - PAYIN
        - PAYOUT

    AppealStatus:
      type: string
      description: The appeal (dispute) lifecycle state.
      enum:
        - OPEN
        - IN_PROGRESS
        - REOPENED
        - SATISFIED
        - REJECTED

    PayOutStatus:
      type: string
      description: The PayOut lifecycle state.
      enum:
        - INITIALIZED
        - MATCHED
        - UNASSIGNED
        - PROCESSING
        - COMPLETED
        - FAILED
        - EXPIRED

    WithdrawalStatus:
      type: string
      description: The crypto withdrawal (USDT) lifecycle state.
      enum:
        - PENDING
        - APPROVED
        - CANCELLED

    PassStatus:
      type: string
      description: The whitelist pass lifecycle state.
      enum:
        - ACTIVE
        - RESERVED
        - USED
        - EXPIRED

    RequisiteType:
      type: string
      description: The classification of a trader's payment instrument.
      enum:
        - BANK_CARD
        - E_WALLET
        - SBP
        - SIM
        - BANK_TRANSFER

    AmountMatchStatus:
      type: string
      description: |
        How well the expected and the actually received payment amounts
        match. Values are lowercase strings.
      enum:
        - full
        - partial
        - overpaid

    CallbackDeliveryStatus:
      type: string
      description: The webhook delivery record lifecycle.
      enum:
        - PENDING
        - SUCCESS
        - RETRY
        - ERROR

    SenderRole:
      type: string
      description: The role of the message sender in the dispute chat.
      enum:
        - MERCHANT
        - TRADER
        - ADMIN

    # ─── Authentication ───────────────────────────────────────────────────────
    AuthenticateMerchantRequest:
      type: object
      properties:
        tenant_id:
          type: string
          format: uuid
          description: The tenant UUID (injected by the API gateway from the JWT claim).
        token:
          type: string
          description: The merchant's public token (64 hex characters).
          example: "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0"
        body:
          type: string
          description: The raw request body for recalculating the signature.
        signature:
          type: string
          description: The hex HMAC-SHA256 signature over the body with the `webhook_secret` key.

    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: Injected from the auth context.
        merchant_id:
          type: string
          format: uuid
          description: Injected from the auth context.

    IssueAPIKeysResponse:
      type: object
      required: [token, secret]
      properties:
        token:
          type: string
          description: The new API token (64 hex characters, NOT stored in plain text).
        secret:
          type: string
          description: The webhook signing secret (32 bytes). Shown exactly ONCE.

    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 when the kill switch is active.
        disabled_at:
          type: string
          format: date-time
          description: The RFC 3339 timestamp of disabling; absent when enabled.

    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: The TRC-20 deposit address.
          example: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
        wallet_derivation_index:
          type: integer
          format: int32
          description: The BIP44 derivation index used to derive the address.

    GetDepositAddressResponse:
      type: object
      required: [trc20_address]
      properties:
        trc20_address:
          type: string
          description: |
            The current active TRC-20 deposit address. Mirrors the
            Generate response form WITHOUT the derivation index
            (a read-only representation).
          example: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
        updated_at:
          type: string
          format: date-time
          description: The RFC 3339 timestamp of the last generation/rotation (the merchant record's updated_at).

    # ─── PayIn / PayOut ──────────────────────────────────────────────────────
    CreatePayInRequest:
      type: object
      required: [amount, currency, idempotency_key, payment_method_id]
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Injected from the auth context; omit it in client requests.
        merchant_id:
          type: string
          format: uuid
          description: Injected from the auth context; omit it in client requests.
        amount:
          type: integer
          format: int64
          description: The amount in minor currency units (kopecks for RUB, for example).
          example: 150000
        currency:
          type: string
          description: ISO 4217 currency code (for example `RUB`).
          example: RUB
        idempotency_key:
          type: string
          description: |
            A unique request key. Repeated requests with the same key return
            the cached result instead of creating a new deal.
          example: "payin_order_12345"
        payment_method_id:
          type: string
          description: |
            The REQUIRED single method reference — the method UUID (the
            canonical fast path by PK) or the catalog slug (`sbp_rub`,
            `card_rub`, ...). Both resolve to the same row via
            `GET /api/v1/p2p/merchant/payment-methods`; unknown references
            are rejected with HTTP 400 and a list of valid slugs. An empty
            value is rejected (cascade routing is bound to the method).
            `GET /banks` does NOT return method identifiers — its `methods`
            field contains only `RequisiteType` values.
          example: "550e8400-e29b-41d4-a716-446655440000"
        currency_id:
          type: string
          format: uuid
          description: An optional currency UUID; required for whitelist pass matching.
        client_id:
          type: string
          description: The client (player) identifier in the merchant's system.
          example: "player_42"
        target_bank:
          type: string
          description: |
            The canonical bank code of the requisite (bank routing,
            optional): a `bank_name` value from GET /banks?method=... —
            the "Alfa 1click" button = method + `target_bank: alfa`. Not
            passed — routing by method as before (byte-identical). An
            empty string is an error; an unknown/disabled/method-unbound
            code = 400 with a list of valid ones. Replay semantics:
            repeating the idempotency_key returns the original deal;
            a different bank — a new key.
          example: "alfa"
        payer_bank:
          type: string
          description: An optional payer bank hint for cascade routing.
        description:
          type: string
          description: An optional note from the merchant, shown on the payment page.
        callback_url:
          type: string
          format: uri
          description: |
            A per-deal callback URL override (PayIn only). Delivery
            hierarchy: per-deal `callback_url` → `merchant.webhook_url`;
            when both are empty, no webhook is created for the deal. PayOut
            requests have no per-deal override — payout webhooks always go
            to `merchant.webhook_url`.
        is_client_commission:
          type: boolean
          default: false
          description: If true, the client pays the commission (instead of the merchant).

    CreatePayOutRequest:
      type: object
      required: [amount, currency, wallet_id, idempotency_key, target_requisite, payment_method]
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Injected from the auth context; omit it in client requests.
        merchant_id:
          type: string
          format: uuid
          description: Injected from the auth context; omit it in client requests.
        amount:
          type: integer
          format: int64
          description: The payout amount in minor currency units.
          example: 500000
        currency:
          type: string
          description: ISO 4217 currency code.
          example: RUB
        wallet_id:
          type: string
          format: uuid
          description: |
            The UUID of the merchant's **USDT wallet** to debit — the hold
            is placed in USDT (converted at the SELL rate at creation
            time), NOT on the fiat wallet. Use the `wallet_id` of the
            USDT row from `GET /balance`.
        idempotency_key:
          type: string
          description: A unique operation key.
          example: "payout_order_99812"
        target_requisite:
          type: string
          description: The recipient payment details (card number, SBP phone, IBAN).
          example: "2202201234567890"
        target_name:
          type: string
          description: The recipient's full name for reconciliation.
          example: "Ivan Ivanovich I."
        target_bank:
          type: string
          description: |
            The recipient's bank: a slug (for example `ziraat`, `garanti`)
            or a human-readable name. Passed to the provider as the payout
            bank and used for intrabank cascade matching (TRY/Havale).
            Empty = no bank hint (card/SBP payouts).
          example: "ziraat"
        requisite_details:
          type: object
          additionalProperties:
            type: string
          description: |
            Named recipient details beyond `target_requisite` /
            `target_name`. Allowed keys (enum): `iban`, `account_number`,
            `document_number`, `bank_name`, `phone_number`. Unknown keys
            are rejected with HTTP 400 (InvalidArgument) and a list of
            valid ones. Optional: missing/empty = no extra details. The
            values are the merchant's own details and are returned
            unmasked.
          example:
            iban: "TR33 0006 1005 1978 6457 8413 26"
            document_number: "12345678901"
        payment_method:
          type: string
          description: |
            The REQUIRED single method reference — the catalog **slug**
            (`sbp_rub`, `card_rub`, `mobcom_rub`, `express-havale`, ...;
            the `slug` field from
            `GET /api/v1/p2p/merchant/payment-methods`, identical across
            all environments) or the method's **UUID** (the `id` field);
            both resolve to the same row. NOT the uppercase
            `RequisiteType` enum (those are informational
            `requisite_types`, never passed in requests). An
            unknown/missing reference, or a method serving a different
            deal currency, is rejected with HTTP 400 (a currency
            cross-check; the error carries the list of valid slugs).
            PayIn uses the same single contract via `payment_method_id`.
          example: "card_rub"

    MerchantDealInfo:
      type: object
      description: |
        The canonical M4 representation of a deal returned by the merchant
        API. Exactly mirrors the `MerchantDealInfo` proto message.
      properties:
        deal_id:
          type: string
          format: uuid
          description: The deal UUID.
        status:
          $ref: '#/components/schemas/OrderStatus'
          description: |
            The canonical status — `OrderStatus` for PayIn deals,
            `PayOutStatus` for PayOut deals (for example `MATCHED`
            immediately after the synchronous auto-matching at creation).
        amount:
          type: integer
          format: int64
          description: The deal amount in minor currency units.
        currency:
          type: string
          description: |
            The ISO 4217 currency code (an echo of the request currency;
            for PayOut — the payout fiat currency).
        created_at:
          type: string
          format: date-time
          description: The RFC 3339 creation timestamp.
        client_id:
          type: string
          description: The client (player) identifier.
        deal_type:
          $ref: '#/components/schemas/DealType'
        updated_at:
          type: string
          format: date-time
          description: The RFC 3339 timestamp of the last update.
        payment_url:
          type: string
          format: uri
          description: |
            The hosted-payform URL to redirect the client to. Filled only
            for PayIn deals; an empty string for PayOut.
        target_requisite:
          type: string
          description: |
            The recipient payment details (an echo of the request value,
            unmasked). Filled only for PayOut deals; an empty string for
            PayIn.

    PayInResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        deal:
          $ref: '#/components/schemas/MerchantDealInfo'

    PayOutResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        deal:
          $ref: '#/components/schemas/MerchantDealInfo'

    RetrieveDealResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        deal:
          $ref: '#/components/schemas/MerchantDealInfo'

    ListDealsResponse:
      type: object
      description: |
        The canonical list wrapper (CONVENTIONS §2.2, applied by the HTTP
        egress marshaller since 2026-08-24): only `data` +
        `next_page_token`. The per-entity proto field name (`deals`) and
        `total_count` exist in the internal gRPC contract but are NEVER
        exposed over HTTP.
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MerchantDealInfo'
        next_page_token:
          type: string
          description: The opaque next-page token (empty when there are no more results).

    # ─── Balance ─────────────────────────────────────────────────────────────
    MerchantWalletBalance:
      type: object
      properties:
        wallet_id:
          type: string
          format: uuid
          description: The money service wallet UUID (every row is a real wallet; the TRC-20 deposit address is a separate entity, see /wallet/deposit-address).
        currency:
          type: string
          description: The currency code (configured by the tenant).
        available:
          type: integer
          format: int64
          description: The available balance (in minor currency units).
        frozen:
          type: integer
          format: int64
          description: The frozen / on-hold balance.
        total:
          type: integer
          format: int64
          description: The total balance (`available + frozen`).

    BalanceResponse:
      type: object
      description: |
        grpc-gateway renders the proto fields at the top level (no `data`
        wrapper). `wallets` are the merchant's real wallets from the
        money service.
      properties:
        wallets:
          type: array
          items:
            $ref: '#/components/schemas/MerchantWalletBalance'

    # ─── Passes ──────────────────────────────────────────────────────────────
    CreatePassRequest:
      type: object
      required: [amount]
      description: Only `amount` is required — all other filters are optional (empty = any).
      properties:
        tenant_id:
          type: string
          format: uuid
          description: Injected from the auth context.
        merchant_id:
          type: string
          format: uuid
          description: An optional merchant UUID; empty = any merchant.
        provider_id:
          type: string
          format: uuid
          description: An optional provider UUID; empty = any provider.
        amount:
          type: integer
          format: int64
          description: The pre-approved amount in minor currency units.
        currency_id:
          type: string
          format: uuid
          description: An optional currency UUID; empty = any currency.
        payment_method_id:
          type: string
          format: uuid
          description: "An optional payment method UUID binding the pass to a single method; empty = any method. UUID only — unlike deals, slug references are not resolved on the pass path."
        requisite_type:
          $ref: '#/components/schemas/RequisiteType'
        expires_at:
          type: string
          format: date-time
          description: An optional RFC 3339 expiration; empty = never expires.

    Pass:
      type: object
      properties:
        pass_id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        merchant_id:
          type: string
          format: uuid
          description: Empty when the pass applies to all merchants.
        provider_id:
          type: string
          format: uuid
          description: "Always empty in merchant-facing responses (TD-080: providers stay anonymous)."
        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: The RFC 3339 expiration, or empty when valid forever.
        used_deal_id:
          type: string
          format: uuid
          description: The UUID of the deal that used this pass, or empty.
        created_at:
          type: string
          format: date-time

    CreatePassResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        pass:
          $ref: '#/components/schemas/Pass'

    ListPassesResponse:
      type: object
      description: The canonical list wrapper (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Pass'

    # ─── Callbacks ───────────────────────────────────────────────────────────
    Callback:
      type: object
      description: A webhook delivery record.
      properties:
        callback_id:
          type: string
          format: uuid
        deal_id:
          type: string
          format: uuid
          description: The deal UUID (empty for payout/withdrawal callbacks).
        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: The HTTP status code returned by the merchant endpoint.
        response_body:
          type: string
          description: The response body captured from the merchant endpoint.
        next_retry_at:
          type: string
          format: date-time
          description: The RFC 3339 timestamp of the next retry, or empty in a terminal state.
        created_at:
          type: string
          format: date-time

    ListCallbacksResponse:
      type: object
      description: The canonical list wrapper (`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: |
            Whole USDT (integer, not float): `1000` = 1000 USDT.
            Internally converted to USDT cents (×100) for storage.
          example: 1000
        tron_address:
          type: string
          description: The destination TRC-20 address (base58check, 34 characters; an invalid format → 400).
          example: "TRx7Vn2a3bK8HJdZmFc4eD5q1wP9nLs6t"
        idempotency_key:
          type: string
          description: |
            Present in the contract but NOT required/checked — the platform
            guarantees withdrawal idempotency server-side (a deterministic
            key of merchant + address + amount + current fee), so an
            identical retry converges to the same on-chain transaction.
            The value is ignored.

    CryptoWithdrawResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        status:
          type: string
          enum: [COMPLETED, PENDING_MANUAL]
          description: |
            The synchronous outcome of THIS request: COMPLETED
            (auto-approved — the on-chain transaction has been sent to
            broadcast, transaction_id carries the hash) or PENDING_MANUAL
            (sent to the admin approval queue). The DOMAIN status
            WithdrawalStatus (PENDING/APPROVED/CANCELLED — withdrawal rows,
            listings, webhook events) is a separate surface.
        transaction_id:
          type: string
          description: The on-chain TX hash (set only for COMPLETED; empty for PENDING_MANUAL).
        amount_usdt:
          type: integer
          format: int64
          description: An echo of the requested amount (whole USDT).
        tron_address:
          type: string
        created_at:
          type: string
          format: date-time
        fee_usdt_cents:
          type: integer
          format: int64
          description: The withdrawal fee in USDT cents — the one actually charged (auto path) or the fee the manual-approval path will charge.
        net_usdt_cents:
          type: integer
          format: int64
          description: "The net on-chain amount the recipient receives — amount_usdt*100 minus fee_usdt_cents."

    # ─── Appeals (Disputes) ──────────────────────────────────────────────────
    CreateAppealRequest:
      type: object
      required: [deal_id, reason]
      properties:
        deal_id:
          type: string
          format: uuid
          description: The UUID of the deal the dispute is opened on.
        reason:
          type: string
          description: A free-form reason for opening the dispute.
          example: "Funds not credited within 30 minutes"
        file_ids:
          type: array
          items:
            type: string
            format: uuid
          description: UUIDs of the uploaded evidence files.

    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: The resolution set when the dispute is closed.
        created_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
          description: The RFC 3339 resolution timestamp, or empty while open.
        updated_at:
          type: string
          format: date-time

    CreateAppealResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        appeal:
          $ref: '#/components/schemas/AppealInfo'

    RetrieveAppealResponse:
      type: object
      description: grpc-gateway renders the proto fields at the top level (no `data` wrapper).
      properties:
        appeal:
          $ref: '#/components/schemas/AppealInfo'

    ListAppealsResponse:
      type: object
      description: The canonical list wrapper (`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: The message text.
        file_ids:
          type: array
          items:
            type: string
            format: uuid
          description: UUIDs of the attached files.

    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 renders the proto fields at the top level (no `data` wrapper).
      properties:
        message:
          $ref: '#/components/schemas/AppealMessageInfo'

    ListAppealMessagesResponse:
      type: object
      description: The canonical list wrapper (`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: |
            The platform's canonical MACHINE bank code (bank routing) —
            the value for `target_bank` on requests. Not a display name.
          example: "sber"
        display_name:
          type: string
          description: A human-readable name.
          example: "Sberbank"
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code.
          example: "RU"
        methods:
          type: array
          items:
            $ref: '#/components/schemas/RequisiteType'
          description: The supported requisite types (with ?method= — the types of that method).
        payin_available:
          type: boolean
          description: The bank actually serves deposits right now (live coverage).
        payout_available:
          type: boolean
          description: The bank actually serves payouts right now (live coverage).

    ListBanksResponse:
      type: object
      description: The canonical list wrapper (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/BankInfo'

    MerchantPaymentMethodInfo:
      type: object
      description: |
        One ACTIVE payment method row of the merchant's tenant joined
        with its currency. `id` is the UUID passed as `payment_method_id`
        in `CreatePayInRequest`; `slug` is the value accepted by the
        `payment_method` field in `CreatePayOutRequest`.
      properties:
        id:
          type: string
          format: uuid
          description: The payment method UUID (`payment_method_id` for PayIn).
          example: "550e8400-e29b-41d4-a716-446655440000"
        slug:
          type: string
          description: The payment method slug (`payment_method` for PayOut).
          example: "express-havale"
        name:
          type: string
          description: The method's human-readable name.
          example: "Havale Express (TRY)"
        requisite_types:
          type: array
          items:
            $ref: '#/components/schemas/RequisiteType'
          description: The requisite types the method serves.
        currency_code:
          type: string
          description: ISO 4217 currency code (joined from `p2p_currencies`).
          example: "TRY"
        status:
          type: string
          enum: [ACTIVE]
          description: Always `ACTIVE` — the endpoint filters out inactive rows.

    ListMerchantPaymentMethodsResponse:
      type: object
      description: The canonical list wrapper (`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: ISO 4217 alpha code (RUB, TRY, ...).
        name:
          type: string
          description: The display name in the merchant cabinet.
        status:
          type: string
          description: Always ACTIVE (the endpoint filters).

    ListMerchantCurrenciesResponse:
      type: object
      description: The canonical list wrapper (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MerchantCurrencyInfo'

    ExportDealsResponse:
      type: object
      description: "The JSON wrapper of the CSV export (the raw document is returned with Accept: text/csv)."
      properties:
        csv:
          type: string
          description: The full RFC 4180 CSV document (header `id,date,type,amount,currency,status,counterparty`, CRLF, quoted fields; amounts in MAJOR units).
        file_name:
          type: string
          description: deals-YYYYMMDD.csv (the UTC date of the export).
        row_count:
          type: integer
          description: Data rows excluding the header.
        truncated:
          type: boolean
          description: true = the 10,000-row limit was reached and rows were cut off.

    TurnoverRow:
      type: object
      properties:
        date:
          type: string
          description: The calendar UTC day of created_at ("YYYY-MM-DD").
        deal_type:
          type: string
          enum: [PAYIN, PAYOUT]
        amount_minor:
          type: integer
          format: int64
          description: The summed deal amount in the currency's minor unit.
        currency:
          type: string
          description: ISO 4217 currency code.

    TurnoverResponse:
      type: object
      description: The canonical list wrapper (`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: The tenant base fee (p2p_system_config.withdrawal_fee_usdt_cents).
        override_applied:
          type: boolean
          description: true = the base replaced by a merchant-specific override.
        total_fee_usdt_cents:
          type: integer
          format: int64
          description: "The fee actually charged — the override if set, otherwise the base."
        net_amount_usdt_cents:
          type: integer
          format: int64
          description: amount − total_fee; 0 when no amount was passed. May be ≤ 0 when the fee covers the entire amount (such a withdrawal is rejected by the payment path).

    DepositEntrySchema:
      type: object
      properties:
        block_timestamp:
          type: string
          format: date-time
          description: The ledger credited_at — the on-chain block timestamp plus the finality depth of 19 confirmations.
        amount_usdt_cents:
          type: integer
          format: int64
          description: The credited amount in USDT cents.
        tx_hash:
          type: string
          description: The TRON on-chain transaction hash.
        trc20_address:
          type: string
          description: The receiving TRC-20 address the deposit arrived at.
        status:
          type: string
          enum: [CREDITED, SWEEP_PENDING, SWEEP_FAILED, SWEEP_STUCK, SWEPT]
          description: Ledger credit + the DepositSweepWorker custody lifecycle.

    ListDepositsResponse:
      type: object
      description: The canonical list wrapper (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/DepositEntrySchema'
        next_page_token:
          type: string
          description: An opaque keyset cursor; an empty string = the last page.

    WithdrawalEntrySchema:
      type: object
      properties:
        created_at:
          type: string
          format: date-time
          description: The withdrawal request time (manual queue) or the ledger debit time (auto path).
        amount_usdt_cents:
          type: integer
          format: int64
          description: The requested gross amount in USDT cents.
        fee_usdt_cents:
          type: integer
          format: int64
          description: The withdrawal fee in USDT cents (0 for history predating the fee).
        net_usdt_cents:
          type: integer
          format: int64
          description: The net on-chain amount = gross − fee.
        tx_hash:
          type: string
          description: The TRON on-chain transaction hash; empty before broadcast.
        status:
          type: string
          enum: [PENDING, APPROVED, COMPLETED, CANCELLED]
          description: PENDING — awaiting admin approval; APPROVED — approved, not yet in broadcast; COMPLETED — broadcast (tx hash present); CANCELLED — rejected/cancelled by the operator.
        tron_address:
          type: string
          description: The destination address on the TRON network.

    ListWithdrawalsResponse:
      type: object
      description: The canonical list wrapper (`data` + `next_page_token`).
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WithdrawalEntrySchema'
        next_page_token:
          type: string
          description: An opaque keyset cursor; an empty string = the last page.

    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: The operator-controlled kill switch (toggled by admins only; the merchant sees the state but can never toggle it via the API).
            kill_switch_reason:
              type: string
              description: The platform kill-switch reason shown as a cabinet banner; empty when it is off.
            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: The merchant's own per-deal minimum (minor units); missing = not set (proto3 optional).
            deal_max_amount:
              type: string
              description: The merchant's own per-deal maximum (minor units); missing = not set.
            daily_deal_amount_limit:
              type: string
              description: The merchant's own daily (UTC) PayIn+PayOut volume limit (minor units); missing = not set.
        auto_withdrawal_mode:
          type: string
          enum: [AUTO_UP_TO_LIMIT, MANUAL_ONLY]
        auto_withdrawal_effective_limit_usdt_cents:
          type: string

    UpdateMyMerchantRequest:
      type: object
      description: "A partial update — pass only the changed fields. Limit pointers — missing = unchanged, 0 = cleared, >0 = set."
      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: A JSON array of IP addresses as a string.
        deal_min_amount:
          type: string
        deal_max_amount:
          type: string
        daily_deal_amount_limit:
          type: string

    # ─── Webhook Payload ─────────────────────────────────────────────────────
    WebhookAck:
      type: object
      description: The acknowledgment body merchants return to accept a webhook delivery.
      properties:
        received:
          type: boolean
          description: Set to `true` to acknowledge receipt of the webhook.
          example: true

    CatalogMethodBefore:
      type: object
      description: A snapshot of the method's catalog-visible fields before the update (update events only).
      properties:
        status:
          type: string
        name:
          type: string
        requisite_types:
          type: array
          items:
            type: string
        is_cross_border:
          type: boolean

    CatalogMethodChange:
      type: object
      description: The AFTER state of one changed payment method plus an optional before snapshot.
      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: |
        The dedicated `payment_methods.updated` catalog broadcast wrapper
        (NOT the deal CallbackEventPayload — amount/currency/deal_id are
        meaningless for a catalog row and are absent).
      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: |
        The canonical webhook body for the merchant, sent on lifecycle
        events. Signed with the same HMAC-SHA256 scheme as incoming
        requests (the `X-Signature` header).
      required: [event, deal_id, merchant_id, tenant_id, amount, currency, status, timestamp]
      properties:
        event:
          type: string
          description: The event identifier (for example `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: The deal amount in minor currency units.
        old_amount:
          type: integer
          format: int64
          description: The deal amount BEFORE the change — present in `deal.amount_changed` only.
        amount_version:
          type: integer
          format: int64
          description: Monotonic amount-edit counter — present in `deal.amount_changed` only (a new deal starts at 1, every edit increments it).
        currency:
          type: string
          description: ISO 4217 currency code.
        status:
          $ref: '#/components/schemas/OrderStatus'
        timestamp:
          type: string
          format: date-time
          description: The RFC 3339 event timestamp.
        received_amount:
          type: integer
          format: int64
          description: The actually received amount (differs from `amount` on partial/overpaid).
        amount_match_status:
          $ref: '#/components/schemas/AmountMatchStatus'
        provider_metadata:
          type: object
          description: Provider-specific passthrough data (deeplinks, payment page URLs).
          additionalProperties: true
        fee_breakdown:
          type: object
          description: |
            Merchant-side economics of the deal — `deal.completed` (PayIn)
            only, V2 only. All amounts are int64 in USDT cents (JSON strings,
            the proto3 JSON rule). The platform's internal costs are not
            disclosed: only the gross, the merchant's commission and the
            merchant's net go out; the promo fields are present only when a
            promo is applied.
          properties:
            gross_usdt_cents:
              type: integer
              format: int64
              description: The total USDT equivalent of the payment.
            merchant_commission_usdt_cents:
              type: integer
              format: int64
              description: The merchant's commission on the deal (gross − merchant_cut), calculated from the actual payment amount.
            merchant_cut_usdt_cents:
              type: integer
              format: int64
              description: The merchant's net before the promo bonus.
            promo_bonus_usdt_cents:
              type: integer
              format: int64
              description: The bonus credited on top of the net — present only when a promo is applied.
            promo_id:
              type: string
              format: uuid
              description: The applied campaign rule identifier — present only when a promo is applied.
            promo_sponsor_type:
              type: string
              enum: [PLATFORM, MERCHANT, PROVIDER]
              description: The bonus sponsor — present only when a promo is applied.
