# =============================================================================
# B2CORE Canonical PSP Deposit API – OpenAPI 3.1.0 Specification
# =============================================================================
#
# This is the canonical (reference) API specification that any PSP must implement
# to integrate deposit operations with B2CORE.
#
# Key design principles:
#   - Explicit versioning via URL path prefix /api/v1.
#   - Uniform error envelope for all 4xx responses.
#   - 5xx responses are treated as transient – they MUST NOT carry error bodies.
#   - Client-side JWT (HS256) authentication – one-time token per request.
#   - Global parameters (globalParam1..3) forwarded with every authenticated request.
#   - Distributed tracing headers forwarded with every request.
#
# =============================================================================

openapi: 3.1.0

info:
  title: B2CORE Canonical PSP Deposit API
  version: "1.0.0"
  summary: Standard interface for PSP deposit integration with B2CORE.
  description: |
    Canonical PSP API specification for the B2CORE deposit integration.

    This specification defines the **standard interface** that any Payment System
    Provider (PSP) must implement in order to integrate deposit operations with B2CORE.

    ---

    ## Versioning

    This API uses explicit **V1 versioning** via the URL path prefix `/api/v1`.
    Future breaking changes will be introduced under `/api/v2`, etc.

    ## Content-Type

    All requests and responses **MUST** use `Content-Type: application/json`.
    PSPs **MUST** return this header in every response (including error responses).

    ## Error Handling

    ### Server Errors (5xx)

    Any 5xx HTTP response is considered a **temporary server error**.
    B2CORE treats all 5xx responses as transient and will retry according to its backoff schedule.

    ### Client Errors (4xx)

    All 4xx responses use a **unified error envelope**:
    ```json
    {
      "error": {
        "code": "machine-readable code",
        "description": "human-readable description"
      }
    }
    ```

    ### Rate Limiting (429)

    A 429 response **MUST** include the `Retry-After` header with the number of seconds
    the client should wait before retrying the request.

    ## Authentication – One-time JWT (HS256)

    All endpoints require a **one-time JWT** in the `Authorization` header:
    ```
    Authorization: Bearer <jwt>
    ```

    ### Credentials

    Two values are issued to the merchant via the PSP personal cabinet:
    - **`app_id`** – unique merchant identifier (used as the `sub` claim).
    - **`app_secret`** – secret key for HMAC-SHA256 signing. **Never transmitted in requests.**

    ### Token generation (client-side)

    The client generates a **new JWT for every request**. No token endpoint is needed.

    **Header** (fixed):
    ```json
    {"alg": "HS256", "typ": "JWT"}
    ```

    **Payload – standard claims only (RFC 7519):**

    | Claim | Type | Description |
    |-------|------|-------------|
    | `sub` | string | `app_id` of the merchant |
    | `iat` | number | Token creation time (Unix timestamp) |
    | `exp` | number | Expiration time: `iat + 30` seconds |
    | `jti` | string | Unique per request – replay protection. Any non-empty unique string (UUID v4 recommended, but not mandatory). |

    **Signature:** `HMAC-SHA256(base64url(header) + "." + base64url(payload), app_secret)`

    ### Server-side verification (recommended implementation)

    1. Decode the JWT header – verify `alg` is `HS256`.
    2. Look up `app_secret` by the `sub` (app_id) claim.
    3. Verify the HMAC-SHA256 signature. Use the `app_secret` string as-is
       for HMAC verification (do not Base64-decode it). Standard JWT libraries
       accept the string directly.
    4. Check `exp` ≥ now — reject expired tokens.
    5. Check `iat` is within ±5 seconds of server time — reject tokens from the future.
    6. *(Recommended)* Check `jti` uniqueness against a store (e.g. Redis with TTL = 30 s)
       to prevent replay attacks. If `jti` was already seen, reject with 401.
    7. On any verification failure, respond with **401 Unauthorized**.

    Any standard JWT library in any language can produce compatible tokens —
    no custom libraries are required.

    ## Global Parameters

    Requests to all authenticated endpoints include three configuration-level parameters:
    - `globalParam1`
    - `globalParam2`
    - `globalParam3`

    These are PSP-specific values configured by the B2CORE administrator and forwarded transparently with every API call.
    For POST endpoints they appear in the request body; for GET endpoints — as query parameters.

    ## Distributed Tracing

    All HTTP requests from B2CORE include standard tracing headers
    (e.g., `traceparent`, `tracestate` per W3C Trace Context) to support
    end-to-end distributed tracing across B2CORE and the PSP.

