openapi: 3.1.0
info:
  title: PDF Workspace API
  description: |
    HTTP API for the PDF integration tool.

    **Module 0 — SSO entry**:
      `GET /sso/mock`     (development only; gated by NETSUITE_ALLOW_MOCK_SSO)
      `GET /sso/start`    (real flow; redirects to NetSuite OIDC authorize)
      `GET /sso/callback` (receives code from NetSuite, exchanges for id_token)

    **Module 1-3 — PDF processing**:
      `POST /pdf/upload`       (multipart upload; dispatches ProcessPdfJob)
      `GET  /pdf/status/{id}`  (poll job status while parsing runs async)

    **Module 4 — third-party write-back**:
      `POST /pdf/submit`       (POSTs reviewed fields back to client system)

    Authentication: a Laravel session cookie is established by `/sso/mock` or
    `/sso/callback`. All `/workspace`, `/pdf/*` routes require that cookie.
    CSRF protection: send the `X-XSRF-TOKEN` header (value from cookie of the
    same name) on any non-GET request.
  version: 0.4.0
  contact:
    name: GC PDF Workspace
servers:
  - url: http://127.0.0.1:8765
    description: Local development
  - url: https://pdf-workspace.example.test
    description: Production (placeholder)

tags:
  - name: SSO
    description: Module 0 — session establishment
  - name: Workspace
    description: Inertia page rendering
  - name: PDF
    description: Modules 1-3 — upload, async parse, status polling
  - name: Submit
    description: Module 4 — write back to third-party

paths:

  /sso/mock:
    get:
      tags: [SSO]
      summary: Mock SSO entry (development only)
      description: |
        Stashes `{client_id, product_id, client_name}` in the session and
        redirects to `/workspace`. Disabled in production by setting
        `NETSUITE_ALLOW_MOCK_SSO=false`.
      parameters:
        - { in: query, name: client_id,  required: true, schema: { type: string, example: CLIENT_PINS } }
        - { in: query, name: product_id, required: true, schema: { type: string, example: PROD_001 } }
      responses:
        '302': { description: Redirect to /workspace; sets session cookie }
        '404': { description: Mock SSO is disabled in this environment }
        '422': { description: Unknown client_id or missing parameters }

  /sso/start:
    get:
      tags: [SSO]
      summary: Begin NetSuite OIDC Authorization Code flow
      description: |
        Generates `state` + `nonce`, stashes `{client_id, product_id, nonce}`
        in the session keyed by `state`, then 302s to NetSuite's authorize URL.
      parameters:
        - { in: query, name: client_id,  required: true, schema: { type: string } }
        - { in: query, name: product_id, required: true, schema: { type: string } }
      responses:
        '302': { description: Redirect to NetSuite authorization endpoint }
        '503': { description: NetSuite OIDC not configured (missing env vars) }

  /sso/callback:
    get:
      tags: [SSO]
      summary: NetSuite OIDC redirect target
      description: |
        Exchanges the `code` for tokens, verifies the id_token against the
        JWKS, then redirects to `/workspace` with the SSO context stored in
        the session.
      parameters:
        - { in: query, name: code,  required: true,  schema: { type: string } }
        - { in: query, name: state, required: true,  schema: { type: string } }
        - { in: query, name: error, required: false, schema: { type: string } }
      responses:
        '302': { description: Redirect to /workspace on success }
        '400': { description: Missing parameters or unknown/expired state }
        '401': { description: id_token verification failed (signature/nonce/aud) }
        '422': { description: client_id not registered in config/pdf_rules.php }

  /workspace:
    get:
      tags: [Workspace]
      summary: Inertia.js React workspace page
      description: |
        Renders `resources/js/Pages/Workspace.jsx`. The Inertia middleware
        shares `sso.{client_id, product_id, client_name}` as props.
      responses:
        '200': { description: HTML page (Inertia-aware) }
        '403': { description: No SSO session established (EnsureSsoContext) }

  /pdf/upload:
    post:
      tags: [PDF]
      summary: Upload a PDF and queue parsing
      description: |
        Accepts a multipart file, stores it under `storage/app/uploads/`,
        dispatches `ProcessPdfJob`, returns the job id for polling.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [pdf]
              properties:
                pdf:
                  type: string
                  format: binary
                  description: PDF file (max 20 MB, .pdf only)
      responses:
        '202':
          description: Job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:     { type: boolean, example: true }
                  job_id: { type: string, format: uuid, example: 3bca0aa4-d73c-4336-af3f-4448024ffd2c }
                  status: { type: string, enum: [queued], example: queued }
        '403': { description: No SSO session }
        '422': { description: Validation failure (missing file, wrong mime type, > 20 MB) }

  /pdf/status/{jobId}:
    get:
      tags: [PDF]
      summary: Poll status of a queued ProcessPdfJob
      description: |
        Frontend polls this endpoint roughly every 700 ms until status
        becomes `done` or `failed`. Result is cached for 1 hour.
      parameters:
        - in: path
          name: jobId
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Current state
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/JobStateQueued'
                  - $ref: '#/components/schemas/JobStateProcessing'
                  - $ref: '#/components/schemas/JobStateDone'
                  - $ref: '#/components/schemas/JobStateFailed'
        '403': { description: Job was created by a different session }
        '404': { description: Job state expired or never existed }

  /pdf/submit:
    post:
      tags: [Submit]
      summary: POST reviewed fields back to the third-party system
      description: |
        Looks up the latest `done` job in the session, then posts:
        `{client_id, product_id, image_url, fields:{name→value}}`
        to the URL configured in `pdf_rules.{client_id}.submit_url`.
        Closes the popup window or postMessages the parent on success.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fields]
              properties:
                fields:
                  type: array
                  items:
                    type: object
                    required: [field]
                    properties:
                      field: { type: string, example: order_no }
                      value: { type: [string, 'null'], example: '55191-1725237' }
      responses:
        '200':
          description: Third-party acknowledged
          content:
            application/json:
              schema: { type: object, properties: { ok: { type: boolean, example: true } } }
        '409': { description: No job in session or job not yet done }
        '422': { description: Invalid fields payload shape }
        '502': { description: Third-party endpoint returned a non-2xx status }

