openapi: 3.1.0
info:
  title: AllNetAirtime API
  version: 1.0.0
  summary: Vendor voucher ordering API.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  description: |
    This OpenAPI document is the canonical contract for the public API endpoints in this project.

    Implementation notes:
    - All JSON API endpoints return the envelope `{ "status": "...", "message": "...", "data": ... }`.
    - Prefer the `X-API-Key` header for vendor API authentication.
    - Some existing endpoints also accept `api_key` in query parameters or JSON bodies for backward compatibility.
    - Voucher order values are whole-number ZAR amounts only.
    - Voucher redemption is not part of the public API contract.
    - To redeem a voucher, dial `*130*410*01*your_pin#`.
servers:
  - url: https://allnetairtime.co.za
    description: Production
  - url: https://allnetairtime:8890
    description: Local development
tags:
  - name: Vendor API
    description: Vendor registration, balance lookups, voucher ordering, and order retrieval.
paths:
  /api/register.php:
    post:
      tags:
        - Vendor API
      summary: Register a vendor account
      operationId: registerVendor
      description: |
        Creates a vendor account, registers the user with FreePaid, and returns a newly issued API key.

        This endpoint does not require authentication.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              basic:
                summary: Standard registration payload
                value:
                  email: vendor@example.com
                  password: secret123
                  full_name: Jane Vendor
                  cell_phone: "0821234567"
                  company_name: Example Spaza
                  affiliate_code: "123456"
      responses:
        '201':
          description: Vendor created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterSuccessEnvelope'
              example:
                status: success
                message: Registration successful. Account is active and ready to use.
                data:
                  user_id: 42
                  email: vendor@example.com
                  full_name: Jane Vendor
                  company_name: Example Spaza
                  cell_phone: "0821234567"
                  freepaid_user_id: "1234567"
                  affiliate_code: "765432"
                  api_key: YOUR_NEW_API_KEY
                  created_at: "2026-03-17 09:15:00"
        '400':
          description: Invalid JSON, missing fields, or validation failure
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/GenericErrorEnvelope'
                  - $ref: '#/components/schemas/ValidationErrorEnvelope'
              examples:
                missing_field:
                  value:
                    status: error
                    message: "Missing required field: email"
                    data: null
                validation_failed:
                  value:
                    status: error
                    message: Validation failed
                    data:
                      errors:
                        - Invalid email format
                        - Password must be at least 6 characters
        '405':
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Method not allowed. Use POST.
                data: null
        '500':
          description: FreePaid or server-side failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: "FreePaid registration failed: Failed to connect to FreePaid system"
                data: null
  /api/order.php:
    post:
      tags:
        - Vendor API
      summary: Place a voucher order
      operationId: placeVoucherOrder
      description: |
        Places a voucher order for the authenticated vendor account.

        Authentication is normally provided via the `X-API-Key` header.
        The current implementation also accepts `api_key` inside the JSON body for backward compatibility.
      security:
        - ApiKeyHeaderAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderRequest'
            examples:
              basic:
                value:
                  quantity: 2
                  value: 10
                  description: Reseller stock top-up
      responses:
        '201':
          description: Order placed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSuccessEnvelope'
              example:
                status: success
                message: Order placed successfully
                data:
                  order_id: 101
                  reference: ALLNET-20260317091500-042-14567-10-2-API
                  total_vouchers: 2
                  value_per_voucher: 10
                  total_amount: 20
                  description: Reseller stock top-up
                  created_at: "2026-03-17 09:15:00"
        '400':
          description: Invalid JSON, missing fields, validation errors, or insufficient balance
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/GenericErrorEnvelope'
                  - $ref: '#/components/schemas/ValidationErrorEnvelope'
              examples:
                api_validation:
                  value:
                    status: error
                    message: Validation failed
                    data:
                      errors:
                        - Value must be a whole number (no decimal points)
                        - Quantity must be at least 1
                insufficient_balance:
                  value:
                    status: error
                    message: Validation failed
                    data:
                      errors:
                        - "Insufficient balance. Required: R20, Available: R5"
        '401':
          description: Missing, invalid, or inactive API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              examples:
                missing_key:
                  value:
                    status: error
                    message: API key required. Provide via X-API-Key header or api_key in payload.
                    data: null
                invalid_key:
                  value:
                    status: error
                    message: Invalid or inactive API key
                    data: null
        '403':
          description: Authenticated user is not allowed to place orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Only vendor accounts can place orders
                data: null
        '405':
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Method not allowed. Use POST.
                data: null
        '500':
          description: Billing or server-side failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Billing system communication error
                data: null
        '503':
          description: Not enough unassigned vouchers available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryErrorEnvelope'
              example:
                status: error
                message: Not enough vouchers available. Please try again later.
                data:
                  requested: 10
                  available: 3
  /api/fetch_order.php:
    get:
      tags:
        - Vendor API
      summary: Fetch an order by reference or ID
      operationId: fetchOrderGet
      description: |
        Retrieves one order and the associated vouchers for the authenticated vendor.

        Supply either `reference` or `order_id`.
        Authentication can be supplied via `X-API-Key` or the `api_key` query parameter.
      security:
        - ApiKeyHeaderAuth: []
        - ApiKeyQueryAuth: []
      parameters:
        - $ref: '#/components/parameters/ReferenceParam'
        - $ref: '#/components/parameters/OrderIdParam'
        - $ref: '#/components/parameters/ApiKeyQueryParam'
      responses:
        '200':
          description: Order fetched successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FetchOrderSuccessEnvelope'
              example:
                status: success
                message: Order fetched successfully
                data:
                  order:
                    id: 101
                    reference: ALLNET-20260317091500-042-14567-10-2-API
                    total_vouchers: 2
                    value_per_voucher: 10
                    total_amount: 20
                    description: Reseller stock top-up
                    is_fully_marked_sent: false
                    created_at: "2026-03-17 09:15:00"
                  statistics:
                    total: 2
                    unused: 2
                    used: 0
                    unassigned: 0
                  vouchers:
                    - voucher_id: 9001
                      pin: "1234567890123456"
                      value: 10
                    - voucher_id: 9002
                      pin: "2234567890123456"
                      value: 10
        '400':
          description: Missing order identifier or invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: "Missing required field: reference or order_id"
                data: null
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Invalid API key
                data: null
        '403':
          description: Inactive account or non-vendor account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              examples:
                inactive:
                  value:
                    status: error
                    message: Account is not active
                    data: null
                not_vendor:
                  value:
                    status: error
                    message: Only vendor accounts can fetch orders
                    data: null
        '404':
          description: Order not found or not accessible to this vendor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Order not found or access denied
                data: null
        '405':
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              example:
                status: error
                message: Method not allowed. Use GET or POST.
                data: null
    post:
      tags:
        - Vendor API
      summary: Fetch an order by reference or ID
      operationId: fetchOrderPost
      description: |
        Same lookup as the GET variant, but accepts JSON input.

        Supply either `reference` or `order_id`.
        Authentication is normally provided via `X-API-Key`.
        The current implementation also accepts `api_key` in the JSON body.
      security:
        - ApiKeyHeaderAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FetchOrderRequest'
            examples:
              by_reference:
                value:
                  reference: ALLNET-20260317091500-042-14567-10-2-API
              by_order_id:
                value:
                  order_id: 101
      responses:
        '200':
          description: Order fetched successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FetchOrderSuccessEnvelope'
        '400':
          description: Invalid JSON, missing order identifier, or invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '403':
          description: Inactive account or non-vendor account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '404':
          description: Order not found or not accessible to this vendor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '405':
          description: Method not allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
  /api/balance.php:
    get:
      tags:
        - Vendor API
      summary: Retrieve the current vendor balance
      operationId: getBalance
      description: |
        Authenticates the vendor, syncs the balance from FreePaid, and returns the current ZAR balance.

        Authentication can be supplied via `X-API-Key` or the `api_key` query parameter.
      security:
        - ApiKeyHeaderAuth: []
        - ApiKeyQueryAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyQueryParam'
      responses:
        '200':
          description: Balance retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceSuccessEnvelope'
              example:
                status: success
                message: Balance retrieved successfully
                data:
                  balance: 150.5
                  currency: ZAR
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              examples:
                missing_key:
                  value:
                    status: error
                    message: API key required. Provide via X-API-Key header or api_key parameter.
                    data: null
                invalid_key:
                  value:
                    status: error
                    message: Invalid API key
                    data: null
        '403':
          description: Inactive account or non-vendor account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
              examples:
                inactive:
                  value:
                    status: error
                    message: Account is not active
                    data: null
                not_vendor:
                  value:
                    status: error
                    message: Only vendor accounts can check balance
                    data: null
        '500':
          description: FreePaid configuration, sync, or database failure
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/GenericErrorEnvelope'
                  - $ref: '#/components/schemas/BalanceSyncErrorEnvelope'
              examples:
                sync_failed:
                  value:
                    status: error
                    message: Failed to sync balance from FreePaid
                    data:
                      error_code: soap
                      error_message: failed to fetch balance
                      details: FreePaid connection timeout
    post:
      tags:
        - Vendor API
      summary: Retrieve the current vendor balance
      operationId: postBalance
      description: |
        Same lookup as the GET variant, but accepts an optional JSON body.

        Authentication is normally provided via `X-API-Key`.
        The current implementation also accepts `api_key` in the JSON body.
      security:
        - ApiKeyHeaderAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalancePostRequest'
            examples:
              with_body_key:
                value:
                  api_key: YOUR_API_KEY
      responses:
        '200':
          description: Balance retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceSuccessEnvelope'
        '400':
          description: Invalid JSON payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '403':
          description: Inactive account or non-vendor account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericErrorEnvelope'
        '500':
          description: FreePaid configuration, sync, or database failure
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/GenericErrorEnvelope'
                  - $ref: '#/components/schemas/BalanceSyncErrorEnvelope'