servers:
  - url: "{baseUrl}"
    description: PSP API server. The base URL is PSP-specific and configured in B2CORE credentials.
    variables:
      baseUrl:
        default: "https://psp-api.example.com"
tags:
  - name: Configuration
    description: Endpoints for validating PSP credentials and configuration.
  - name: Currency
    description: Endpoints for checking currency support and amount restrictions.
  - name: Deposit
    description: Endpoints for creating and polling deposit operations.

paths:

  /api/v1/configuration/test:
    post:
      operationId: testConfiguration
      tags: [Configuration]
      summary: Test configuration validity
      description: |
        Validates that the current credentials and configuration are correct
        and the PSP is reachable. This endpoint is **mandatory** — it is always
        called during configuration testing.

        This is a **read-only** endpoint — side-effect-free operation.

        **Response statuses (within a 2xx body):**
        - `"available"` — API is reachable, credentials are valid. Does not guarantee
          that creating a deposit will succeed.
        - `"failed"` — bad credentials or configuration. Explicitly indicates a setup error.
          Ideally, the API error makes it clear which field is filled incorrectly.

        When the status is `"failed"`, the response includes an `error` object
        **within the 2xx response body** (not as a 4xx error). `error.code` and `error.description` are each limited to **500 characters**.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                globalParam1:
                  type: string
                globalParam2:
                  type: string
                globalParam3:
                  type: string
            examples:
              default:
                value:
                  globalParam1: "example_1"
                  globalParam2: "example_2"
      responses:
        '200':
          description: |
            Configuration test completed. Check the `status` field to determine
            the result. Both `"available"` and `"failed"` are returned as 200 OK.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/TestConfigAvailable'
                  - $ref: '#/components/schemas/TestConfigFailed'
                discriminator:
                  propertyName: status
                  mapping:
                    available: '#/components/schemas/TestConfigAvailable'
                    failed: '#/components/schemas/TestConfigFailed'
              examples:
                available:
                  summary: Configuration is valid
                  value:
                    status: "available"
                failed:
                  summary: Configuration is invalid
                  value:
                    status: "failed"
                    error:
                      code: "example_code"
                      description: "App ID is not registered in the system"
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '5XX':
          $ref: '#/components/responses/ServerError'

  # ===========================================================================
  # GET /api/v1/currencies/{currencyCode}/support — Check currency support and limits
  # ===========================================================================
  /api/v1/currencies/{currencyCode}/support:
    get:
      operationId: checkCurrencySupport
      tags: [Currency]
      summary: Check currency support and amount restrictions
      description: |
        Checks whether the given currency code is supported by the PSP and
        returns any amount restrictions (minimum, maximum, decimal scale).

        **Response statuses:**
        - `"supported"` — the currency is explicitly supported. An `amountRestrictions`
          object may be present.
        - `"notSupported"` — the currency is explicitly NOT supported. Do not attempt
          to create deposits with this currency.
        - `"unknown"` — the PSP cannot confirm or deny support. A deposit may be
          attempted but success is not guaranteed.
      security:
        - bearerAuth: []
      parameters:
        - name: currencyCode
          in: path
          required: true
          description: |
            Currency code to check.
            - Fiat: uppercase ISO 4217 code (e.g., "USD", "EUR").
            - Crypto: standard symbol (e.g., "BTC", "USDT").
          schema:
            type: string
            minLength: 2
            maxLength: 10
        - name: globalParam1
          in: query
          required: false
          description: Configuration-level parameter 1. PSP-specific.
          schema:
            type: string
        - name: globalParam2
          in: query
          required: false
          description: Configuration-level parameter 2. PSP-specific.
          schema:
            type: string
        - name: globalParam3
          in: query
          required: false
          description: Configuration-level parameter 3. PSP-specific.
          schema:
            type: string
      responses:
        '200':
          description: Currency support check completed.
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    type: string
                    enum: [supported, notSupported, unknown]
                    description: |
                      Currency support status:
                      - `"supported"` — explicitly supported. Check `amountRestrictions` for limits.
                      - `"notSupported"` — explicitly NOT supported. Do not attempt deposits.
                      - `"unknown"` — cannot confirm or deny. Deposits may be attempted but
                        are not guaranteed to succeed.
                  amountRestrictions:
                    $ref: '#/components/schemas/AmountRestrictions'
                    description: |
                      Amount restrictions for this currency. Only relevant when `status` is `"supported"`.
                      Omit this field if the PSP has no restrictions configured.
                      If present, at least one field within the object must be non-null.
              examples:
                supported_with_restrictions:
                  summary: Currency supported with restrictions
                  value:
                    status: "supported"
                    amountRestrictions:
                      minAmount: "10.00"
                      maxAmount: "10000.00"
                      scale: 2
                supported_no_restrictions:
                  summary: Currency supported, no restrictions configured
                  value:
                    status: "supported"
                not_supported:
                  summary: Currency not supported
                  value:
                    status: "notSupported"
                unknown:
                  summary: Support status unknown
                  value:
                    status: "unknown"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '5XX':
          $ref: '#/components/responses/ServerError'

  # ===========================================================================
  # POST /api/v1/deposits — Start a new deposit
  # ===========================================================================
  /api/v1/deposits:
    post:
      operationId: startDeposit
      tags: [Deposit]
      summary: Start a new deposit
      description: |
        Initiates a deposit operation on the PSP side.

        **`externalID` (UUID):** Generated by B2CORE and serves three purposes:
        1. **Idempotency key** — repeated calls with the same `externalID` must
           not create duplicate deposits.
        2. **Lookup key** — used in `GET /api/v1/deposits/{externalID}` to retrieve
           the deposit status during polling.
        3. **Webhook correlation key** — included in the webhook payload to identify
           which deposit the notification belongs to.

        **`currencyCode`:** Must be a currency that previously returned `"supported"`
        or `"unknown"` from the `GET /api/v1/currencies/{currencyCode}/support` endpoint.

        **PSP payment page constraint:** If the PSP redirects the user to an external
        payment page, that page **MUST NOT** allow the end user to change the
        `currencyCode` to an equivalent or analog currency.

        **On success (201 Created):** The response body contains an `action` object
        describing the next client step (currently only `"redirect"` is supported).
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - externalID
                - currencyCode
                - amount
                - returnURL
                - notificationURL
                - userInfo
              properties:
                externalID:
                  type: string
                  format: uuid
                  description: |
                    Unique deposit identifier generated by B2CORE (UUID v4).
                    Serves as idempotency key, lookup key for polling, and webhook correlation key.
                  examples:
                    - "550e8400-e29b-41d4-a716-446655440000"
                currencyCode:
                  type: string
                  minLength: 2
                  maxLength: 10
                  description: |
                    Currency code for the deposit.
                    - Fiat: uppercase ISO 4217 code (e.g., "USD", "EUR").
                    - Crypto: standard symbol (e.g., "BTC", "USDT").
                  examples:
                    - "USD"
                amount:
                  type: string
                  pattern: '^\d+(\.\d+)?$'
                  description: |
                    Deposit amount as a decimal string. The dot (`.`) is the decimal separator.
                    Must be a positive number. Leading zeros are not allowed (except `"0.xx"`).
                  examples:
                    - "100.00"
                returnURL:
                  type: string
                  format: uri
                  pattern: '^https://'
                  description: |
                    HTTPS URL to redirect the end user back to after they complete interaction
                    with the PSP payment page. This is a B2CORE frontend URL.
                    **This is NOT a webhook URL** — it is for browser redirect only.
                    **MUST** use HTTPS scheme.
                  examples:
                    - "https://broker.example.com/deposit/return"
                notificationURL:
                  type: string
                  format: uri
                  pattern: '^https://'
                  description: |
                    HTTPS URL where the PSP should send webhook notifications when the deposit
                    reaches a terminal status (success / failed / unprocessable).
                    **MUST** use HTTPS scheme. See **Webhook Authentication** section.
                  examples:
                    - "https://b2core.example.com/webhooks/deposits/psp-name"
                userInfo:
                  $ref: '#/components/schemas/UserInfo'
                globalParam1:
                  type: string
                globalParam2:
                  type: string
                globalParam3:
                  type: string
            examples:
              default:
                value:
                  externalID: "550e8400-e29b-41d4-a716-446655440000"
                  currencyCode: "USD"
                  amount: "100.00"
                  returnURL: "https://broker.example.com/deposit/return"
                  notificationURL: "https://b2core.example.com/webhooks/deposits/psp-name"
                  userInfo:
                    locale: "en_US"
                    firstName: "John"
                    lastName: "Doe"
                    email: "john.doe@example.com"
                    dob: "1990-01-15"
                    phone: "+1234567890"
                    countryAlphaCode2: "US"
                    city: "New York"
                    address: "123 Main St"
                    zip: "10001"
                  globalParam1: "example_1"
                  globalParam2: "example_2"
      responses:
        '201':
          description: |
            Deposit created successfully. The response contains an `action` object
            describing the next step for the end user.
          content:
            application/json:
              schema:
                type: object
                required:
                  - action
                properties:
                  action:
                    $ref: '#/components/schemas/DepositAction'
              examples:
                default:
                  value:
                    action:
                      type: "redirect"
                      redirect:
                        url: "https://psp.example.com/pay/abc123"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '5XX':
          $ref: '#/components/responses/ServerError'

      callbacks:
        depositStatusChanged:
          '{$request.body#/notificationURL}':
            post:
              operationId: notifyDepositStatusChange
              summary: Webhook — deposit reached a terminal status
              description: |
                The PSP **MUST** send this webhook to the `notificationURL` provided
                in the start deposit request when the deposit transitions to a
                **terminal status**: `success`, `failed`, or `unprocessable`.

                **Authentication:** Absent. Webhook authentication is not implemented.

                **Content-Type:** `application/json`

                **Delivery requirements:**
                - B2CORE responds with `200 OK` (empty body) to acknowledge receipt.
                - If B2CORE returns a non-2xx response, the PSP **SHOULD** retry
                  with exponential backoff (recommended: up to 5 retries over 24 hours).
                - The webhook payload must be JSON.

                **Important:** Webhooks are used only as an **optimization** to reduce
                the number of polling requests. `GET /api/v1/deposits/{externalID}`
                remains the authoritative source of truth for deposit status.
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      type: object
                      required:
                        - externalID
                        - status
                      properties:
                        externalID:
                          type: string
                          format: uuid
                          description: |
                            The deposit's external ID (UUID), as originally provided in the
                            `POST /api/v1/deposits` request.
                        status:
                          type: string
                          enum: [success, failed, unprocessable]
                          description: |
                            The terminal status of the deposit:
                            - `"success"` — the broker received the client's money.
                            - `"failed"` — the client did not lose any money; the broker did not
                              receive the client's money.
                            - `"unprocessable"` — the deposit reached a terminal state that is neither
                              success nor failed, requiring admin investigation (e.g., voided, refunded).
                    examples:
                      default:
                        value:
                          externalID: "550e8400-e29b-41d4-a716-446655440000"
                          status: "success"
              responses:
                '200':
                  description: Webhook received and acknowledged. Response body is empty.
                '400':
                  description: Bad Request — the webhook payload is malformed or missing. Response body is empty.
                '500':
                  description: |
                    B2CORE internal error. The PSP **SHOULD** retry the webhook
                    delivery with exponential backoff.

  # ===========================================================================
  # GET /api/v1/deposits/{externalID} — Poll deposit status
  # ===========================================================================
  /api/v1/deposits/{externalID}:
    get:
      operationId: getDeposit
      tags: [Deposit]
      summary: Get deposit status
      description: |
        Retrieves the current status of a deposit by its `externalID`.

        This endpoint is called repeatedly by B2CORE's poller to track deposit
        progress until a terminal status is reached.

        **404 Not Found with safe-to-fail header:**
        Some PSPs create the deposit on their side only AFTER the end user completes
        the PSP payment page. In this case, polling before the user completes the page
        will return 404. The PSP **SHOULD** include the `X-Safe-To-Fail-After-Seconds`
        header indicating the number of seconds after the redirect time when B2CORE
        can safely mark the deposit as failed with reason "redirect link expired".
        B2CORE adds a safety margin to this value.
      security:
        - bearerAuth: []
      parameters:
        - name: externalID
          in: path
          required: true
          description: |
            The deposit's external ID (UUID v4), as provided in the
            `POST /api/v1/deposits` request.
          schema:
            type: string
            format: uuid
        - name: globalParam1
          in: query
          required: false
          description: Configuration-level parameter 1. PSP-specific.
          schema:
            type: string
        - name: globalParam2
          in: query
          required: false
          description: Configuration-level parameter 2. PSP-specific.
          schema:
            type: string
        - name: globalParam3
          in: query
          required: false
          description: Configuration-level parameter 3. PSP-specific.
          schema:
            type: string
      responses:
        '200':
          description: Deposit status retrieved successfully.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DepositStatusInProgress'
                  - $ref: '#/components/schemas/DepositStatusSuccess'
                  - $ref: '#/components/schemas/DepositStatusFailed'
                  - $ref: '#/components/schemas/DepositStatusUnprocessable'
                discriminator:
                  propertyName: status
                  mapping:
                    inProgress: '#/components/schemas/DepositStatusInProgress'
                    success: '#/components/schemas/DepositStatusSuccess'
                    failed: '#/components/schemas/DepositStatusFailed'
                    unprocessable: '#/components/schemas/DepositStatusUnprocessable'
              examples:
                inProgress:
                  summary: Deposit is still being processed
                  value:
                    status: "inProgress"
                success:
                  summary: Deposit completed successfully
                  value:
                    status: "success"
                    finalAmount: "99.50"
                    finalCurrencyCode: "USD"
                    externalDetails:
                      - name: "psp_transaction_id"
                        label: "PSP Transaction ID"
                        format: "text"
                        value: "TXN-2026-04-01-ABC123"
                failed:
                  summary: Deposit failed
                  value:
                    status: "failed"
                    reason: "The card was declined by the issuing bank"
                unprocessable:
                  summary: Deposit in unprocessable state
                  value:
                    status: "unprocessable"
                    reason: "The deposit was voided by the PSP after a compliance review"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Deposit not found on the PSP side. This may occur when the deposit
            is only created after the end user completes the PSP payment page.

            If the `X-Safe-To-Fail-After-Seconds` header is present, B2CORE will
            wait that many seconds (plus a safety margin) from the redirect time
            before marking the deposit as failed with reason "redirect link expired".
          headers:
            X-Safe-To-Fail-After-Seconds:
              required: false
              description: |
                Number of seconds from the original redirect time after which
                B2CORE may safely mark the deposit as failed if it still returns 404.
              schema:
                type: integer
                minimum: 0
                examples:
                  - 900
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                default:
                  value:
                    error:
                      code: "example_code"
                      description: "No deposit found with the given externalID"
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '5XX':
          $ref: '#/components/responses/ServerError'

