openapi: 3.1.0
info:
  title: 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'