components:
  securitySchemes:
    ApiKeyHeaderAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Preferred authentication method for vendor API endpoints.
    ApiKeyQueryAuth:
      type: apiKey
      in: query
      name: api_key
      description: Legacy fallback accepted by some vendor API endpoints.
  parameters:
    ApiKeyQueryParam:
      name: api_key
      in: query
      required: false
      schema:
        type: string
      description: Legacy fallback for authentication. Prefer `X-API-Key`.
    ReferenceParam:
      name: reference
      in: query
      required: false
      schema:
        type: string
      description: Order reference. Supply either `reference` or `order_id`.
    OrderIdParam:
      name: order_id
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
      description: Numeric order ID. Supply either `reference` or `order_id`.
  schemas:
    RegisterRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - password
        - full_name
        - cell_phone
        - company_name
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 6
        full_name:
          type: string
        cell_phone:
          type: string
          pattern: '^[0-9]{10}$'
          description: South African mobile number as 10 digits.
        company_name:
          type: string
        affiliate_code:
          type: string
          pattern: '^[1-9]{6}$'
    OrderRequest:
      type: object
      additionalProperties: false
      required:
        - quantity
        - value
      properties:
        quantity:
          type: integer
          minimum: 1
        value:
          type: integer
          minimum: 2
          maximum: 999
          description: Whole-number voucher value in ZAR.
        description:
          type: string
        api_key:
          type: string
          description: Legacy fallback. Prefer `X-API-Key`.
    FetchOrderRequest:
      type: object
      additionalProperties: false
      anyOf:
        - required:
            - reference
        - required:
            - order_id
      properties:
        reference:
          type: string
        order_id:
          type: integer
          minimum: 1
        api_key:
          type: string
          description: Legacy fallback. Prefer `X-API-Key`.
    BalancePostRequest:
      type: object
      additionalProperties: false
      properties:
        api_key:
          type: string
          description: Legacy fallback. Prefer `X-API-Key`.
    RegisterSuccessData:
      type: object
      additionalProperties: false
      required:
        - user_id
        - email
        - full_name
        - company_name
        - cell_phone
        - freepaid_user_id
        - affiliate_code
        - api_key
        - created_at
      properties:
        user_id:
          type: integer
        email:
          type: string
          format: email
        full_name:
          type: string
        company_name:
          type: string
        cell_phone:
          type: string
        freepaid_user_id:
          type: string
        affiliate_code:
          type: string
        api_key:
          type: string
          description: Store this securely. It is returned on registration only.
        created_at:
          type: string
          description: Timestamp in `YYYY-MM-DD HH:MM:SS` format.
    OrderSuccessData:
      type: object
      additionalProperties: false
      required:
        - order_id
        - reference
        - total_vouchers
        - value_per_voucher
        - total_amount
        - description
        - created_at
      properties:
        order_id:
          type: integer
        reference:
          type: string
        total_vouchers:
          type: integer
        value_per_voucher:
          type: integer
        total_amount:
          type: integer
        description:
          type: string
        created_at:
          type: string
          description: Timestamp in `YYYY-MM-DD HH:MM:SS` format.
    FetchOrder:
      type: object
      additionalProperties: false
      required:
        - id
        - reference
        - total_vouchers
        - value_per_voucher
        - total_amount
        - description
        - is_fully_marked_sent
        - created_at
      properties:
        id:
          type: integer
        reference:
          type: string
        total_vouchers:
          type: integer
        value_per_voucher:
          type: number
        total_amount:
          type: number
        description:
          type: string
        is_fully_marked_sent:
          type: boolean
        created_at:
          type: string
          description: Timestamp in `YYYY-MM-DD HH:MM:SS` format.
    OrderStatistics:
      type: object
      additionalProperties: false
      required:
        - total
        - unused
        - used
        - unassigned
      properties:
        total:
          type: integer
        unused:
          type: integer
        used:
          type: integer
        unassigned:
          type: integer
    VoucherItem:
      type: object
      additionalProperties: false
      required:
        - voucher_id
        - pin
        - value
      properties:
        voucher_id:
          type: integer
        pin:
          type: string
        value:
          type: number
    FetchOrderData:
      type: object
      additionalProperties: false
      required:
        - order
        - statistics
        - vouchers
      properties:
        order:
          $ref: '#/components/schemas/FetchOrder'
        statistics:
          $ref: '#/components/schemas/OrderStatistics'
        vouchers:
          type: array
          items:
            $ref: '#/components/schemas/VoucherItem'
    BalanceData:
      type: object
      additionalProperties: false
      required:
        - balance
        - currency
      properties:
        balance:
          type: number
        currency:
          type: string
          enum:
            - ZAR
    ValidationErrorData:
      type: object
      additionalProperties: false
      required:
        - errors
      properties:
        errors:
          type: array
          items:
            type: string
    InventoryErrorData:
      type: object
      additionalProperties: false
      required:
        - requested
        - available
      properties:
        requested:
          type: integer
        available:
          type: integer
    BalanceSyncErrorData:
      type: object
      additionalProperties: false
      required:
        - error_code
        - error_message
      properties:
        error_code:
          type: string
        error_message:
          type: string
        details:
          type:
            - string
            - 'null'
    RegisterSuccessEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - success
        message:
          type: string
        data:
          $ref: '#/components/schemas/RegisterSuccessData'
    OrderSuccessEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - success
        message:
          type: string
        data:
          $ref: '#/components/schemas/OrderSuccessData'
    FetchOrderSuccessEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - success
        message:
          type: string
        data:
          $ref: '#/components/schemas/FetchOrderData'
    BalanceSuccessEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - success
        message:
          type: string
        data:
          $ref: '#/components/schemas/BalanceData'
    GenericErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - error
        message:
          type: string
        data:
          type:
            - object
            - 'null'
          additionalProperties: true
    ValidationErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - error
        message:
          type: string
        data:
          $ref: '#/components/schemas/ValidationErrorData'
    InventoryErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - error
        message:
          type: string
        data:
          $ref: '#/components/schemas/InventoryErrorData'
    BalanceSyncErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - status
        - message
        - data
      properties:
        status:
          type: string
          enum:
            - error
        message:
          type: string
        data:
          $ref: '#/components/schemas/BalanceSyncErrorData'