# =============================================================================
# Components
# =============================================================================
components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        One-time JWT (HS256) generated client-side for each request.
        Include it in every request as:
        ```
        Authorization: Bearer <jwt>
        ```
        See the **Authentication** section in the API description for
        claim structure, signing details, and server-side verification steps.

  # ---------------------------------------------------------------------------
  # Reusable response definitions
  # ---------------------------------------------------------------------------
  responses:
    BadRequest:
      description: Bad Request — one or more input parameters are invalid or missing.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            default:
              value:
                error:
                  code: "example_code"
                  description: "Field 'currencyCode' is required"

    Unauthorized:
      description: Unauthorized — the Bearer token is missing, expired, or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            default:
              value:
                error:
                  code: "example_code"
                  description: "The provided Bearer token has expired"

    Forbidden:
      description: |
        Forbidden — the Bearer token is valid but the authenticated entity
        does not have permission to perform this operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            default:
              value:
                error:
                  code: "example_code"
                  description: "This App ID does not have permission to perform deposit operations"

    TooManyRequests:
      description: Too Many Requests — rate limit exceeded.
      headers:
        Retry-After:
          required: true
          description: Number of seconds to wait before retrying the request.
          schema:
            type: integer
            minimum: 1
            examples:
              - 30
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            default:
              value:
                error:
                  code: "rate_limit_exceeded"
                  description: "Too many requests. Please retry after 30 seconds"

    ServerError:
      description: |
        Server Error (5xx) — a temporary server-side error occurred.
        5xx responses **MUST NOT** carry a response body.
        B2CORE treats all 5xx responses as transient and will retry
        the request according to its backoff schedule.
        No `content` is defined for this response intentionally.

  # ---------------------------------------------------------------------------
  # Reusable schemas
  # ---------------------------------------------------------------------------
  schemas:

    # ── Error types ─────────────────────────────────────────────────────────

    Error:
      type: object
      description: |
        Machine-readable error information.
        Used both in HTTP 4xx responses (wrapped in ErrorEnvelope) and in
        business-level error responses embedded within 2xx bodies.
      required:
        - code
        - description
      properties:
        code:
          type: string
          minLength: 1
          maxLength: 500
          description: Machine-readable error code for automated processing and metrics.
        description:
          type: string
          minLength: 1
          maxLength: 500
          description: Human-readable error description providing additional context.

    ErrorEnvelope:
      type: object
      description: Standard error response envelope for all 4xx HTTP responses.
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/Error'

    # ── Test Configuration response variants ────────────────────────────────

    TestConfigAvailable:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: ["available"]
          description: API is reachable, credentials and configuration are valid.

    TestConfigFailed:
      type: object
      required:
        - status
        - error
      properties:
        status:
          type: string
          enum: ["failed"]
          description: Bad credentials or configuration.
        error:
          $ref: '#/components/schemas/Error'

    # ── Amount Restrictions ─────────────────────────────────────────────────

    AmountRestrictions:
      type: object
      description: |
        Amount restrictions for a currency.

        **Validation rules:**
        1. `minAmount` must be > 0 if provided.
        2. `maxAmount` must be > 0 if provided.
        3. If both provided: `minAmount` ≤ `maxAmount`.
        4. `scale` must be ≥ 0 if provided.
        5. `minAmount` / `maxAmount` must not have more decimal places than `scale` allows.
      properties:
        minAmount:
          type: string
          minLength: 1
          pattern: '^\d+(\.\d+)?$'
          description: Minimum allowed deposit amount as a decimal string with dot separator.
          examples:
            - "10.00"
        maxAmount:
          type: string
          minLength: 1
          pattern: '^\d+(\.\d+)?$'
          description: Maximum allowed deposit amount as a decimal string with dot separator.
          examples:
            - "10000.00"
        scale:
          type: integer
          minimum: 0
          description: |
            Maximum number of decimal places allowed.
            `0` — integer amounts only. `2` — typical fiat. `8` — typical crypto.
          examples:
            - 2

    # ── User Info ───────────────────────────────────────────────────────────

    UserInfo:
      type: object
      description: |
        End user's personal information, pre-filled from the client's B2CORE profile.
        Only `email` is guaranteed to always be present. Other fields may be empty strings
        if not configured as mandatory in the B2CORE driver configuration
        ("Required fields (read-only)" or "Required fields (editable)").
        The `locale` field is not governed by these settings — it is present only
        if the user has selected a preferred locale.
      required:
        - email
      properties:
        locale:
          type: string
          description: "User's preferred locale (e.g., `\"en_US\"`, `\"ru_RU\"`)."
        firstName:
          type: string
          description: User's first (given) name.
        lastName:
          type: string
          description: User's last (family) name.
        email:
          type: string
          format: email
          minLength: 1
          description: "User's email address. **Always present.**"
        dob:
          type: string
          format: date
          pattern: '^\d{4}-\d{2}-\d{2}$'
          description: "User's date of birth in `YYYY-MM-DD` format."
          examples:
            - "1990-01-15"
        phone:
          type: string
          pattern: '^\+[1-9]\d{1,14}$'
          description: |
            User's phone number in E.164 format: `+[country code][number]`.
            Always starts with `+`, followed by 1–15 digits. First digit after `+` is non-zero.
          examples:
            - "+14155552671"
        countryAlphaCode2:
          type: string
          minLength: 2
          maxLength: 2
          pattern: '^[A-Z]{2}$'
          description: "User's country (ISO 3166-1 alpha-2, e.g., `\"US\"`)."
        city:
          type: string
          description: User's city.
        address:
          type: string
          description: User's street address.
        zip:
          type: string
          description: User's postal / ZIP code.

    # ── Deposit Start response variants ─────────────────────────────────────

    DepositAction:
      type: object
      description: |
        Describes the action the end user should take after a successful deposit start.

        Currently only the `"redirect"` action type is supported. Future versions
        may add new types. Clients that encounter an unknown `action.type` should
        treat it as an error and surface it to the user.
      required:
        - type
        - redirect
      properties:
        type:
          type: string
          enum: [redirect]
          description: |
            Action type. Currently only `"redirect"` is supported.
            See **Forward Compatibility** section for handling unknown types.
        redirect:
          type: object
          required:
            - url
          properties:
            url:
              type: string
              format: uri
              pattern: '^https://'
              description: HTTPS URL of the PSP payment page.
              examples:
                - "https://psp.example.com/pay/abc123"

    # ── Deposit Poll response variants ──────────────────────────────────────

    ExternalDetail:
      type: object
      description: A single PSP-specific detail about the deposit (read-only).
      required:
        - name
        - label
        - format
        - value
      properties:
        name:
          type: string
          minLength: 1
          description: Machine-readable identifier for this detail.
        label:
          type: string
          minLength: 1
          description: Human-readable label displayed in the admin UI.
        format:
          type: string
          enum: [text]
          description: Display format. Currently only `"text"` is supported.
        value:
          type: string
          description: The detail's value. May be empty (filtered out by B2CORE).

    ExternalDetailsArray:
      description: |
        Optional array of PSP-specific details, only expected for terminal statuses
        (`success`, `failed`, `unprocessable`). Provides read-only information
        for admin reference — NOT used in B2CORE business logic.
      oneOf:
        - type: "array"
          items:
            $ref: '#/components/schemas/ExternalDetail'
        - type: "null"

    DepositStatusInProgress:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: ["inProgress"]
          description: Polling continues. Any intermediate status.

    DepositStatusSuccess:
      type: object
      required:
        - status
        - finalAmount
        - finalCurrencyCode
      properties:
        status:
          type: string
          enum: ["success"]
          description: The broker received the client's money.
        finalAmount:
          type: string
          pattern: '^\d+(\.\d+)?$'
          description: |
            The actual amount received by the broker, as reported by the PSP.
            Decimal string with dot (`.`) separator.
            May differ from the initial `amount` due to PSP fees or currency conversion.
          examples:
            - "99.50"
        finalCurrencyCode:
          type: string
          minLength: 2
          maxLength: 10
          description: |
            The actual currency in which the broker received the funds.
            May differ from the initial `currencyCode` if the PSP performed conversion.
          examples:
            - "USD"
        externalDetails:
          $ref: '#/components/schemas/ExternalDetailsArray'

    DepositStatusFailed:
      type: object
      required:
        - status
        - reason
      properties:
        status:
          type: string
          enum: ["failed"]
          description: |
            The client did not lose any money; the broker did not receive the client's money.
        reason:
          type: string
          minLength: 1
          maxLength: 500
          description: Human-readable reason why the deposit failed.
        externalDetails:
          $ref: '#/components/schemas/ExternalDetailsArray'

    DepositStatusUnprocessable:
      type: object
      required:
        - status
        - reason
      properties:
        status:
          type: string
          enum: ["unprocessable"]
          description: |
            The deposit reached a terminal state that is neither success nor failed,
            requiring admin investigation (e.g., voided, refunded).
        reason:
          type: string
          minLength: 1
          maxLength: 500
          description: Human-readable reason describing the unprocessable state.
        externalDetails:
          $ref: '#/components/schemas/ExternalDetailsArray'
