openapi: '3.1.0'
info:
  title: Motionworks API - Redemption
  version: 1.0.0
  description: >
    Conference-code PLG redemption endpoints. Two operations:
    `POST /v2/redemption/validate` (anonymous pre-flight) and
    `POST /v2/redemption/complete` (authenticated atomic redemption).

    Contract source of truth:
    `docs/architecture/contracts/redemption-v1.md`. Both endpoints emit the
    canonical 6-field error envelope via `services/router/src/lib/errors.ts buildError()`.
    Success responses are bare objects (no `{ data, meta }` wrapper) per the
    auth-infrastructure precedent in
    `services/router/src/routes/anonymous-session.ts:7-18`.
  contact:
    name: Motionworks AI
    url: https://mworks.com
    email: api@mworks.com

servers:
  - url: https://api.mworks.com/v2
    description: Production

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Supabase user JWT for authenticated redemption.

  schemas:
    ValidateRequest:
      type: object
      required: [code]
      properties:
        code:
          type: string
          pattern: '^MW-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
          description: Redemption code in `MW-XXXX-XXXX-XXXX` format.
          example: 'MW-OAAA-2026-0001'

    ValidateSuccess:
      type: object
      required: [code_source, recipient_class, credits_allocated, expires_at, default_path]
      properties:
        code_source:
          type: string
          enum: [geopath, conference, direct]
        recipient_class:
          type: string
          enum: [operator, agency, researcher, brand]
        credits_allocated:
          type: integer
          minimum: 1
          example: 10000
        expires_at:
          type: string
          format: date-time
        default_path:
          type: string
          enum: [app, api]

    CompleteRequest:
      type: object
      required: [code, chosen_path]
      properties:
        code:
          type: string
          pattern: '^MW-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
        chosen_path:
          type: string
          enum: [app, api]

    CompleteSuccess:
      type: object
      required: [success, credits_added, new_balance, new_expiry, redirect_url, telemetry_id]
      properties:
        success:
          type: boolean
          enum: [true]
        credits_added:
          type: integer
          example: 10000
        new_balance:
          type: integer
          example: 15000
        new_expiry:
          type: string
          format: date-time
        redirect_url:
          type: string
          format: uri
        telemetry_id:
          type: string
          format: uuid

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, status, request_id, product, docs_url]
          properties:
            code:
              type: string
              description: snake_case error code.
            message:
              type: string
              description: Plain-English message safe to render to end-users.
            status:
              type: integer
              description: HTTP status code (mirrors response status).
            request_id:
              type: string
              format: uuid
              description: Auto-generated per-request UUID for log correlation.
            product:
              type: string
              enum: [router]
            docs_url:
              type: string
              format: uri

paths:
  /redemption/validate:
    post:
      operationId: validateRedemptionCode
      summary: Validate a redemption code (pre-flight, anonymous)
      tags: [Redemption]
      description: |
        Pre-flight check on a redemption code. Returns metadata for the
        dual-path picker. WITHOUT mutating state. Anonymous; no JWT required.

        Invalid, expired, or not-found codes all return an identical opaque
        `404 redemption_unavailable` envelope (constant-time response). The
        ONLY pre-redemption distinguishable error is `410 already_redeemed`.
      x-motionworks-status: customer
      x-credit-cost: 0
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
      responses:
        '200':
          description: Code is valid and not yet redeemed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateSuccess'
        '404':
          description: >
            Code is invalid, expired, or not found. Opaque envelope per the
            leak-prevention requirement in the contract.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Code has already been redeemed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /redemption/complete:
    post:
      operationId: completeRedemption
      summary: Atomically redeem a code for the authenticated user
      tags: [Redemption]
      description: |
        Calls the `redeem_code` Supabase RPC inside its FOR-UPDATE
        transaction; the RPC writes the credit-grant `credit_events` row.
        After RPC success, the router writes a SECOND telemetry row with
        `event_type='admin_adjust'`, `delta=0`, and
        `metadata.type='redemption_telemetry'` as the discriminator
        (the `credit_event_type` enum has no `redemption_telemetry` value;
        `admin_adjust` is the right vessel for a zero-delta bookkeeping row).

        Idempotent. The required `Idempotency-Key` header is a UUID;
        repeat calls with the same key + same body return the cached
        response. Different body + same key → `409 idempotency_mismatch`.
        Missing header → `400 missing_idempotency_key`. Malformed UUID →
        `400 invalid_idempotency_key`.

        Field names in the success body mirror the `redeem_code` RPC
        return JSON verbatim. The router does NOT remap.
      x-motionworks-status: customer
      x-credit-cost: 0
      security:
        - bearerAuth: []
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema:
            type: string
            format: uuid
          description: Caller-generated UUIDv4 for replay-safe redemption.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompleteRequest'
      responses:
        '200':
          description: Redemption successful.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompleteSuccess'
        '400':
          description: >
            `missing_idempotency_key` (header absent), `invalid_idempotency_key`
            (header is not a UUID), or `invalid_chosen_path` (body field not
            'app' or 'api').
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid Supabase JWT.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Opaque `redemption_unavailable` (same envelope as validate).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Idempotency key reused with a different request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Code has already been redeemed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: RPC error (transient infrastructure failure).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
