openapi: 3.0.3
info:
  title: Project Sync API
  version: 1.0.0
  description: |
    Comprehensive backend REST API for Project Sync multi-store e-commerce deployments.
    
    ### Key Architecture Rules:
    - **Single-Tenant Isolation**: Each merchant deployment runs in its own isolated environment and database.
    - **Zero Trust Client Calculations**: Cart totals and product prices submitted by clients are non-authoritative. The backend calculates all totals from persisted database records.
    - **Money Representation**: All monetary values are represented as non-negative integer minor units (Nigerian Kobo), e.g., `15000` = `NGN 150.00`.
    - **Payment Security Warning**: A browser redirect or Paystack client callback is never proof of payment. Payment state must be read from Project Sync after backend verification.
    - **Fulfilment Status Lifecycle**: Initial state is `new`. Valid transitions:
      `new` -> `confirmed` | `cancelled`
      `confirmed` -> `processing` | `cancelled`
      `processing` -> `ready` | `cancelled`
      `ready` -> `completed` | `cancelled`
      Terminal states `completed` and `cancelled` cannot transition into active states.
    - **Product Availability vs Active Status**:
      - `is_active = false`: Product is hidden publicly and cannot be ordered.
      - `is_active = true, is_available = false`: Product is visible in public catalogue with `is_available = false`, but checkout will reject purchase.

servers:
  - url: /api/v1
    description: Current business API

tags:
  - name: Authentication
    description: Administrator authentication, JWT Bearer access tokens, and rotating refresh cookies
  - name: Store
    description: Public storefront profile and settings
  - name: Categories
    description: Public category catalogue
  - name: Products
    description: Public product catalogue and detail lookup
  - name: Orders
    description: Server-authoritative guest checkout and order confirmation
  - name: Payments
    description: Paystack payment initialization, attempt polling, and webhook processing
  - name: Admin Profile
    description: Merchant business profile management
  - name: Admin Categories
    description: Category management and deactivation
  - name: Admin Products
    description: Product CRUD, image upload, and ordering availability toggling
  - name: Admin Orders
    description: Order lifecycle management, status updates, and summary counts
  - name: Admin Payments
    description: Payment attempt inspection and Paystack S2S reconciliation
  - name: System
    description: Health and operational monitoring

