openapi: 3.1.0
info:
  title: Ayamaz VTU Backend API
  version: "1.0.0"
  description: >
    REST API for the Ayamaz VTU platform — customer wallet/ledger, airtime
    & data purchase, payment gateway virtual accounts, and the staff admin
    portal (governance, reconciliation, observability). Generated from the
    route table in `lib/src/shared/transport/http/router.dart` and the
    controller implementations behind it (Phase 14 deliverable — see
    `doc/dev_phases/14_PHASE_PRODUCTION_READINESS.md`).
  contact:
    name: Ayamaz Engineering
  license:
    name: Proprietary
externalDocs:
  description: Development phase documents
  url: https://github.com/ayamaz/backend/tree/main/doc/dev_phases

servers:
  - url: /api/v1
    description: >
      Relative to whichever environment's base URL is deploying this spec
      (production/staging Cloud Run URL, or http://localhost:8080 locally)
      — no production domain is provisioned yet, see Phase 14.

tags:
  - name: Auth
    description: Customer authentication (register, login, Google Sign-In, refresh, logout)
  - name: Admin Auth
    description: Staff authentication and Step-Up MFA exchange
  - name: Profile & KYC
    description: Customer self-service profile, PIN, and KYC submission
  - name: Notifications
    description: Device token registration, notification preferences, and push/email delivery workers
  - name: Wallet & Ledger
    description: Customer wallet balances and transaction history
  - name: Catalog & Pricing
    description: Public VTU service/product catalog and tier pricing quotes
  - name: VTU
    description: Airtime and data purchase, order status
  - name: Webhooks
    description: Inbound provider/gateway webhook ingress (BillStack, VTU providers) — signature or shared-secret authenticated, not JWT
  - name: Internal
    description: Cloud Tasks / Pub/Sub / Cloud Scheduler push endpoints — OIDC-authenticated, not for direct client use
  - name: Admin Users
    description: Staff customer administration — search, freeze, KYC review
  - name: Admin Wallets
    description: Staff wallet inspection and Maker-Checker manual adjustments
  - name: Admin Payments
    description: Staff payment gateway administration and webhook replay
  - name: Admin Catalog
    description: Staff catalog/product management and Maker-Checker pricing changes
  - name: Admin Providers
    description: Staff VTU provider float and routing administration
  - name: Admin Orders
    description: Staff VTU order search, requery, and manual resolution
  - name: Admin Governance
    description: Maker-Checker approvals inbox, staff account management, audit trail
  - name: Admin Reconciliation
    description: 3-way reconciliation batch review and discrepancy write-off
  - name: Admin Feature Flags
    description: Platform feature flag administration (Phase 14)
  - name: Observability
    description: Health/readiness probes, Prometheus metrics, partition management job

security:
  - bearerAuth: []

paths:
  # ---------------------------------------------------------------------
  # Health / Observability (unauthenticated / job endpoints)
  # ---------------------------------------------------------------------
  /health:
    get:
      tags: [Observability]
      summary: Liveness probe
      security: []
      servers:
        - url: /
          description: Root-level, not under /api/v1
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /ready:
    get:
      tags: [Observability]
      summary: Deep readiness probe (DB, Redis, partition health)
      security: []
      servers:
        - url: /
          description: Root-level, not under /api/v1
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "503": { $ref: "#/components/responses/Envelope" }
  /metrics:
    get:
      tags: [Observability]
      summary: Prometheus exposition-format metrics
      security: []
      responses:
        "200":
          description: Prometheus text exposition format (not an Envelope response)
          content:
            text/plain: { schema: { type: string } }
  /jobs/create-partitions:
    get:
      tags: [Observability]
      summary: Create forward-looking monthly range partitions (Cloud Scheduler)
      description: >
        `months_ahead` (query, default 3) controls the forward window. Pass
        `months_ahead=6` for the one-time Phase 14 pre-launch run.
      security: []
      parameters:
        - name: months_ahead
          in: query
          schema: { type: integer, default: 3 }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
    post:
      tags: [Observability]
      summary: Create forward-looking monthly range partitions (Cloud Scheduler)
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                months_ahead: { type: integer, default: 3 }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /jobs/reconcile:
    post:
      tags: [Observability]
      summary: Run the nightly 3-way reconciliation batch (Cloud Scheduler)
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Customer Auth
  # ---------------------------------------------------------------------
  /auth/register:
    post:
      tags: [Auth]
      summary: Register a new customer account
      description: Gated by the CUSTOMER_REGISTRATION_ENABLED feature flag (403 when disabled).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number, first_name, last_name, password, device_id]
              properties:
                phone_number: { type: string, example: "+2348012345678" }
                email: { type: string, format: email }
                first_name: { type: string }
                last_name: { type: string }
                password: { type: string, format: password }
                device_id: { type: string }
      responses:
        "201": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
  /auth/login:
    post:
      tags: [Auth]
      summary: Log in with phone number and password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number, password]
              properties:
                phone_number: { type: string }
                password: { type: string, format: password }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }
  /auth/google:
    post:
      tags: [Auth]
      summary: Sign in with a verified Google ID token (existing account only)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_token, device_id]
              properties:
                id_token: { type: string }
                device_id: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
  /auth/refresh:
    post:
      tags: [Auth]
      summary: Exchange a refresh token for a new access token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }
  /auth/logout:
    post:
      tags: [Auth]
      summary: Revoke the current access/refresh token pair
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /auth/google/link:
    post:
      tags: [Auth]
      summary: Link a verified Google identity to the authenticated account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_token]
              properties:
                id_token: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "409": { $ref: "#/components/responses/Error" }

  /admin/auth/login:
    post:
      tags: [Admin Auth]
      summary: Staff login (email + password). TOTP is not checked at login — it's enforced at Step-Up for sensitive mutations.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, device_id]
              properties:
                email: { type: string }
                password: { type: string, format: password }
                device_id: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }
  /admin/auth/step-up:
    post:
      tags: [Admin Auth]
      summary: Exchange a valid TOTP code for a single-use Step-Up token
      description: Step-Up tokens gate every CRITICAL_MAKER_CHECKER-risk admin mutation.
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [totp_code]
              properties:
                totp_code: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }

  # ---------------------------------------------------------------------
  # Profile / KYC / Notifications
  # ---------------------------------------------------------------------
  /users/me:
    get:
      tags: [Profile & KYC]
      summary: Get the authenticated customer's full profile
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /users/me/profile:
    put:
      tags: [Profile & KYC]
      summary: Update profile name fields
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                first_name: { type: string }
                last_name: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /users/me/pin:
    post:
      tags: [Profile & KYC]
      summary: Set the transaction PIN (first-time only)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [pin]
              properties: { pin: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "409": { $ref: "#/components/responses/Error" }
    put:
      tags: [Profile & KYC]
      summary: Change the transaction PIN (requires the current PIN)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [current_pin, new_pin]
              properties:
                current_pin: { type: string }
                new_pin: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }
  /users/me/kyc/bvn-nin:
    post:
      tags: [Profile & KYC]
      summary: Submit BVN/NIN for Tier 1 KYC verification
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                bvn: { type: string }
                nin: { type: string }
      responses:
        "202": { $ref: "#/components/responses/Envelope" }
  /users/me/kyc/documents:
    post:
      tags: [Profile & KYC]
      summary: Submit identity documents for Tier 2 KYC verification
      responses:
        "202": { $ref: "#/components/responses/Envelope" }
  /users/me/devices:
    post:
      tags: [Notifications]
      summary: Register an FCM device token for push notifications
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [device_token, platform]
              properties:
                device_token: { type: string }
                platform: { type: string, enum: [IOS, ANDROID, WEB] }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /users/me/devices/{token}:
    delete:
      tags: [Notifications]
      summary: Remove a registered device token
      parameters:
        - $ref: "#/components/parameters/PathToken"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }
  /users/me/notification-preferences:
    put:
      tags: [Notifications]
      summary: Update email/push notification preferences
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Wallet & Ledger / Catalog & Pricing / VTU
  # ---------------------------------------------------------------------
  /wallets/me:
    get:
      tags: [Wallet & Ledger]
      summary: Get all wallet bucket balances (main, commission, escrow)
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /wallets/virtual-accounts:
    get:
      tags: [Wallet & Ledger]
      summary: List the customer's dedicated virtual accounts (DVAs)
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /wallets/commission/sweep:
    post:
      tags: [Wallet & Ledger]
      summary: Sweep commission wallet balance into the main wallet
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [pin]
              properties: { pin: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "422": { $ref: "#/components/responses/Error" }
  /transactions:
    get:
      tags: [Wallet & Ledger]
      summary: Cursor-paginated ledger transaction history
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /transactions/{id}:
    get:
      tags: [Wallet & Ledger]
      summary: Get a single transaction's detail
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

  /services:
    get:
      tags: [Catalog & Pricing]
      summary: List active VTU services (airtime, data, ...)
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /services/{id}/products:
    get:
      tags: [Catalog & Pricing]
      summary: List a service's products, tier-priced if authenticated
      security: []
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /pricing/quote:
    post:
      tags: [Catalog & Pricing]
      summary: Get a tier-priced quote for a product
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [product_id]
              properties: { product_id: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /vtu/airtime/purchase:
    post:
      tags: [VTU]
      summary: Purchase airtime
      description: Requires an `Idempotency-Key` header. Escrow-reserved, then dispatched to a routed provider.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [product_id, recipient, pin]
              properties:
                product_id: { type: string }
                recipient: { type: string, description: "Recipient MSISDN" }
                pin: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "422": { $ref: "#/components/responses/Error" }
  /vtu/data/purchase:
    post:
      tags: [VTU]
      summary: Purchase a data bundle
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [product_id, recipient, pin]
              properties:
                product_id: { type: string }
                recipient: { type: string }
                pin: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /vtu/orders/{id}:
    get:
      tags: [VTU]
      summary: Get VTU order status
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "403": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }

  /internal/uploads/{token}:
    put:
      tags: [Internal]
      summary: Local-storage stand-in for a GCS presigned upload URL
      parameters:
        - $ref: "#/components/parameters/PathToken"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Webhooks
  # ---------------------------------------------------------------------
  /webhooks/billstack:
    post:
      tags: [Webhooks]
      summary: BillStack payment gateway webhook (HMAC-SHA512 signed)
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }
  /webhooks/vtu/{provider}:
    post:
      tags: [Webhooks]
      summary: VTU provider status webhook (shared-secret query token, not HMAC)
      security: []
      parameters:
        - name: provider
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Error" }

  /tasks/notifications/send-email:
    post:
      tags: [Internal]
      summary: Cloud Tasks worker — send a queued email
      description: OIDC bearer-token authenticated (Cloud Tasks service account), not customer/staff JWT.
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "403": { $ref: "#/components/responses/Error" }
  /tasks/notifications/send-push:
    post:
      tags: [Internal]
      summary: Cloud Tasks worker — send a queued push notification
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "403": { $ref: "#/components/responses/Error" }
  /events/order-events:
    post:
      tags: [Internal]
      summary: Pub/Sub push subscription — VTU order lifecycle events
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "403": { $ref: "#/components/responses/Error" }
  /events/wallet-events:
    post:
      tags: [Internal]
      summary: Pub/Sub push subscription — wallet funding events
      security: []
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "403": { $ref: "#/components/responses/Error" }

  # ---------------------------------------------------------------------
  # Admin: Users
  # ---------------------------------------------------------------------
  /admin/users:
    get:
      tags: [Admin Users]
      summary: Search customers
      description: "Requires permission: user:profile:read"
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "403": { $ref: "#/components/responses/Error" }
  /admin/users/{id}:
    get:
      tags: [Admin Users]
      summary: Get customer detail
      description: "Requires permission: user:profile:read"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/users/{id}/freeze:
    post:
      tags: [Admin Users]
      summary: Freeze a customer account
      description: "Requires permission: user:status:freeze + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/users/{id}/unfreeze:
    post:
      tags: [Admin Users]
      summary: Unfreeze a customer account
      description: "Requires permission: user:status:unfreeze + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/users/{id}/kyc/approve:
    post:
      tags: [Admin Users]
      summary: Approve a pending KYC verification
      description: "Requires permission: user:kyc:approve + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/users/{id}/kyc/reject:
    post:
      tags: [Admin Users]
      summary: Reject a pending KYC verification
      description: "Requires permission: user:kyc:reject + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Admin: Wallets
  # ---------------------------------------------------------------------
  /admin/wallets/{user_id}:
    get:
      tags: [Admin Wallets]
      summary: Get a customer's wallet balances
      description: "Requires permission: wallet:balance:read"
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/wallets/{user_id}/ledger:
    get:
      tags: [Admin Wallets]
      summary: Get a customer's ledger entries with GL account detail
      description: "Requires permission: wallet:balance:read"
      parameters:
        - name: user_id
          in: path
          required: true
          schema: { type: string }
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/wallets/adjust:
    post:
      tags: [Admin Wallets]
      summary: Manual wallet credit/debit adjustment
      description: >
        Requires Step-Up. Adjustments over ₦10,000 return 202 and create a
        Maker-Checker approval request instead of executing immediately.
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, adjustment_type, amount, rationale, ticket_reference]
              properties:
                user_id: { type: string }
                adjustment_type: { type: string, enum: [CREDIT, DEBIT] }
                amount: { type: string, example: "5000.00" }
                rationale: { type: string }
                ticket_reference: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "202": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Admin: Payments / Catalog / Providers
  # ---------------------------------------------------------------------
  /admin/payments/gateways:
    get:
      tags: [Admin Payments]
      summary: List configured payment gateways
      description: "Requires permission: payment:gateway:read"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/payments/webhooks/{id}/replay:
    post:
      tags: [Admin Payments]
      summary: Replay a stored webhook payload through ingestion
      description: "Requires permission: payment:webhook:replay + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /admin/catalog/products:
    get:
      tags: [Admin Catalog]
      summary: List all products (including inactive)
      description: "Requires permission: catalog:product:update"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/catalog/products/{id}:
    put:
      tags: [Admin Catalog]
      summary: Update a product (name, active flag)
      description: "Requires permission: catalog:product:update"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/catalog/product-groups:
    get:
      tags: [Admin Catalog]
      summary: List the network/plan-type visibility hierarchy for a service
      description: "Requires permission: catalog:product_group:read"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/catalog/product-groups/{id}:
    put:
      tags: [Admin Catalog]
      summary: Toggle a plan-type/network group active/inactive
      description: "Requires permission: catalog:product_group:update"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/catalog/pricing:
    put:
      tags: [Admin Catalog]
      summary: Directly update a product's selling price (single-step)
      description: "Requires permission: catalog:pricing:update. Applies immediately — no second approver."
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /admin/providers:
    get:
      tags: [Admin Providers]
      summary: List VTU providers with routing/float status
      description: "Requires permission: provider:routing:read"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/providers/{id}/balance:
    get:
      tags: [Admin Providers]
      summary: Get a provider's current float balance
      description: "Requires permission: provider:balance:read"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/providers/{id}/sync-float:
    post:
      tags: [Admin Providers]
      summary: Force a live float balance sync from the provider
      description: "Requires permission: provider:balance:read"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/providers/routing:
    put:
      tags: [Admin Providers]
      summary: Propose a routing priority change (Maker)
      description: "Requires permission: provider:routing:maker + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      responses:
        "202": { $ref: "#/components/responses/Envelope" }
  /admin/providers/routing/{id}/approve:
    put:
      tags: [Admin Providers]
      summary: Approve or reject a proposed routing change (Checker)
      description: "Requires permission: provider:routing:checker + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Admin: Orders / Transactions
  #
  # A VTU order IS this platform's service/financial transaction, so
  # /admin/transactions/* below is an alias over the exact same handlers,
  # permissions, and state as /admin/orders/* — not a second system.
  # ---------------------------------------------------------------------
  /admin/orders:
    get:
      tags: [Admin Orders]
      summary: Search VTU orders / transactions
      description: >
        Requires permission: transaction:order:read. Query parameters:
        `page`, `per_page` (max 200), `user_id`, `status`, `product_id`,
        `provider_id` (matches any dispatch attempt, not just the latest),
        `reference` (partial match on order_ref, or exact order id),
        `provider_reference`, `date_from`/`date_to` (ISO-8601, on
        created_at), `min_amount`/`max_amount` (on amount_debited). Each
        result includes `resolution_status`, `is_refunded`, `is_reversed`,
        `requires_admin_action`, and — when at least one provider attempt
        exists — `provider_code`, `provider_reference`,
        `last_attempt_status` from the most recent dispatch attempt.
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/orders/{id}:
    get:
      tags: [Admin Orders]
      summary: Get full order/transaction investigation detail
      description: >
        Requires permission: transaction:order:read. Returns `order` (the
        order itself), `provider_attempts` (every `vtu.transactions` dispatch
        attempt, including provider request/response payloads),
        `ledger_entries` (every ledger leg posted for this order, correlated
        via `metadata->>'order_id'` — reservation escrow, settlement,
        reversal, or refund), and `audit_trail` (every admin action recorded
        against this order in the immutable, hash-chained audit log).
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/orders/{id}/requery:
    post:
      tags: [Admin Orders]
      summary: Force a provider status requery for a stuck order
      description: >
        Requires permission: transaction:order:requery + Step-Up. Read-only
        with respect to money — it re-checks provider status and does not
        itself settle, reverse, or refund anything, so it is intentionally
        held by lower-privilege support roles.
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/orders/{id}/manual-resolve:
    post:
      tags: [Admin Orders]
      summary: Manually resolve an order stuck in ADMIN_MANUAL_REVIEW
      description: >
        Permission depends on `action` (checked in-service, not by a single
        router-level permission, since the two branches are different-risk):
        `CONFIRM_SUCCESS` requires `transaction:order:resolve` — a
        *different*, higher permission than `transaction:order:requery`, so
        a role that can only requery (e.g. SUPPORT_AGENT) cannot force-settle
        an order. `CONFIRM_FAILURE` (refund) requires
        `transaction:refund:maker`, and is further gated by the approved
        ₦5,000 Maker-Checker threshold (same policy as
        `POST /admin/wallets/adjust`): at or below ₦5,000 it executes
        directly (200, Step-Up only); above it, the response is `202` with
        a `governance.approval_requests` row, and a *different* staff member
        holding `transaction:refund:checker` must approve it via
        `POST /admin/approvals/{id}/act` before the wallet is touched — the
        proposing maker cannot self-approve. Body: `action`
        (`CONFIRM_SUCCESS` or `CONFIRM_FAILURE`), `rationale` (string, min
        10 chars), `resolution_type` — **required** when `action` is
        `CONFIRM_FAILURE`, one of `SERVICE_NOT_DELIVERED`,
        `PROVIDER_FAILED`, `PROVIDER_TIMEOUT`, `DUPLICATE_DEBIT`,
        `SYSTEM_ERROR`, `CUSTOMER_DISPUTE`, `RECONCILIATION_ADJUSTMENT`, or
        `OTHER` — and optional `ticket_reference` (only required in
        practice once a refund turns out to exceed the threshold). A refund
        posts a real ledger credit against the order's escrowed debit
        (never a direct wallet balance write) and moves the order to
        `FAILED_REFUNDED`, which is terminal — retrying the same call, or
        two admins racing each other, returns `409 CONFLICT` or
        `ORDER_ALREADY_RESOLVED` rather than a second refund.
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "202": { $ref: "#/components/responses/Envelope" }
        "409": { $ref: "#/components/responses/Error" }
  /admin/transactions:
    get:
      tags: [Admin Orders]
      summary: Alias of GET /admin/orders
      description: "Requires permission: transaction:order:read"
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/transactions/{id}:
    get:
      tags: [Admin Orders]
      summary: Alias of GET /admin/orders/{id}
      description: "Requires permission: transaction:order:read"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/transactions/{id}/refund:
    post:
      tags: [Admin Orders]
      summary: Alias of POST /admin/orders/{id}/manual-resolve
      description: >
        Requires permission: transaction:order:resolve (CONFIRM_SUCCESS) or
        transaction:refund:maker/checker (CONFIRM_FAILURE, ₦5,000
        Maker-Checker threshold) + Step-Up — see
        /admin/orders/{id}/manual-resolve for full details.
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "202": { $ref: "#/components/responses/Envelope" }
        "409": { $ref: "#/components/responses/Error" }

  # ---------------------------------------------------------------------
  # Admin: Governance (Phase 10)
  # ---------------------------------------------------------------------
  /admin/approvals/pending:
    get:
      tags: [Admin Governance]
      summary: List pending Maker-Checker approval requests
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/approvals/{id}/act:
    post:
      tags: [Admin Governance]
      summary: Approve or reject a pending approval request (Checker)
      description: Requires Step-Up. A different actor from the one who proposed it.
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "409": { $ref: "#/components/responses/Error" }
  /admin/approvals/expire:
    post:
      tags: [Admin Governance]
      summary: Expire stale pending approval requests (job endpoint)
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /admin/staff:
    get:
      tags: [Admin Governance]
      summary: List staff accounts
      description: "Requires permission: iam:staff:read"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
    post:
      tags: [Admin Governance]
      summary: Provision a new staff account with a TOTP secret
      description: "Requires permission: iam:staff:create + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      responses:
        "201": { $ref: "#/components/responses/Envelope" }
  /admin/staff/{id}/roles:
    put:
      tags: [Admin Governance]
      summary: Propose a staff role assignment change (Maker-Checker)
      description: "Requires permission: iam:staff:assign_role + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "202": { $ref: "#/components/responses/Envelope" }
  /admin/staff/{id}/reset-mfa:
    post:
      tags: [Admin Governance]
      summary: Propose a staff TOTP/MFA reset (Maker-Checker)
      description: "Requires permission: iam:staff:reset_mfa + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "202": { $ref: "#/components/responses/Envelope" }

  /admin/audit/logs:
    get:
      tags: [Admin Governance]
      summary: Query the hash-chained audit log
      description: "Requires permission: audit:log:read"
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/audit/logs/export:
    get:
      tags: [Admin Governance]
      summary: Forensic export of the audit log (CSV or JSON)
      description: "Requires permission: audit:log:export"
      parameters:
        - name: format
          in: query
          schema: { type: string, enum: [csv, json], default: json }
      responses:
        "200":
          description: CSV or JSON export, not an Envelope response
          content:
            text/csv: { schema: { type: string } }
            application/json: { schema: { type: object } }

  # ---------------------------------------------------------------------
  # Admin: Reconciliation (Phase 11)
  # ---------------------------------------------------------------------
  /admin/reports/recon:
    get:
      tags: [Admin Reconciliation]
      summary: List reconciliation batches
      description: "Requires permission: audit:log:read"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/reports/recon/{id}:
    get:
      tags: [Admin Reconciliation]
      summary: Get a reconciliation batch with its discrepancies
      description: "Requires permission: audit:log:read"
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/reports/recon/{id}/discrepancies/{discId}/resolve:
    post:
      tags: [Admin Reconciliation]
      summary: Mark a discrepancy resolved (no ledger impact)
      description: "Requires permission: audit:log:read + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
        - name: discId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/reports/recon/{id}/discrepancies/{discId}/write-off:
    post:
      tags: [Admin Reconciliation]
      summary: Propose a ledger write-off for a discrepancy (Maker-Checker)
      description: "Requires permission: finance:reconciliation:write_off + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/PathId"
        - name: discId
          in: path
          required: true
          schema: { type: string }
      responses:
        "202": { $ref: "#/components/responses/Envelope" }

  # ---------------------------------------------------------------------
  # Admin: Feature Flags (Phase 14)
  # ---------------------------------------------------------------------
  /admin/feature-flags:
    get:
      tags: [Admin Feature Flags]
      summary: List all platform feature flags
      description: "Requires permission: system:feature_flag:read"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
  /admin/feature-flags/{key}:
    put:
      tags: [Admin Feature Flags]
      summary: Toggle a feature flag
      description: "Requires permission: system:feature_flag:write + Step-Up"
      security: [{ bearerAuth: [], stepUpAuth: [] }]
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string }
          example: CUSTOMER_REGISTRATION_ENABLED
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [is_enabled]
              properties: { is_enabled: { type: boolean } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: RS256-signed access token from /auth/login, /auth/register, /auth/google, or /admin/auth/login.
    stepUpAuth:
      type: apiKey
      in: header
      name: X-Step-Up-Token
      description: >
        Single-use token from POST /admin/auth/step-up. Required in addition
        to bearerAuth on every CRITICAL_MAKER_CHECKER-risk admin mutation.

  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema: { type: string }
    PathToken:
      name: token
      in: path
      required: true
      schema: { type: string }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
      description: Opaque pagination cursor from a previous response's meta.next_cursor.
    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 20, maximum: 100 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, format: uuid }
      description: Client-generated UUID. Replaying the same key + body returns the original result instead of double-vending.

  schemas:
    Envelope:
      type: object
      required: [success, status_code, message, meta]
      properties:
        success: { type: boolean }
        status_code: { type: integer }
        message: { type: string }
        data: {}
        pagination:
          type: object
          properties:
            next_cursor: { type: ["string", "null"] }
            has_more: { type: boolean }
        meta:
          type: object
          properties:
            request_id: { type: string }
            timestamp: { type: string, format: date-time }
    ErrorEnvelope:
      type: object
      required: [success, status_code, error, meta]
      properties:
        success: { type: boolean, enum: [false] }
        status_code: { type: integer }
        error:
          type: object
          required: [error_code, message]
          properties:
            error_code: { type: string, example: VALIDATION_ERROR }
            message: { type: string }
            details:
              type: array
              items: { type: object }
        meta:
          type: object
          properties:
            request_id: { type: string }

  responses:
    Envelope:
      description: Success envelope. `data` shape is endpoint-specific — see the referencing controller.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Envelope" }
    Error:
      description: Error envelope.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
