openapi: 3.0.3
info:
  title: TabOrder Public API
  version: 1.0.0
  description: |
    Core endpoints for USSD-based commerce, OTP/POD, Products, Orders, Commission, and Suffix/Prefix bundles.
    Curated set for marketplace publication: USSD, OTP/POD, Products, Orders, Suffix Registry, Commission, Prefix Bundles.
servers:
  - url: https://taborder.co.za
  - url: https://taborder.onrender.com
tags:
  - name: USSD
  - name: Products
  - name: Orders
  - name: Delivery
  - name: Media
  - name: Commission
  - name: Suffix Registry
  - name: Prefix Bundles
  - name: Wallet
  - name: Webhooks
security:
  - ApiKeyAuth: []

paths:
  /ussd:
    post:
      tags: [USSD]
      summary: USSD aggregator entrypoint (suffix-aware)
      description: |
        Accepts Africa's Talking-style USSD form POST. Parses vendor prefix and bundle code when present.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                sessionId: { type: string }
                phoneNumber: { type: string }
                text: { type: string, description: "Dialed input e.g. *120*2477*77*2025*25#" }
              required: [sessionId, phoneNumber]
      responses:
        '200':
          description: USSD session response
          content:
            text/plain:
              schema:
                type: string
  /api/vendor/products:
    get:
      tags: [Products]
      summary: List vendor products
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Products
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  total: { type: integer }
                  products:
                    type: array
                    items:
                      $ref: '#/components/schemas/Product'
        '401': { description: Missing or invalid API key }
    post:
      tags: [Products]
      summary: Create product
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreate'
            example:
              name: Test Product
              category: General
              brand: TabOrder
              size: 1 unit
              price: 55.00
              wholesale_price: 45.00
              quantity: 10
              min_stock_alert: 2
              is_combo: false
              combo_items: []
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  product: { $ref: '#/components/schemas/Product' }
  /api/vendor/products/{product_id}:
    put:
      tags: [Products]
      summary: Update product
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: product_id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpdate'
      responses:
        '200':
          description: Updated
    delete:
      tags: [Products]
      summary: Delete product
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: product_id
          required: true
          schema: { type: string }
      responses:
        '200': { description: Deleted }
        '401': { description: Missing or invalid API key }
        '404': { description: Not found }

  /api/orders:
    get:
      tags: [Orders]
      summary: List orders
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: vendor_id
          schema: { type: string }
      responses:
        '200':
          description: Orders
          content:
            application/json:
              schema:
                type: object
                properties:
                  orders:
                    type: array
                    items: { $ref: '#/components/schemas/Order' }
        '401': { description: Missing or invalid API key }

  /api/delivery/verify-otp:
    post:
      tags: [Delivery]
      summary: Verify delivery OTP
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order_number: { type: string }
                otp: { type: string }
              required: [order_number, otp]
      responses:
        '200': { description: Verified }
        '401': { description: Missing or invalid API key }
        '410': { description: OTP expired }
        '423': { description: OTP locked }
    get:
      tags: [Delivery]
      summary: Probe availability (no-op)
      security:
        - ApiKeyAuth: []
      responses:
        '200': { description: Available }
  /api/delivery/resend-otp:
    post:
      tags: [Delivery]
      summary: Resend delivery OTP
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order_number: { type: string }
              required: [order_number]
      responses:
        '200': { description: OTP resent }
        '401': { description: Missing or invalid API key }
        '429': { description: Rate limited }

  /api/delivery-proof/feed:
    get:
      tags: [Delivery]
      summary: Delivery proof feed (OTP/POD timeline)
      security:
        - ApiKeyAuth: []
      responses:
        '200': { description: Feed JSON }
        '401': { description: Missing or invalid API key }

  /api/media/proxy:
    get:
      tags: [Media]
      summary: Proxy external media URLs (e.g., WhatsApp images)
      parameters:
        - in: query
          name: url
          schema: { type: string }
          required: true
      responses:
        '200': { description: Image stream }
        '401': { description: Missing or invalid API key }

  /ping:
    get:
      tags: [Health]
      summary: Health check
      responses:
        '200': { description: Service OK }

  /api/suffix-parser:
    post:
      tags: [Suffix Registry]
      summary: Parse and validate vendor prefix + bundle code
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                vendor_prefix: { type: string, example: '2025' }
                bundle_code: { type: string, example: '25' }
                session_id: { type: string }
                user_msisdn: { type: string }
              required: [vendor_prefix, bundle_code]
      responses:
        '200': { description: Parsed bundle and context }
        '401': { description: Missing or invalid API key }
        '429': { description: Rate limited }

  /api/commission/calculate:
    post:
      tags: [Commission]
      summary: Calculate commission (1%–3% tiered)
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order_amount_cents: { type: integer }
                vendor_id: { type: string }
              required: [order_amount_cents]
      responses:
        '200': { description: Commission result }
        '401': { description: Missing or invalid API key }
        '429': { description: Rate limited }

  /api/wallet/debit/prefix:
    post:
      tags: [Prefix Bundles]
      summary: Debit wallet for monthly Prefix Bundle
      description: Activates or debits the monthly Prefix Bundle (per vendor/partner terms).
      security:
        - ApiKeyAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                months: { type: integer, default: 1 }
                user: { type: string, description: Optional wallet owner override }
      responses:
        '200': { description: Debited }
        '401': { description: Missing or invalid API key }
        '429': { description: Rate limited }

  /api/wallet/pricing:
    get:
      tags: [Wallet]
      summary: Get unit pricing by currency (USD default)
      parameters:
        - in: query
          name: currency
          schema: { type: string, example: USD }
      responses:
        '200': { description: Pricing map }
        '401': { description: Missing or invalid API key }

  /api/wallet/topup/paystack/init:
    post:
      tags: [Wallet]
      summary: Initialize Paystack top-up (hosted checkout)
      description: Always initializes in ZAR for SA merchants; foreign cards may see USD at 3DS but settle in ZAR.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_cents: { type: integer }
                currency: { type: string, example: ZAR }
                user: { type: string, description: Optional wallet owner override }
              required: [amount_cents]
      responses:
        '200':
          description: Authorization URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  authorization_url: { type: string }
                  reference: { type: string }
        '400': { description: Provider rejected init (see body.details) }
        '401': { description: Missing or invalid API key }

  /api/wallet/topup/paystack/metered/init:
    post:
      tags: [Wallet]
      summary: Initialize Paystack top-up based on recent metered usage
      description: Computes an amount from recent usage; if usage is zero, `min_amount_cents` can force a floor.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                period: { oneOf: [ { type: string, enum: [day, week, month] }, { type: integer, description: Hours } ] }
                product: { type: string, enum: [otp, invoicing, pod, prefix, suffix, all], default: all }
                multiplier: { type: number, format: float, default: 1.0 }
                min_amount_cents: { type: integer, default: 0 }
                currency: { type: string, example: ZAR }
                user: { type: string }
      responses:
        '200':
          description: Authorization URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  authorization_url: { type: string }
                  reference: { type: string }
                  amount_cents: { type: integer }
                  period_hours: { type: integer }
                  product: { type: string }
        '400': { description: Metering unavailable or provider error }
        '401': { description: Missing or invalid API key }

  /api/wallet/debit/split:
    post:
      tags: [Wallet]
      summary: Split debit across developer and sub-wallets
      description: Debits the developer wallet and attributes portions to sub-wallet ledgers per-tenant.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                product: { type: string }
                qty: { type: integer, default: 1 }
                charges:
                  type: array
                  items:
                    type: object
                    properties:
                      tenant_id: { type: string, nullable: true }
                      unit_price_cents: { type: integer }
                meta: { type: object }
              required: [product, charges]
      responses:
        '200': { description: Debited }
        '402': { description: Insufficient funds }
        '401': { description: Missing or invalid API key }

  /api/wallet/subwallets:
    get:
      tags: [Wallet]
      summary: List sub-wallet balances for the current API key owner
      responses:
        '200':
          description: Sub-wallets
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        tenant_id: { type: string }
                        currency: { type: string }
                        balance_cents: { type: integer }
                        updated_at: { type: string, format: date-time }
        '401': { description: Missing or invalid API key }

  /api/wallet/subwallets/{tenant_id}/ledger:
    get:
      tags: [Wallet]
      summary: Get sub-wallet ledger for a tenant
      parameters:
        - in: path
          name: tenant_id
          required: true
          schema: { type: string }
      responses:
        '200': { description: Ledger entries }
        '401': { description: Missing or invalid API key }

  /webhooks/africastalking/dlr:
    post:
      tags: [Webhooks]
      summary: Africa's Talking DLR webhook
      responses:
        '200': { description: Acknowledged }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  schemas:
    Product:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        category: { type: string }
        brand: { type: string }
        size: { type: string }
        price: { type: number, format: float }
        wholesale_price: { type: number, format: float }
        quantity: { type: integer }
        min_stock_alert: { type: integer }
        status: { type: string }
        is_combo: { type: boolean }
        combo_items:
          type: array
          items: { type: object, properties: { name: { type: string } } }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ProductCreate:
      type: object
      properties:
        name: { type: string }
        category: { type: string }
        brand: { type: string }
        size: { type: string }
        price: { type: number, format: float }
        wholesale_price: { type: number, format: float }
        quantity: { type: integer }
        min_stock_alert: { type: integer }
        is_combo: { type: boolean }
        combo_items:
          type: array
          items: { type: object, properties: { name: { type: string } } }
      required: [name, price]
    ProductUpdate:
      allOf:
        - $ref: '#/components/schemas/ProductCreate'
    Order:
      type: object
      properties:
        order_number: { type: string }
        customer_name: { type: string }
        customer_phone: { type: string }
        items:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              price: { type: number, format: float }
              quantity: { type: integer }
        total_amount: { type: number, format: float }
        final_amount: { type: number, format: float }
        status: { type: string }
        delivery_otp: { type: string }
        created_at: { type: string, format: date-time }