openapi: 3.1.0
info:
  title: SLC API
  version: 0.1.0
  description: |
    Shopify Legacy Connector (SLC).

    Contract notes:
    - API Gateway base path: `/slc`
    - Auth: session/api_key headers (x-session-guid or x-api-key).
    - SLC uses an action-dispatch model: `POST /slc` with `{"action":"<action>", ...body}`.
    - Two channel types: Standard (full product lifecycle) and Custom (variant-level updates).
    - Credential modes: static (legacy non-expiring) and expiring (1h access + 90d refresh).
  license:
    name: Proprietary
    url: https://g3nretailstack.com/license
servers:
  - url: https://api.g3nretailstack.com/slc
security: []
components:
  securitySchemes:
    sessionAuth:
      type: apiKey
      in: header
      name: x-session-guid
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
  schemas:
    ActionRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          description: The action to perform
        orgcode:
          type: string
          description: Organization code (required for most actions)
        channel_guid:
          type: string
          format: uuid
          description: Channel identifier
    ActionResponse:
      type: object
      properties:
        action:
          type: string
        success:
          type: boolean
        data:
          type: object
        stats:
          type: object
    ErrorResponse:
      type: object
      properties:
        action:
          type: string
        success:
          type: boolean
          enum: [false]
        error:
          type: object
          properties:
            tag:
              type: string
            message:
              type: string
            error_code:
              type: string
            http_status:
              type: integer
            retryable:
              type: boolean
