openapi: 3.1.0
info:
  title: Lumo B2B Platform API
  version: 1.0.0
  description: |
    API reference for **company-facing** endpoints of Lumo B2B platform.

    Scope of this reference:
    - Company auth/profile
    - Orders (prepare/accept/list/get)
    - Company disputes
    - Company wallet + deposits
    - User wallets (per externalId) + user deposits

    Not included:
    - Admin endpoints
servers:
  - url: https://b2b.lumowallet.io
    description: Production
  - url: https://b2b-staging.lumowallet.io
    description: Staging
tags:
  - name: Company Auth
    description: Company registration, login, and profile management.
  - name: Orders
    description: Quote preparation and order lifecycle operations.
  - name: Disputes
    description: Company dispute creation and listing.
  - name: Company Wallet
    description: Company deposit wallet, balances, and deposit history.
  - name: User Wallets
    description: Per-user wallet management and user deposit history.
paths:
  /auth/company/login:
    post:
      security: []
      tags:
        - Company Auth
      summary: Company login
      description: |
        Authenticates company by email/password and rotates API key.
        Previous key becomes invalid after successful login.
      operationId: loginCompany
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '201':
          description: Logged in, fresh API key issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyAuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/company/profile:
    get:
      tags:
        - Company Auth
      summary: Get company profile
      operationId: getCompanyProfile
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Company profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags:
        - Company Auth
      summary: Update company profile
      operationId: updateCompanyProfile
      security:
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCompanyProfileRequest'
      responses:
        '200':
          description: Updated company profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/company/regenerate-key:
    post:
      tags:
        - Company Auth
      summary: Regenerate company API key
      description: Rotates and returns a new API key for current company.
      operationId: regenerateCompanyApiKey
      security:
        - apiKeyAuth: []
      responses:
        '201':
          description: New API key issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyAuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /orders/prepare:
    post:
      tags:
        - Orders
      summary: Prepare order (create quote)
      description: |
        Creates quote from NSPK QR code.
        Quote TTL is short-lived (30 seconds in current implementation).
      operationId: prepareOrder
      security:
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PrepareOrderRequest'
      responses:
        '201':
          description: Quote prepared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PrepareOrderResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /orders/accept/{id}:
    post:
      tags:
        - Orders
      summary: Accept quote and create order
      description: >
        Converts quote into order and reserves funds (`frozenBalance`).


        Idempotency:

        - Optional header `Idempotency-Key`.

        - Repeated request with same key for same company returns stored
        response.
      operationId: acceptOrder
      security:
        - apiKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Quote ID
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            maxLength: 255
      responses:
        '201':
          description: Order accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /orders:
    get:
      tags:
        - Orders
      summary: List company orders
      operationId: listOrders
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: status
          required: false
          schema:
            $ref: '#/components/schemas/OrderStatus'
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 200
        - in: query
          name: offset
          required: false
          schema:
            type: integer
            default: 0
            minimum: 0
      responses:
        '200':
          description: Orders list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrdersListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /orders/{id}:
    get:
      tags:
        - Orders
      summary: Get order by id
      operationId: getOrder
      security:
        - apiKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /orders/{id}/disputes:
    post:
      tags:
        - Disputes
      summary: Open dispute for order
      description: >
        Allowed only for completed order states (`success` or `failed`).


        Behavior:

        - On create, order is moved to `disputed`.

        - If an open dispute already exists for the order (`submitted` /
        `in_review`), API returns that dispute.

        - Dispute is also sent to wallet-backend for synchronized processing.
      operationId: createDispute
      security:
        - apiKeyAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Order ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDisputeRequest'
      responses:
        '201':
          description: Dispute created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Dispute'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /disputes:
    get:
      tags:
        - Disputes
      summary: List company disputes
      description: >
        Returns company dispute history (newest first).


        Resolution fields:

        - `resolvedAt` is set when dispute is resolved.

        - `resolution` is set on resolve (`confirm`, `refund`,
        `partial_refund`).

        - `refundAmount` is set only for `partial_refund`; for
        `confirm`/`refund` it is `null`.


        Disputes can be resolved by admin flow or automatically from wallet
        callback final status.
      operationId: listDisputes
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Disputes list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Dispute'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /wallets:
    get:
      tags:
        - Company Wallet
      summary: Get company deposit wallet
      description: |
        Returns company TRON wallet address for incoming USDT (TRC-20).
        Wallet is auto-created on first request if absent.
      operationId: getCompanyWallet
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Company wallet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyWallet'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /wallets/balance:
    get:
      tags:
        - Company Wallet
      summary: Get wallet balance view
      description: |
        Returns company balance view.
        `usdtBalance` reflects company balance used by platform accounting.
      operationId: getCompanyWalletBalance
      security:
        - apiKeyAuth: []
      responses:
        '200':
          description: Balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyWalletBalance'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /wallets/deposits:
    get:
      tags:
        - Company Wallet
      summary: List company deposits
      description: >
        Deposit status lifecycle:

        - `new`

        - `fee_charged`

        - `aml_pending` / `aml_passed` / `aml_failed`

        - `forwarding`

        - `completed` / `failed`


        Company deposit sweep target is `B2B_TREASURY_ADDRESS`.

        Processing fee is retained from incoming deposit amount (not debited
        from company balance).
      operationId: listCompanyDeposits
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: status
          required: false
          schema:
            $ref: '#/components/schemas/CompanyDepositStatus'
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 200
        - in: query
          name: offset
          required: false
          schema:
            type: integer
            default: 0
            minimum: 0
      responses:
        '200':
          description: Deposits list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyDepositsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /user-wallets:
    post:
      tags:
        - User Wallets
      summary: Create user wallet
      description: Creates or returns wallet for given `externalId` within company scope.
      operationId: createUserWallet
      security:
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserWalletRequest'
      responses:
        '201':
          description: User wallet created or returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserWallet'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags:
        - User Wallets
      summary: List user wallets
      operationId: listUserWallets
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: externalId
          required: false
          schema:
            type: string
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - in: query
          name: offset
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Wallets list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserWalletsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /user-wallets/blocked:
    get:
      tags:
        - User Wallets
      summary: List blocked user wallets
      operationId: listBlockedUserWallets
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: externalId
          required: false
          schema:
            type: string
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - in: query
          name: offset
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: Blocked wallets list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserWalletsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /user-wallets/{walletId}/block:
    post:
      tags:
        - User Wallets
      summary: Block user wallet
      operationId: blockUserWallet
      security:
        - apiKeyAuth: []
      parameters:
        - in: path
          name: walletId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlockWalletRequest'
      responses:
        '200':
          description: Wallet blocked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserWallet'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /user-wallets/{walletId}/replace:
    post:
      tags:
        - User Wallets
      summary: Replace user wallet
      description: Blocks old wallet and creates a new one for the same external user.
      operationId: replaceUserWallet
      security:
        - apiKeyAuth: []
      parameters:
        - in: path
          name: walletId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReplaceWalletRequest'
      responses:
        '200':
          description: New wallet created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserWallet'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /user-wallets/deposits:
    get:
      tags:
        - User Wallets
      summary: List user-wallet deposits
      operationId: listUserWalletDeposits
      security:
        - apiKeyAuth: []
      parameters:
        - in: query
          name: externalId
          required: false
          schema:
            type: string
        - in: query
          name: status
          required: false
          schema:
            $ref: '#/components/schemas/UserDepositStatus'
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
        - in: query
          name: offset
          required: false
          schema:
            type: integer
            minimum: 0
      responses:
        '200':
          description: User deposits list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserDepositsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >
        Company API key.

        Service also accepts `Authorization: Bearer <api-key>` and `api_key`
        query param,

        but `X-API-Key` is recommended.
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing/invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Conflict (already exists)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    ErrorResponse:
      type: object
      properties:
        statusCode:
          type: integer
          example: 400
        message:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        error:
          type: string
          example: Bad Request
    CreateCompanyAccountRequest:
      type: object
      required:
        - companyName
        - email
        - password
      properties:
        companyName:
          type: string
          example: Acme LLC
        email:
          type: string
          format: email
          example: owner@acme.com
        password:
          type: string
          minLength: 8
          example: supersecret123
        contactTelegram:
          type: string
          example: '@acme_support'
        webhookUrl:
          type: string
          example: https://merchant.example.com/webhooks/lumo
        depositForwardAddress:
          type: string
          example: TQ2x4...
    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
        password:
          type: string
    CompanyInfo:
      type: object
      required:
        - id
        - name
        - email
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
    CompanyAuthResponse:
      type: object
      required:
        - company
        - apiKey
      properties:
        company:
          $ref: '#/components/schemas/CompanyInfo'
        apiKey:
          type: string
          description: Save securely; used for company API auth.
          example: lb2b_5f7f1f7b7f034dbf81b0b8b8d7c0b9f1
    CompanyProfile:
      type: object
      required:
        - id
        - name
        - email
        - isActive
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        contactEmail:
          type:
            - string
            - 'null'
          format: email
        contactTelegram:
          type:
            - string
            - 'null'
        isActive:
          type: boolean
        webhookUrl:
          type:
            - string
            - 'null'
        depositForwardAddress:
          type:
            - string
            - 'null'
          description: >-
            TRON address where captured deposits are forwarded after fee is
            charged
    UpdateCompanyProfileRequest:
      type: object
      properties:
        webhookUrl:
          type: string
          format: uri
          example: https://merchant.example.com/webhooks/lumo
        depositForwardAddress:
          type: string
          description: TRON address where captured deposits are forwarded
          example: TQ2x4...
    PrepareOrderRequest:
      type: object
      required:
        - qrCode
      properties:
        qrCode:
          type: string
          maxLength: 4000
          description: NSPK QR payload or URL
        externalOrderId:
          type: string
          maxLength: 255
    PrepareOrderResponse:
      type: object
      required:
        - id
        - amountRub
        - amountUsdt
        - rate
        - expiresAt
        - isBalanceSufficient
      properties:
        id:
          type: string
          format: uuid
        amountRub:
          type: number
        amountUsdt:
          type: number
        rate:
          type: number
          description: RUB/USDT rate used for conversion
        expiresAt:
          type: string
          format: date-time
        isBalanceSufficient:
          type: boolean
        merchantName:
          type:
            - string
            - 'null'
    OrderStatus:
      type: string
      enum:
        - created
        - in_progress
        - success
        - failed
        - expired_no_taker
        - expired_by_system
        - disputed
    Order:
      type: object
      required:
        - id
        - amountUsdt
        - amountRub
        - status
        - paymentUntil
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        amountUsdt:
          type: number
        amountRub:
          type: number
        status:
          $ref: '#/components/schemas/OrderStatus'
        paymentUntil:
          type: string
          format: date-time
        merchantName:
          type:
            - string
            - 'null'
        externalOrderId:
          type:
            - string
            - 'null'
        completedAt:
          type:
            - string
            - 'null'
          format: date-time
        failureReason:
          type:
            - string
            - 'null'
        createdAt:
          type: string
          format: date-time
    OrdersListResponse:
      type: object
      required:
        - orders
        - total
      properties:
        orders:
          type: array
          items:
            $ref: '#/components/schemas/Order'
        total:
          type: integer
    DisputeStatus:
      type: string
      enum:
        - submitted
        - in_review
        - resolved
        - rejected
    DisputeResolution:
      type: string
      enum:
        - refund
        - confirm
        - partial_refund
    CreateDisputeRequest:
      type: object
      required:
        - reason
      properties:
        reason:
          type: string
    Dispute:
      type: object
      required:
        - id
        - orderId
        - status
        - reason
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        orderId:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/DisputeStatus'
        resolution:
          description: Set when dispute is resolved.
          oneOf:
            - $ref: '#/components/schemas/DisputeResolution'
            - type: 'null'
        reason:
          type: string
        adminComment:
          type:
            - string
            - 'null'
        resolvedBy:
          type:
            - string
            - 'null'
        refundAmount:
          type:
            - number
            - 'null'
          description: Filled for `partial_refund`; `null` for `confirm` and full `refund`.
        resolvedAt:
          type:
            - string
            - 'null'
          format: date-time
          description: Resolution timestamp (admin or wallet callback sync).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CompanyWallet:
      type: object
      required:
        - id
        - address
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        address:
          type: string
          description: TRON address for USDT TRC-20 deposits
        createdAt:
          type: string
          format: date-time
    CompanyWalletBalance:
      type: object
      required:
        - address
        - usdtBalance
        - trxBalance
      properties:
        address:
          type: string
        usdtBalance:
          type: number
        trxBalance:
          type: number
    CompanyDepositStatus:
      type: string
      enum:
        - new
        - on_hold_insufficient_balance
        - fee_charged
        - aml_pending
        - aml_passed
        - aml_failed
        - forwarding
        - sweeping
        - completed
        - failed
    CompanyDeposit:
      type: object
      required:
        - id
        - txHash
        - amount
        - status
        - blockTimestamp
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        txHash:
          type: string
        amount:
          type: number
        status:
          $ref: '#/components/schemas/CompanyDepositStatus'
        fromAddress:
          type:
            - string
            - 'null'
        sweepTxHash:
          type:
            - string
            - 'null'
        error:
          type:
            - string
            - 'null'
        blockTimestamp:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
    CompanyDepositsListResponse:
      type: object
      required:
        - deposits
        - total
      properties:
        deposits:
          type: array
          items:
            $ref: '#/components/schemas/CompanyDeposit'
        total:
          type: integer
    CreateUserWalletRequest:
      type: object
      required:
        - externalId
      properties:
        externalId:
          type: string
          description: User ID in merchant system
    UserWallet:
      type: object
      required:
        - id
        - companyId
        - externalId
        - address
        - isBlocked
        - lastUsdtBalance
        - lastTrxBalance
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        companyId:
          type: string
          format: uuid
        externalId:
          type: string
        address:
          type: string
        isBlocked:
          type: boolean
        blockReason:
          type:
            - string
            - 'null'
        lastUsdtBalance:
          type: number
        lastTrxBalance:
          type: number
        createdAt:
          type: string
          format: date-time
    UserWalletsListResponse:
      type: object
      required:
        - wallets
        - total
      properties:
        wallets:
          type: array
          items:
            $ref: '#/components/schemas/UserWallet'
        total:
          type: integer
    BlockWalletRequest:
      type: object
      required:
        - reason
      properties:
        reason:
          type: string
    ReplaceWalletRequest:
      type: object
      properties:
        reason:
          type: string
    UserDepositStatus:
      type: string
      description: |
        Wallet-backend statuses may include legacy and normalized values.
      enum:
        - new
        - aml_pending
        - aml_pass
        - aml_fail
        - aml_passed
        - aml_failed
        - sweeping
        - completed
        - failed
    UserDeposit:
      type: object
      required:
        - id
        - companyId
        - walletId
        - externalId
        - walletAddress
        - txHash
        - amount
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        companyId:
          type: string
          format: uuid
        walletId:
          type: string
          format: uuid
        externalId:
          type: string
        walletAddress:
          type: string
        txHash:
          type: string
        fromAddress:
          type:
            - string
            - 'null'
        amount:
          type: number
        status:
          $ref: '#/components/schemas/UserDepositStatus'
        amlCheckId:
          type:
            - string
            - 'null'
        amlRiskIndicator:
          type:
            - string
            - 'null'
        sweepTxHash:
          type:
            - string
            - 'null'
        error:
          type:
            - string
            - 'null'
        blockTimestamp:
          type:
            - string
            - 'null'
          format: date-time
        createdAt:
          type: string
          format: date-time
    UserDepositsListResponse:
      type: object
      required:
        - deposits
        - total
      properties:
        deposits:
          type: array
          items:
            $ref: '#/components/schemas/UserDeposit'
        total:
          type: integer