paths:
  # =========================================================================
  # System
  # =========================================================================
  /health:
    get:
      tags: [System]
      summary: Health check
      operationId: getHealth
      description: Returns the minimal operational liveness status of the service.
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /health/ready:
    get:
      tags: [System]
      summary: Readiness check
      operationId: getReadiness
      description: Returns the operational readiness of the service and local dependencies (database connectivity) without exposing credentials.
      responses:
        '200':
          description: Service and dependencies are ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadinessResponse'
        '503':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Store & Business Profile (Public)
  # =========================================================================
  /store:
    get:
      tags: [Store]
      summary: Get public store profile
      operationId: getStoreProfile
      description: Returns public merchant profile, branding, supported fulfilment methods, and delivery fees.
      responses:
        '200':
          description: Public store profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProfileResponse'
        '404':
          $ref: '#/components/responses/ProfileNotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Categories (Public)
  # =========================================================================
  /categories:
    get:
      tags: [Categories]
      summary: List active categories
      operationId: listPublicCategories
      description: Returns active categories ordered by display order for storefront navigation.
      responses:
        '200':
          description: Active category list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryListResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Products (Public)
  # =========================================================================
  /products:
    get:
      tags: [Products]
      summary: List active products
      operationId: listPublicProducts
      description: |
        Returns paginated active products for storefront browsing.
        Inactive products (`is_active = false`) are excluded.
        Active but unavailable products (`is_available = false`) are included with `"is_available": false`.
      parameters:
        - name: category
          in: query
          description: Filter by category slug
          required: false
          schema:
            type: string
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/PublicProductSort'
      responses:
        '200':
          description: Paginated public product list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProductListResponse'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  /products/{slug}:
    get:
      tags: [Products]
      summary: Get public product by slug
      operationId: getPublicProductBySlug
      description: Returns active product details by URL slug. Returns 404 if inactive or not found.
      parameters:
        - name: slug
          in: path
          required: true
          description: URL slug of the product
          schema:
            type: string
            example: artisan-ceramic-mug
      responses:
        '200':
          description: Product details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicProductDetailResponse'
        '404':
          $ref: '#/components/responses/ProductNotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Orders (Public & Guest Checkout)
  # =========================================================================
  /orders:
    post:
      tags: [Orders]
      summary: Create guest order (Checkout)
      operationId: createGuestOrder
      description: |
        Creates a transactional guest order.
        
        **Important Rules**:
        1. Client-submitted prices and totals are not accepted or trusted. Backend computes totals strictly from persisted database products and business delivery fee configuration.
        2. If any item is inactive (`is_active = false`) or unavailable (`is_available = false`), checkout is rejected with `422 PRODUCT_UNAVAILABLE`.
        3. Requires an `Idempotency-Key` header (16 to 200 ASCII characters). Replays with identical key return the created order with `meta.idempotent_replay = true`.
        4. Returns an order reference and a secret confirmation token.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
      responses:
        '201':
          description: Order created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCreatedResponse'
        '200':
          description: Idempotent replay of previously created order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCreatedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          $ref: '#/components/responses/Conflict'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /orders/{reference}/confirmation:
    get:
      tags: [Orders]
      summary: Get order confirmation summary
      operationId: getOrderConfirmation
      description: |
        Retrieves confirmation-safe order details for guest customers.
        Requires either `X-Confirmation-Token` header or `token` query parameter issued at checkout.
      parameters:
        - name: reference
          in: path
          required: true
          description: Order reference code (e.g. SYNC-2026-ABC123)
          schema:
            type: string
            example: SYNC-2026-ABC123
        - $ref: '#/components/parameters/ConfirmationTokenHeader'
        - $ref: '#/components/parameters/ConfirmationTokenQuery'
      responses:
        '200':
          description: Order confirmation summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderConfirmationResponse'
        '401':
          $ref: '#/components/responses/ConfirmationTokenInvalid'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Payments (Public & Webhook)
  # =========================================================================
  /orders/{reference}/payments:
    post:
      tags: [Payments]
      summary: Initialize Paystack payment attempt
      operationId: initializeOrderPayment
      description: |
        > **CRITICAL SECURITY WARNING**:
        > A browser redirect or Paystack client callback is never proof of payment. Payment state must be read from Project Sync after backend verification.
        
        Initializes a server-authoritative Paystack payment attempt for an active unpaid order.
        
        **Requirements**:
        - Requires `Idempotency-Key` header (16 to 200 characters).
        - Requires `X-Confirmation-Token` header or `token` query parameter matching the order's confirmation token.
        - Fails if order is already paid, completed, or cancelled.
        - Idempotent replays return existing pending attempt with `meta.idempotent_replay = true`.
      parameters:
        - name: reference
          in: path
          required: true
          description: Order reference code
          schema:
            type: string
            example: SYNC-2026-ABC123
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/ConfirmationTokenHeader'
        - $ref: '#/components/parameters/ConfirmationTokenQuery'
      responses:
        '201':
          description: Payment attempt initialized successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentInitializationResponse'
        '200':
          description: Idempotent replay of previously initialized attempt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentInitializationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/ConfirmationTokenInvalid'
        '409':
          description: Order is already paid or completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Order is cancelled or invalid amount
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: Payment provider gateway unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  /orders/{reference}/payments/{paymentReference}:
    get:
      tags: [Payments]
      summary: Get payment attempt status
      operationId: getPaymentAttemptStatus
      description: |
        > **CRITICAL SECURITY WARNING**:
        > A browser redirect or Paystack client callback is never proof of payment. Payment state must be read from Project Sync after backend verification.
        
        Read-only guest endpoint to poll local payment attempt state and aggregate order payment status.
        Requires `X-Confirmation-Token` header or `token` query parameter.
      parameters:
        - name: reference
          in: path
          required: true
          description: Order reference code
          schema:
            type: string
            example: SYNC-2026-ABC123
        - name: paymentReference
          in: path
          required: true
          description: Payment attempt reference (e.g. PAY-SYNC-...)
          schema:
            type: string
            example: PAY-SYNC-9AB4C3D2E1F0G8H7J6K5
        - $ref: '#/components/parameters/ConfirmationTokenHeader'
        - $ref: '#/components/parameters/ConfirmationTokenQuery'
      responses:
        '200':
          description: Current payment attempt and order payment status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentStatusResponse'
        '401':
          $ref: '#/components/responses/ConfirmationTokenInvalid'
        '404':
          description: Payment attempt or order not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /payments/paystack/webhook:
    post:
      tags: [Payments]
      summary: Paystack webhook handler (Provider-only)
      operationId: handlePaystackWebhook
      description: |
        **Provider-Only Endpoint**:
        This endpoint is invoked exclusively by Paystack's webhook servers via HTTPS POST. It is not called by the frontend application.
        
        - Authenticated cryptographically using HMAC-SHA512 with `x-paystack-signature` against `PAYSTACK_SECRET_KEY`.
        - Requires raw unmodified request body for signature verification.
        - Processes `charge.success` events to transition payment attempts to `successful` and order status to `paid`.
        - If payment arrives for an already cancelled order, marks `orders.payment_status = paid`, flags attempt with `resolution_status = requires_action`, and enqueues merchant late payment action notification.
      parameters:
        - $ref: '#/components/parameters/PaystackSignature'
      requestBody:
        required: true
        description: Raw JSON payload sent directly by Paystack webhook dispatcher
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Webhook processed or safely ignored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericSuccessResponse'
        '400':
          description: Malformed JSON or missing required webhook event fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid cryptographic signature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Admin Authentication
  # =========================================================================
  /admin/login:
    post:
      tags: [Authentication]
      summary: Administrator login
      operationId: adminLogin
      description: |
        Authenticates an administrator with email and password.
        On success:
        - Returns a short-lived Bearer JWT access token (15-minute lifetime) in response body.
        - Sets a rotating, Secure, HttpOnly, SameSite refresh token cookie (`project_sync_refresh`).
        - Enforces strict same-origin checking and rate limiting.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login successful; Bearer access token returned and refresh cookie set
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/refresh:
    post:
      tags: [Authentication]
      summary: Refresh administrator access token
      operationId: adminRefresh
      description: |
        Rotates the refresh token cookie and returns a new Bearer JWT access token.
        Requires the `project_sync_refresh` HttpOnly cookie.
        If a previously rotated token is reused, the entire token family is revoked immediately.
      security:
        - refreshCookie: []
      responses:
        '200':
          description: Access token refreshed and refresh cookie rotated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/me:
    get:
      tags: [Authentication]
      summary: Get current administrator
      operationId: getCurrentAdmin
      description: Returns the currently authenticated merchant user profile.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Safe administrator user profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrentAdminResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/logout:
    post:
      tags: [Authentication]
      summary: Administrator logout
      operationId: adminLogout
      description: |
        Revokes the refresh token family, denylists the supplied JWT access token by `jti` until expiry, and expires the cookie.
      security:
        - bearerAuth: []
        - refreshCookie: []
      responses:
        '200':
          description: Logout completed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericSuccessResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Admin Business Profile
  # =========================================================================
  /admin/profile:
    get:
      tags: [Admin Profile]
      summary: Get business profile for administration
      operationId: getAdminBusinessProfile
      description: Returns complete business profile and operational settings including notification emails and switches.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Complete admin business profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProfileNotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags: [Admin Profile]
      summary: Update business profile
      operationId: updateAdminBusinessProfile
      description: Replaces business profile fields, contact info, branding, notifications, and delivery settings.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileUpdateRequest'
      responses:
        '200':
          description: Updated admin business profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProfileResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProfileNotFound'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Admin Categories
  # =========================================================================
  /admin/categories:
    get:
      tags: [Admin Categories]
      summary: List all categories (Admin)
      operationId: listAdminCategories
      description: Returns all active and inactive categories for management.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: All categories list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminCategoryListResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Admin Categories]
      summary: Create category
      operationId: createCategory
      description: Creates a new product category.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryInput'
      responses:
        '201':
          description: Category created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '409':
          description: Category slug conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/categories/{id}:
    parameters:
      - $ref: '#/components/parameters/InternalUuidId'
    get:
      tags: [Admin Categories]
      summary: Get category by ID
      operationId: getAdminCategoryById
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Category detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/CategoryNotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags: [Admin Categories]
      summary: Update category
      operationId: updateAdminCategory
      description: Replaces category name, slug, description, display order, and active state.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryInput'
      responses:
        '200':
          description: Category updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/CategoryNotFound'
        '409':
          description: Category slug conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [Admin Categories]
      summary: Deactivate category
      operationId: deleteAdminCategory
      description: 'Soft-deactivates category (is_active = false). Existing products remain assigned but display category: null on public storefront.'
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Category deactivated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/CategoryNotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Admin Products
  # =========================================================================
  /admin/products:
    get:
      tags: [Admin Products]
      summary: List products for administration
      operationId: listAdminProducts
      description: Returns paginated products with filtering by category, active status, availability, and search term.
      security:
        - bearerAuth: []
      parameters:
        - name: category_id
          in: query
          description: Filter by category UUID
          required: false
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          description: Filter by active status
          required: false
          schema:
            type: string
            enum: [active, inactive, all]
            default: all
        - name: availability
          in: query
          description: Filter by ordering availability
          required: false
          schema:
            type: string
            enum: [available, unavailable, all]
            default: all
        - name: search
          in: query
          description: Search term across title and description
          required: false
          schema:
            type: string
            maxLength: 160
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/AdminProductSort'
      responses:
        '200':
          description: Paginated admin product list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductListResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Admin Products]
      summary: Create product
      operationId: createAdminProduct
      description: Creates a new product in the merchant catalogue.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductInput'
      responses:
        '201':
          description: Product created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '409':
          description: Product slug conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/products/{id}:
    parameters:
      - $ref: '#/components/parameters/InternalUuidId'
    get:
      tags: [Admin Products]
      summary: Get product by ID
      operationId: getAdminProductById
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Product detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProductNotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags: [Admin Products]
      summary: Update product
      operationId: updateAdminProduct
      description: Replaces product fields, category, title, description, price (kobo), image URL, and active/availability status.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductInput'
      responses:
        '200':
          description: Product updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProductNotFound'
        '409':
          description: Product slug conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [Admin Products]
      summary: Deactivate product
      operationId: deleteAdminProduct
      description: Soft-deactivates product (`is_active = false`). It is immediately hidden from public storefront and slug lookup.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Product deactivated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProductNotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/products/{id}/availability:
    patch:
      tags: [Admin Products]
      summary: Toggle product ordering availability
      operationId: patchProductAvailability
      description: |
        Mutates `is_available` flag without altering product catalogue content.
        Canonical frontend field is `is_available` (or `available`).
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/InternalUuidId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductAvailabilityInput'
      responses:
        '200':
          description: Availability updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProductNotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/products/{id}/image:
    post:
      tags: [Admin Products]
      summary: Upload product image
      operationId: uploadProductImage
      description: |
        Uploads a primary product image file (`multipart/form-data`).
        Validates MIME types (`image/jpeg`, `image/png`, `image/webp`), enforces 5MB limit, and updates `image_url`.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/InternalUuidId'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                  description: Binary image file (JPEG, PNG, WEBP, max 5MB)
      responses:
        '200':
          description: Image uploaded and product updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminProductDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/ProductNotFound'
        '413':
          description: File size exceeds 5MB limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          description: Unsupported image format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Admin Orders
  # =========================================================================
  /admin/orders:
    get:
      tags: [Admin Orders]
      summary: List merchant orders
      operationId: listAdminOrders
      description: Returns paginated merchant orders with filtering by status, search keyword, date ranges, and sorting.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          description: Filter by fulfilment status
          required: false
          schema:
            type: string
            enum: [new, confirmed, processing, ready, completed, cancelled]
        - name: search
          in: query
          description: Search by reference, customer name, phone, or email
          required: false
          schema:
            type: string
            maxLength: 100
        - name: sort
          in: query
          description: Sort field and direction
          required: false
          schema:
            type: string
            enum: [newest, oldest, total_high, total_low]
            default: newest
        - name: date_from
          in: query
          description: Filter orders created on or after date (YYYY-MM-DD)
          required: false
          schema:
            type: string
            format: date
            example: '2026-08-01'
        - name: date_to
          in: query
          description: Filter orders created on or before date (YYYY-MM-DD)
          required: false
          schema:
            type: string
            format: date
            example: '2026-08-31'
      responses:
        '200':
          description: Paginated merchant order list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderListResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/orders/summary:
    get:
      tags: [Admin Orders]
      summary: Summary counts of orders by status
      operationId: getAdminOrderSummary
      description: Returns fast count metrics grouped by canonical fulfilment statuses.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Order status summary counts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderSummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/orders/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Order ID or Order Reference
        schema:
          type: string
          example: '1'
    get:
      tags: [Admin Orders]
      summary: Get merchant order details
      operationId: getAdminOrderDetail
      description: Returns full merchant order record including order item snapshots and status transition history.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Order detail record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/orders/{id}/status:
    parameters:
      - name: id
        in: path
        required: true
        description: Order ID or Order Reference
        schema:
          type: string
          example: '1'
    patch:
      tags: [Admin Orders]
      summary: Update order fulfilment status
      operationId: updateAdminOrderStatus
      description: |
        Performs a validated state transition with `FOR UPDATE` locking and appends an audit record to `order_status_history`.
        
        **Valid State Transitions**:
        - `new` -> `confirmed` | `cancelled`
        - `confirmed` -> `processing` | `cancelled`
        - `processing` -> `ready` | `cancelled`
        - `ready` -> `completed` | `cancelled`
        - `completed` and `cancelled` are terminal and cannot be changed.
        - Identical status submission is idempotent (`unchanged: true`).
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminOrderStatusUpdateRequest'
      responses:
        '200':
          description: Status updated or idempotent replay
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderDetailResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '409':
          description: Invalid status transition attempted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '500':
          $ref: '#/components/responses/InternalError'

  # =========================================================================
  # Admin Payments
  # =========================================================================
  /admin/orders/{orderId}/payments:
    parameters:
      - name: orderId
        in: path
        required: true
        description: Order ID or Order Reference
        schema:
          type: string
          example: '1'
    get:
      tags: [Admin Payments]
      summary: List payment attempts for order
      operationId: listAdminOrderPayments
      description: Returns all payment attempts, provider references, resolution states, and audit events for an order.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Payment attempts and audit log for order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminOrderPaymentsResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /admin/orders/{orderId}/payments/{paymentId}/reconcile:
    parameters:
      - name: orderId
        in: path
        required: true
        description: Order ID or Order Reference
        schema:
          type: string
          example: '1'
      - name: paymentId
        in: path
        required: true
        description: Payment Attempt ID or Reference
        schema:
          type: string
          example: '1'
    post:
      tags: [Admin Payments]
      summary: Trigger S2S Paystack payment reconciliation
      operationId: reconcileAdminPayment
      description: |
        > **CRITICAL SECURITY RULE**:
        > Reconciliation calls Paystack's server-to-server TLS verification API (`GET /transaction/verify/{ref}`) using `PAYSTACK_SECRET_KEY` to discover authoritative financial truth.
        > It does NOT manually set the order or payment status arbitrarily.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Reconciliation outcome completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReconciliationResponse'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '502':
          description: Paystack verification gateway unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  # =========================================================================
  # Security Schemes
  # =========================================================================
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Short-lived administrator JWT access token sent in Authorization: Bearer <token> header.'
    refreshCookie:
      type: apiKey
      in: cookie
      name: project_sync_refresh
      description: 'Rotating HttpOnly cookie used exclusively for /admin/refresh and /admin/logout.'

  # =========================================================================
  # Parameters
  # =========================================================================
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Unique client-generated idempotency key (16 to 200 ASCII characters). Ensures retry safety.
      schema:
        type: string
        minLength: 16
        maxLength: 200
        example: checkout-attempt-20260818-abc12345
    ConfirmationTokenHeader:
      name: X-Confirmation-Token
      in: header
      required: false
      description: Order-scoped secret confirmation token issued at checkout.
      schema:
        type: string
        example: tok_8f3a9b1c7d2e4f5a6b0c1d2e3f4a5b6c
    ConfirmationTokenQuery:
      name: token
      in: query
      required: false
      description: Alternative query parameter representation for confirmation token.
      schema:
        type: string
        example: tok_8f3a9b1c7d2e4f5a6b0c1d2e3f4a5b6c
    PaystackSignature:
      name: x-paystack-signature
      in: header
      required: true
      description: HMAC-SHA512 cryptographic signature computed with `PAYSTACK_SECRET_KEY` over raw request body.
      schema:
        type: string
        example: a1b2c3d4e5f60718293a4b5c6d7e8f90...
    InternalUuidId:
      name: id
      in: path
      required: true
      description: Internal entity UUID
      schema:
        type: string
        format: uuid
        example: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
    Page:
      name: page
      in: query
      required: false
      description: Page number (1-based index)
      schema:
        type: integer
        minimum: 1
        default: 1
        example: 1
    PerPage:
      name: per_page
      in: query
      required: false
      description: Number of items per page (1 to 100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
        example: 20
    PublicProductSort:
      name: sort
      in: query
      required: false
      description: Catalogue sort order
      schema:
        type: string
        enum: [display_order, title, price_low, price_high, newest]
        default: display_order
    AdminProductSort:
      name: sort
      in: query
      required: false
      description: Product admin sort order
      schema:
        type: string
        enum: [display_order, title, price_low, price_high, newest]
        default: display_order

  # =========================================================================
  # Schemas
  # =========================================================================
  schemas:
    MoneyKobo:
      type: integer
      minimum: 0
      description: Non-negative monetary amount in Nigerian Kobo (e.g. 15000 = NGN 150.00)
      example: 15000

    SuccessMeta:
      type: object
      required: [request_id]
      properties:
        request_id:
          type: string
          example: req_66c1f8a2b3c4d
        idempotent_replay:
          type: boolean
          example: false
        unchanged:
          type: boolean
          example: false

    PaginationMeta:
      type: object
      required: [page, per_page, total, total_pages, request_id]
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 20
        total:
          type: integer
          example: 45
        total_pages:
          type: integer
          example: 3
        request_id:
          type: string
          example: req_66c1f8a2b3c4d

    ErrorDetail:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code
          example: VALIDATION_FAILED
        message:
          type: string
          description: Human-readable error message
          example: The request payload failed field validation.
        fields:
          type: object
          description: Field-level validation error lists where applicable
          additionalProperties:
            type: array
            items:
              type: string
          example:
            phone_number:
              - 'Please provide a valid phone number.'

    ErrorResponse:
      type: object
      required: [success, error, meta]
      properties:
        success:
          type: boolean
          const: false
          example: false
        error:
          $ref: '#/components/schemas/ErrorDetail'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    GenericSuccessResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
          example: true
        data:
          type: object
          example: {}
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    HealthResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
          example: true
        data:
          type: object
          required: [status]
          properties:
            status:
              type: string
              example: ok
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    ReadinessResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
          example: true
        data:
          type: object
          required: [status, database]
          properties:
            status:
              type: string
              example: ready
            database:
              type: string
              example: connected
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    # Business Profile Models
    PublicProfile:
      type: object
      required:
        - business_name
        - slug
        - domain
        - whatsapp_number
        - support_email
        - logo_url
        - template_id
        - currency
        - timezone
        - delivery_enabled
        - pickup_enabled
        - fixed_delivery_fee_kobo
      properties:
        business_name:
          type: string
          example: Vintage Boutique Lagos
        slug:
          type: string
          example: vintage-boutique-lagos
        domain:
          type: string
          example: store.vintageboutique.ng
        whatsapp_number:
          type: string
          example: '+2348012345678'
        support_email:
          type: string
          nullable: true
          format: email
          example: hello@vintageboutique.ng
        logo_url:
          type: string
          nullable: true
          format: uri
          example: https://store.vintageboutique.ng/assets/logo.png
        template_id:
          type: string
          example: modern-minimal
        currency:
          type: string
          const: NGN
          example: NGN
        timezone:
          type: string
          example: Africa/Lagos
        delivery_enabled:
          type: boolean
          example: true
        pickup_enabled:
          type: boolean
          example: true
        fixed_delivery_fee_kobo:
          $ref: '#/components/schemas/MoneyKobo'

    PublicProfileResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [profile]
          properties:
            profile:
              $ref: '#/components/schemas/PublicProfile'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminProfile:
      allOf:
        - $ref: '#/components/schemas/PublicProfile'
        - type: object
          required:
            - id
            - order_notification_email
            - merchant_email_notifications_enabled
            - customer_email_notifications_enabled
            - whatsapp_handoff_enabled
            - created_at
            - updated_at
          properties:
            id:
              type: string
              example: '1'
            order_notification_email:
              type: string
              nullable: true
              format: email
              example: orders@vintageboutique.ng
            merchant_email_notifications_enabled:
              type: boolean
              example: true
            customer_email_notifications_enabled:
              type: boolean
              example: true
            whatsapp_handoff_enabled:
              type: boolean
              example: true
            created_at:
              type: string
              format: date-time
              example: '2026-08-01T12:00:00Z'
            updated_at:
              type: string
              format: date-time
              example: '2026-08-18T10:00:00Z'

    AdminProfileResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [profile]
          properties:
            profile:
              $ref: '#/components/schemas/AdminProfile'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    ProfileUpdateRequest:
      type: object
      required:
        - business_name
        - whatsapp_number
        - support_email
        - order_notification_email
        - merchant_email_notifications_enabled
        - customer_email_notifications_enabled
        - whatsapp_handoff_enabled
        - logo_url
        - template_id
        - currency
        - timezone
        - delivery_enabled
        - pickup_enabled
        - fixed_delivery_fee_kobo
      properties:
        business_name:
          type: string
          minLength: 2
          maxLength: 120
          example: Vintage Boutique Lagos
        whatsapp_number:
          type: string
          example: '+2348012345678'
        support_email:
          type: string
          nullable: true
          format: email
          example: hello@vintageboutique.ng
        order_notification_email:
          type: string
          nullable: true
          format: email
          example: orders@vintageboutique.ng
        merchant_email_notifications_enabled:
          type: boolean
          example: true
        customer_email_notifications_enabled:
          type: boolean
          example: true
        whatsapp_handoff_enabled:
          type: boolean
          example: true
        logo_url:
          type: string
          nullable: true
          format: uri
          example: https://store.vintageboutique.ng/assets/logo.png
        template_id:
          type: string
          example: modern-minimal
        currency:
          type: string
          const: NGN
          example: NGN
        timezone:
          type: string
          example: Africa/Lagos
        delivery_enabled:
          type: boolean
          example: true
        pickup_enabled:
          type: boolean
          example: true
        fixed_delivery_fee_kobo:
          $ref: '#/components/schemas/MoneyKobo'

    # Category Models
    Category:
      type: object
      required: [id, name, slug, description, display_order, is_active, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
          example: 4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
        name:
          type: string
          example: Coffee Mugs
        slug:
          type: string
          example: coffee-mugs
        description:
          type: string
          nullable: true
          example: Handcrafted artisanal coffee and tea mugs
        display_order:
          type: integer
          example: 1
        is_active:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time
          example: '2026-08-01T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-08-18T10:00:00Z'

    CategoryInput:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
          example: Coffee Mugs
        slug:
          type: string
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
          example: coffee-mugs
        description:
          type: string
          nullable: true
          maxLength: 1000
          example: Handcrafted artisanal coffee and tea mugs
        display_order:
          type: integer
          minimum: 0
          default: 0
          example: 1
        is_active:
          type: boolean
          default: true
          example: true

    CategoryListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Category'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminCategoryListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Category'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    CategoryDetailResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/Category'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    # Product Models
    Product:
      type: object
      required:
        - id
        - category_id
        - category
        - slug
        - title
        - description
        - price_kobo
        - image_url
        - is_active
        - is_available
        - display_order
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          example: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
        category_id:
          type: string
          nullable: true
          format: uuid
          example: 4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
        category:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            slug:
              type: string
        slug:
          type: string
          example: artisan-ceramic-mug
        title:
          type: string
          example: Artisan Ceramic Mug
        description:
          type: string
          nullable: true
          example: High-fired stoneware mug with matte glaze.
        price_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        image_url:
          type: string
          nullable: true
          format: uri
          example: https://store.vintageboutique.ng/uploads/products/mug.jpg
        is_active:
          type: boolean
          description: When false, completely hidden from public storefront and slug lookup
          example: true
        is_available:
          type: boolean
          description: When false, visible in catalogue with is_available=false, but rejected during checkout
          example: true
        display_order:
          type: integer
          example: 0
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ProductInput:
      type: object
      required: [title, price_kobo]
      properties:
        category_id:
          type: string
          nullable: true
          format: uuid
          example: 4a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
        slug:
          type: string
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
          example: artisan-ceramic-mug
        title:
          type: string
          minLength: 2
          maxLength: 160
          example: Artisan Ceramic Mug
        description:
          type: string
          nullable: true
          maxLength: 10000
          example: High-fired stoneware mug with matte glaze.
        price_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        image_url:
          type: string
          nullable: true
          format: uri
          example: https://store.vintageboutique.ng/uploads/products/mug.jpg
        is_active:
          type: boolean
          default: true
          example: true
        is_available:
          type: boolean
          default: true
          example: true
        display_order:
          type: integer
          minimum: 0
          default: 0
          example: 0

    ProductAvailabilityInput:
      type: object
      properties:
        is_available:
          type: boolean
          description: Canonical boolean flag controlling whether the product can be ordered
          example: false
        available:
          type: boolean
          description: Compatibility alias for is_available
          example: false

    PublicProductListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Product'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    PublicProductDetailResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/Product'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminProductListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/Product'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    AdminProductDetailResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/Product'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    # Order Models
    CheckoutItemInput:
      type: object
      required: [product_id, quantity]
      properties:
        product_id:
          type: string
          format: uuid
          example: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
        quantity:
          type: integer
          minimum: 1
          maximum: 100
          example: 2

    CheckoutRequest:
      type: object
      required:
        - customer_name
        - phone_number
        - fulfilment_method
        - payment_method
        - items
      properties:
        customer_name:
          type: string
          minLength: 2
          maxLength: 120
          example: Amaka Okafor
        phone_number:
          type: string
          description: Nigerian or international telephone number normalized to E.164
          example: '+2348012345678'
        customer_email:
          type: string
          nullable: true
          format: email
          example: amaka@example.com
        fulfilment_method:
          type: string
          enum: [pickup, delivery]
          example: delivery
        delivery_address:
          type: string
          nullable: true
          maxLength: 500
          example: 12 Marina Road, Lagos Island
        state:
          type: string
          nullable: true
          maxLength: 100
          example: Lagos
        payment_method:
          type: string
          enum: [cash_on_delivery, paystack]
          example: paystack
        notes:
          type: string
          nullable: true
          maxLength: 500
          example: Please ring gate bell upon arrival.
        items:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: '#/components/schemas/CheckoutItemInput'

    OrderItemSnapshot:
      type: object
      required:
        - id
        - order_id
        - product_public_id
        - product_title
        - product_slug
        - unit_price_kobo
        - quantity
        - line_total_kobo
        - created_at
      properties:
        id:
          type: string
          example: '1'
        order_id:
          type: string
          example: '1'
        product_id:
          type: string
          nullable: true
          example: '1'
        product_public_id:
          type: string
          format: uuid
          example: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
        product_title:
          type: string
          example: Artisan Ceramic Mug
        product_slug:
          type: string
          example: artisan-ceramic-mug
        unit_price_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        quantity:
          type: integer
          example: 2
        line_total_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        created_at:
          type: string
          format: date-time

    OrderStatusHistoryEntry:
      type: object
      required: [id, previous_status, new_status, changed_by, created_at]
      properties:
        id:
          type: string
          example: '1'
        previous_status:
          type: string
          nullable: true
          example: new
        new_status:
          type: string
          example: confirmed
        changed_by:
          type: string
          nullable: true
          example: usr_admin123
        created_at:
          type: string
          format: date-time

    OrderRecord:
      type: object
      required:
        - reference
        - customer_name
        - phone_number
        - fulfilment_method
        - delivery_address
        - state
        - subtotal_kobo
        - delivery_fee_kobo
        - total_kobo
        - currency
        - payment_method
        - payment_status
        - fulfilment_status
        - items
        - created_at
      properties:
        reference:
          type: string
          example: SYNC-2026-ABC123
        customer_name:
          type: string
          example: Amaka Okafor
        phone_number:
          type: string
          example: '+2348012345678'
        customer_email:
          type: string
          nullable: true
          format: email
          example: amaka@example.com
        fulfilment_method:
          type: string
          enum: [pickup, delivery]
          example: delivery
        delivery_address:
          type: string
          nullable: true
          example: 12 Marina Road, Lagos Island
        state:
          type: string
          nullable: true
          example: Lagos
        subtotal_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        delivery_fee_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        total_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        currency:
          type: string
          const: NGN
          example: NGN
        payment_method:
          type: string
          example: paystack
        payment_status:
          type: string
          description: Aggregate order payment status
          enum: [unpaid, pending, paid, refunded]
          example: unpaid
        fulfilment_status:
          type: string
          description: Fulfilment state machine status
          enum: [new, confirmed, processing, ready, completed, cancelled]
          example: new
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItemSnapshot'
        created_at:
          type: string
          format: date-time
        whatsapp_url:
          type: string
          nullable: true
          format: uri
          example: https://wa.me/2348012345678?text=Hello...
        confirmation_token:
          type: string
          description: Order-scoped secret token returned at checkout for retrieving confirmation and initializing payment
          example: tok_8f3a9b1c7d2e4f5a6b0c1d2e3f4a5b6c

    OrderCreatedResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/OrderRecord'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    OrderConfirmationResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/OrderRecord'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminOrderSummaryItem:
      type: object
      required:
        - id
        - reference
        - customer_name
        - phone_number
        - fulfilment_method
        - subtotal_kobo
        - delivery_fee_kobo
        - total_kobo
        - currency
        - payment_method
        - payment_status
        - fulfilment_status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          example: '1'
        reference:
          type: string
          example: SYNC-2026-ABC123
        customer_name:
          type: string
          example: Amaka Okafor
        phone_number:
          type: string
          example: '+2348012345678'
        customer_email:
          type: string
          nullable: true
          example: amaka@example.com
        fulfilment_method:
          type: string
          enum: [pickup, delivery]
        delivery_address:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        subtotal_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        delivery_fee_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        total_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        currency:
          type: string
          const: NGN
        payment_method:
          type: string
        payment_status:
          type: string
          enum: [unpaid, pending, paid, refunded]
        fulfilment_status:
          type: string
          enum: [new, confirmed, processing, ready, completed, cancelled]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AdminOrderDetail:
      allOf:
        - $ref: '#/components/schemas/AdminOrderSummaryItem'
        - type: object
          required: [items, status_history]
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/OrderItemSnapshot'
            status_history:
              type: array
              items:
                $ref: '#/components/schemas/OrderStatusHistoryEntry'

    AdminOrderListResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/AdminOrderSummaryItem'
        meta:
          $ref: '#/components/schemas/PaginationMeta'

    AdminOrderDetailResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/AdminOrderDetail'
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminOrderSummaryResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [all, new, confirmed, processing, ready, completed, cancelled]
          properties:
            all:
              type: integer
              example: 42
            new:
              type: integer
              example: 5
            confirmed:
              type: integer
              example: 8
            processing:
              type: integer
              example: 12
            ready:
              type: integer
              example: 4
            completed:
              type: integer
              example: 11
            cancelled:
              type: integer
              example: 2
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminOrderStatusUpdateRequest:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [new, confirmed, processing, ready, completed, cancelled]
          example: confirmed

    # Payment Models
    PaymentInitializationResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required:
            - payment_reference
            - authorization_url
            - access_code
            - status
            - expected_amount_kobo
            - currency
          properties:
            payment_reference:
              type: string
              example: PAY-SYNC-9AB4C3D2E1F0G8H7J6K5
            authorization_url:
              type: string
              format: uri
              example: https://checkout.paystack.com/access12345
            access_code:
              type: string
              example: access12345
            status:
              type: string
              description: Payment attempt status
              enum: [initialized, pending, successful, failed, abandoned]
              example: pending
            expected_amount_kobo:
              $ref: '#/components/schemas/MoneyKobo'
            currency:
              type: string
              const: NGN
              example: NGN
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    PaymentStatusResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required:
            - payment_reference
            - order_reference
            - status
            - payment_status
            - amount_kobo
            - currency
            - resolution_status
          properties:
            payment_reference:
              type: string
              example: PAY-SYNC-9AB4C3D2E1F0G8H7J6K5
            order_reference:
              type: string
              example: SYNC-2026-ABC123
            status:
              type: string
              description: Individual payment attempt status
              enum: [initialized, pending, successful, failed, abandoned]
              example: successful
            payment_status:
              type: string
              description: Aggregate order payment status
              enum: [unpaid, pending, paid, refunded]
              example: paid
            amount_kobo:
              $ref: '#/components/schemas/MoneyKobo'
            currency:
              type: string
              const: NGN
              example: NGN
            resolution_status:
              type: string
              description: Operational action status
              enum: [none, requires_action]
              example: none
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    AdminPaymentAttempt:
      type: object
      required:
        - id
        - payment_reference
        - order_id
        - provider
        - provider_reference
        - amount_kobo
        - currency
        - status
        - resolution_status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          example: '1'
        payment_reference:
          type: string
          example: PAY-SYNC-9AB4C3D2E1F0G8H7J6K5
        order_id:
          type: string
          example: '1'
        provider:
          type: string
          example: paystack
        provider_reference:
          type: string
          nullable: true
          example: pstk_ref_123456
        amount_kobo:
          $ref: '#/components/schemas/MoneyKobo'
        currency:
          type: string
          const: NGN
          example: NGN
        status:
          type: string
          enum: [initialized, pending, successful, failed, abandoned]
          example: successful
        resolution_status:
          type: string
          enum: [none, requires_action]
          example: none
        notes:
          type: string
          nullable: true
          example: payment_received_after_cancellation
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AdminOrderPaymentsResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [attempts, events]
          properties:
            attempts:
              type: array
              items:
                $ref: '#/components/schemas/AdminPaymentAttempt'
            events:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  provider:
                    type: string
                  event_type:
                    type: string
                  provider_reference:
                    type: string
                  processing_status:
                    type: string
                  created_at:
                    type: string
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    ReconciliationResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required:
            - reconciled
            - payment_reference
            - attempt_status
            - order_payment_status
            - order_fulfilment_status
            - resolution_status
          properties:
            reconciled:
              type: boolean
              example: true
            payment_reference:
              type: string
              example: PAY-SYNC-9AB4C3D2E1F0G8H7J6K5
            attempt_status:
              type: string
              example: successful
            order_payment_status:
              type: string
              example: paid
            order_fulfilment_status:
              type: string
              example: confirmed
            resolution_status:
              type: string
              example: none
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    # Authentication Models
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
          example: admin@vintageboutique.ng
        password:
          type: string
          format: password
          example: SecureAdminPassphrase123!

    AuthSuccessResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [access_token, token_type, expires_in, admin]
          properties:
            access_token:
              type: string
              description: Short-lived signed JWT access token
              example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
            token_type:
              type: string
              example: Bearer
            expires_in:
              type: integer
              description: Token expiration in seconds (default 900 = 15 minutes)
              example: 900
            admin:
              type: object
              required: [id, email, full_name, is_active]
              properties:
                id:
                  type: string
                  example: usr_admin123
                email:
                  type: string
                  format: email
                  example: admin@vintageboutique.ng
                full_name:
                  type: string
                  example: Store Manager
                is_active:
                  type: boolean
                  example: true
        meta:
          $ref: '#/components/schemas/SuccessMeta'

    CurrentAdminResponse:
      type: object
      required: [success, data, meta]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          required: [admin]
          properties:
            admin:
              type: object
              required: [id, email, full_name, is_active]
              properties:
                id:
                  type: string
                  example: usr_admin123
                email:
                  type: string
                  format: email
                  example: admin@vintageboutique.ng
                full_name:
                  type: string
                  example: Store Manager
                is_active:
                  type: boolean
                  example: true
        meta:
          $ref: '#/components/schemas/SuccessMeta'

  # =========================================================================
  # Standard Error Responses
  # =========================================================================
  responses:
    BadRequest:
      description: The request was malformed or missing required parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: BAD_REQUEST
              message: Missing or invalid request payload.
            meta:
              request_id: req_66c1f8a2b3c4d

    Unauthenticated:
      description: Missing, expired, or invalid administrator authentication credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: UNAUTHENTICATED
              message: Authentication required.
            meta:
              request_id: req_66c1f8a2b3c4d

    Forbidden:
      description: Insufficient permissions or CSRF / origin check failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    ProfileNotFound:
      description: Business profile has not been initialized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: PROFILE_NOT_FOUND
              message: Store profile not configured.
            meta:
              request_id: req_66c1f8a2b3c4d

    CategoryNotFound:
      description: The requested category was not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: CATEGORY_NOT_FOUND
              message: The requested category does not exist.
            meta:
              request_id: req_66c1f8a2b3c4d

    ProductNotFound:
      description: The requested product was not found or is inactive
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: PRODUCT_NOT_FOUND
              message: The requested product does not exist.
            meta:
              request_id: req_66c1f8a2b3c4d

    OrderNotFound:
      description: The requested order was not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: ORDER_NOT_FOUND
              message: The requested order does not exist.
            meta:
              request_id: req_66c1f8a2b3c4d

    ConfirmationTokenInvalid:
      description: The supplied order confirmation token is missing or invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: CONFIRMATION_TOKEN_INVALID
              message: Order confirmation token is missing or invalid.
            meta:
              request_id: req_66c1f8a2b3c4d

    Conflict:
      description: Resource conflict (e.g. duplicate slug or conflicting state)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    UnsupportedMediaType:
      description: Content-Type must be application/json or multipart/form-data
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: UNSUPPORTED_MEDIA_TYPE
              message: Content-Type must be application/json.
            meta:
              request_id: req_66c1f8a2b3c4d

    ValidationFailed:
      description: Input validation failed on one or more fields
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: VALIDATION_FAILED
              message: The request payload failed field validation.
              fields:
                customer_name:
                  - 'Customer name is required.'
            meta:
              request_id: req_66c1f8a2b3c4d

    RateLimited:
      description: Too many requests. Rate limit window exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: RATE_LIMITED
              message: Too many attempts. Please try again later.
            meta:
              request_id: req_66c1f8a2b3c4d

    InternalError:
      description: Production-safe internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              code: INTERNAL_ERROR
              message: An unexpected error occurred.
            meta:
              request_id: req_66c1f8a2b3c4d