paths:
  /:
    post:
      operationId: slcDispatch
      x-route-class: Tier B
      x-qps-target: 50
      x-concurrency-target: 10
      x-latency-p95-ms: 500
      summary: Dispatch SLC admin action
      description: |
        All SLC admin operations are dispatched via a single POST endpoint.
        The `action` field in the request body determines which operation to perform.

        Available actions:
        - `channel/create` — Create inactive channel
        - `channel/get` — Get channel (credentials redacted)
        - `channel/list` — List channels for org
        - `channel/update` — Update name/config
        - `channel/activate` — inactive → active
        - `channel/pause` — active → paused
        - `channel/resume` — paused → active
        - `channel/deactivate` — any → inactive
        - `channel/delete` — Permanent removal (requires confirmation)
        - `channel/export` — Export config (credentials redacted)
        - `channel/force-sync` — Enqueue all explicit variants → SQS
        - `channel/force-sync-variant` — Enqueue specific variant(s) → SQS
        - `channel/stats` — Sync stats for period
        - `channel/uninstall` — Revoke token, delete webhooks, deactivate channel
        - `channel/purge-maps` — Delete all SHOPIFY_MAP# records for a channel
        - `variants/add` — Batch add to inclusion list
        - `variants/remove` — Batch remove from inclusion list
        - `variants/list` — Paginated list of included variants
        - `facility/bind` — Bind facility to channel
        - `facility/unbind` — Unbind facility
        - `credentials/rotate` — Rotate Shopify token (tests connectivity first)
        - `audit/variant-history` — Query audit by variant GUID
        - `audit/shopify-history` — Query audit by Shopify variant ID
        - `audit/sku-history` — Query audit by SKU across channels
        - `oauth/install-url` — Generate Shopify OAuth authorization URL
        - `oauth/exchange` — Exchange Shopify auth code for access token
        - `vacuum/org` — Delete all channel data for org
        - `ping` — Health check
      security:
        - sessionAuth: []
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ActionRequest'
      responses:
        '200':
          description: Action result (success or error wrapped in envelope)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ActionResponse'
                  - $ref: '#/components/schemas/ErrorResponse'
      x-action-details:
        channel/force-sync-variant:
          summary: Force-sync specific variant(s) on a channel
          description: |
            Enqueues specific variants for immediate sync to Shopify via SQS.
            Channel must be in `active` state.
          request:
            required: [action, channel_guid, variant_guids]
            properties:
              action:
                type: string
                enum: [channel/force-sync-variant]
              channel_guid:
                type: string
                format: uuid
              variant_guids:
                type: array
                items:
                  type: string
                  format: uuid
                minItems: 1
                maxItems: 100
                description: Variant GUIDs to force-sync (max 100 per call)
          response:
            properties:
              channel_guid:
                type: string
                format: uuid
              enqueued:
                type: integer
                description: Number of SQS messages enqueued
              trigger:
                type: string
                enum: [force-sync-variant]
          errors:
            - tag: invalid-input
              when: channel_guid missing, variant_guids empty or > 100
            - tag: not-found
              when: Channel does not exist
            - tag: invalid-state
              when: Channel is not active

        oauth/exchange:
          summary: Exchange Shopify auth code for access token
          description: |
            Exchanges a Shopify OAuth authorization code for an access token.
            Validates granted scopes, tests connectivity, encrypts token via KMS,
            and persists credentials to the channel entity.
            Channel must be in `inactive` state with no existing token.
          request:
            required: [action, channel_guid, orgcode, code]
            properties:
              action:
                type: string
                enum: [oauth/exchange]
              channel_guid:
                type: string
                format: uuid
              orgcode:
                type: string
              code:
                type: string
                description: Shopify OAuth authorization code
          response:
            properties:
              channel_guid:
                type: string
                format: uuid
              exchanged:
                type: boolean
                enum: [true]
              token_mode:
                type: string
                enum: [static, expiring]
              scopes_granted:
                type: string
                description: Comma-separated list of granted Shopify scopes
              token_expires_at:
                type: string
                format: date-time
                description: Present only when token_mode is expiring
              updated_at:
                type: string
                format: date-time
          errors:
            - tag: invalid-input
              when: channel_guid, orgcode, or code missing; channel has no shopify_shop
            - tag: forbidden
              when: orgcode mismatch
            - tag: not-found
              when: Channel does not exist
            - tag: invalid-state
              when: Channel is not inactive
            - tag: config-error
              when: SHOPIFY_CLIENT_ID or SHOPIFY_CLIENT_SECRET not configured
            - tag: exchange-failed
              when: Shopify token exchange HTTP error or no access_token returned
            - tag: insufficient-scopes
              when: Granted scopes do not cover required scopes
            - tag: credential-test-failed
              when: Post-exchange connectivity test fails

        channel/uninstall:
          summary: Revoke token, delete webhooks, deactivate channel
          description: |
            Disconnects a Shopify channel: deletes webhook subscriptions (best-effort),
            revokes the access token (best-effort), clears stored credentials, and
            transitions the channel to `inactive`. SHOPIFY_MAP# records are preserved;
            use `channel/purge-maps` to remove them.
            Requires confirmation string `"UNINSTALL"`.
          request:
            required: [action, channel_guid, orgcode, confirm]
            properties:
              action:
                type: string
                enum: [channel/uninstall]
              channel_guid:
                type: string
                format: uuid
              orgcode:
                type: string
              confirm:
                type: string
                enum: [UNINSTALL]
                description: Must be the literal string "UNINSTALL"
          response:
            properties:
              channel_guid:
                type: string
                format: uuid
              uninstalled:
                type: boolean
                enum: [true]
              state:
                type: string
                enum: [inactive]
              results:
                type: array
                items:
                  type: string
                description: Step-by-step results (webhook deletion, token revocation)
              maps_note:
                type: string
                description: Reminder that SHOPIFY_MAP records are preserved
              updated_at:
                type: string
                format: date-time
          errors:
            - tag: invalid-input
              when: channel_guid or orgcode missing; confirm is not "UNINSTALL"
            - tag: forbidden
              when: orgcode mismatch
            - tag: not-found
              when: Channel does not exist
            - tag: invalid-state
              when: Channel is not active or paused

        channel/purge-maps:
          summary: Delete all SHOPIFY_MAP# records for a channel
          description: |
            Permanently deletes all Shopify variant/product mapping records
            (`SHOPIFY_MAP#` DynamoDB items) for the specified channel.
            Channel must be in `inactive` state (run `channel/uninstall` first).
            Requires confirmation string `"PURGE MAPS"`.
          request:
            required: [action, channel_guid, orgcode, confirm]
            properties:
              action:
                type: string
                enum: [channel/purge-maps]
              channel_guid:
                type: string
                format: uuid
              orgcode:
                type: string
              confirm:
                type: string
                enum: [PURGE MAPS]
                description: Must be the literal string "PURGE MAPS"
          response:
            properties:
              channel_guid:
                type: string
                format: uuid
              purged_count:
                type: integer
                description: Number of SHOPIFY_MAP# records deleted
          errors:
            - tag: invalid-input
              when: channel_guid or orgcode missing; confirm is not "PURGE MAPS"
            - tag: forbidden
              when: orgcode mismatch
            - tag: not-found
              when: Channel does not exist
            - tag: invalid-state
              when: Channel is not inactive

    get:
      operationId: slcOAuthRedirect
      x-route-class: Tier C
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 1000
      x-latency-p99-ms: 2000
      summary: OAuth redirect — Shopify app install entry point
      description: |
        Public endpoint (no auth). Shopify redirects merchants here when they
        install the app (custom distribution). The handler validates the HMAC
        signature from Shopify and issues a 302 redirect to the Shopify OAuth
        authorization URL with the required scopes.

        Query parameters are provided by Shopify:
        `?hmac=...&shop=...&host=...&timestamp=...`
      security: []
      parameters:
        - name: shop
          in: query
          required: true
          schema:
            type: string
            pattern: '^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$'
          description: The merchant's myshopify.com domain
        - name: hmac
          in: query
          required: true
          schema:
            type: string
          description: HMAC-SHA256 signature from Shopify
        - name: timestamp
          in: query
          required: true
          schema:
            type: string
          description: Request timestamp from Shopify
        - name: host
          in: query
          schema:
            type: string
          description: Base64-encoded hostname for embedded app
      responses:
        '302':
          description: Redirect to Shopify OAuth authorization URL
          headers:
            Location:
              schema:
                type: string
              description: 'Shopify OAuth URL: https://{shop}/admin/oauth/authorize?...'
        '400':
          description: Invalid shop parameter
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '403':
          description: HMAC validation failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '500':
          description: Server configuration error (missing SHOPIFY_CLIENT_SECRET)
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string

  /oauth/callback:
    get:
      operationId: slcOAuthCallback
      x-route-class: Tier C
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 3000
      x-latency-p99-ms: 5000
      summary: OAuth callback — Shopify completes token exchange
      description: |
        Public endpoint (no auth). After the merchant approves the app on Shopify,
        Shopify redirects here with the authorization code. The handler:
        1. Validates the HMAC signature
        2. Finds the inactive channel matching the shop domain
        3. Exchanges the auth code for an access token
        4. Validates granted scopes against required scopes
        5. Tests connectivity with a GraphQL health query
        6. Encrypts the token via KMS and persists to the channel entity
        7. Returns an HTML page indicating success or failure

        This is a separate Lambda from the admin handler, with its own
        DynamoDB and KMS grants.
      security: []
      parameters:
        - name: code
          in: query
          required: true
          schema:
            type: string
          description: Shopify OAuth authorization code
        - name: hmac
          in: query
          required: true
          schema:
            type: string
          description: HMAC-SHA256 signature from Shopify
        - name: shop
          in: query
          required: true
          schema:
            type: string
            pattern: '^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$'
          description: The merchant's myshopify.com domain
        - name: state
          in: query
          schema:
            type: string
          description: Nonce originally sent in the authorization request
        - name: timestamp
          in: query
          required: true
          schema:
            type: string
          description: Request timestamp from Shopify
      responses:
        '200':
          description: HTML page — "Setup Complete" with shop name
          content:
            text/html:
              schema:
                type: string
        '400':
          description: HTML page — missing code or invalid shop
          content:
            text/html:
              schema:
                type: string
        '403':
          description: HTML page — HMAC validation failed
          content:
            text/html:
              schema:
                type: string
        '404':
          description: HTML page — no inactive channel found for this shop
          content:
            text/html:
              schema:
                type: string
        '422':
          description: HTML page — token exchange failed, insufficient scopes, or connectivity test failed
          content:
            text/html:
              schema:
                type: string
        '500':
          description: HTML page — server config error or DynamoDB write failure
          content:
            text/html:
              schema:
                type: string
        '502':
          description: HTML page — failed to communicate with Shopify
          content:
            text/html:
              schema:
                type: string