components:
  schemas:

    JobStateQueued:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [queued] }

    JobStateProcessing:
      type: object
      required: [status]
      properties:
        status: { type: string, enum: [processing] }

    JobStateDone:
      type: object
      required: [status, client_id, product_id, crop_url, fields]
      properties:
        status:     { type: string, enum: [done] }
        client_id:  { type: string, example: CLIENT_PINS }
        product_id: { type: string, example: PROD_001 }
        crop_url:   { type: string, example: /storage/crops/c39111fc56759fc4-1.png }
        fields:
          type: array
          items:
            $ref: '#/components/schemas/ParsedField'

    JobStateFailed:
      type: object
      required: [status, error]
      properties:
        status:  { type: string, enum: [failed] }
        error:   { type: string, example: 文件內容與當前客戶代號不符,請重新確認 }
        missing:
          type: array
          items: { type: string }
          description: Keywords expected by config/pdf_rules.php but absent from the PDF
          example: ['PINS']

    ParsedField:
      type: object
      required: [field, value, strategy]
      properties:
        field:    { type: string, example: order_no }
        value:    { type: [string, 'null'], example: '55191-1725237' }
        strategy:
          type: string
          enum: [regex, fixed_coord, checkbox_choice]
          example: regex

  securitySchemes:
    SessionCookie:
      type: apiKey
      in: cookie
      name: laravel_session
    XsrfHeader:
      type: apiKey
      in: header
      name: X-XSRF-TOKEN
      description: |
        Mirror of the `XSRF-TOKEN` cookie (URL-decoded). Required for
        non-GET requests by Laravel's VerifyCsrfToken middleware.

security:
  - SessionCookie: []
    XsrfHeader: []
