openapi: 3.1.0
info:
  title: CustomDesigner Integration API
  version: 1.0.0
  description: |
    Company-scoped CRM API. Authenticate with `Authorization: Bearer <token>` using a token
    generated from **Settings → Developer** (64 hex characters).

servers:
  - url: /api/v1
    description: Relative to your site origin

security:
  - bearerAuth: []

paths:
  /clients:
    get:
      summary: List clients
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            # Token from Settings → Developer. In /docs/api/explorer, the site base is filled from your browser.
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/clients" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "page=1" \
              --data-urlencode "per_page=25" \
              --data-urlencode "q=acme"
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
        - name: q
          in: query
          description: Search company name, name, email, phone
          schema: { type: string }
      responses:
        "200":
          description: Paginated clients
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { type: object }
                  pagination:
                    type: object
                    properties:
                      page: { type: integer }
                      per_page: { type: integer }
                      total: { type: integer }
    post:
      summary: Create client
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X POST "YOUR_APP_ORIGIN/api/v1/clients" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"company_name":"Acme Co","first_name":"Jane","email":"jane@acme.test"}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company_name]
              properties:
                company_name: { type: string }
                first_name: { type: string, nullable: true }
                last_name: { type: string, nullable: true }
                email: { type: string, nullable: true }
                website: { type: string, nullable: true }
                phone: { type: string, nullable: true }
                quickbooks_enabled: { type: boolean }
                tax_exempt: { type: boolean }
                tax_exempt_reason: { type: string, nullable: true }
      responses:
        "200":
          description: Created client (same shape as POST /api/clients/create)
  /clients/{id}:
    get:
      summary: Get one client
      description: |
        Look up a client by numeric ID in the path, by email in the path (URL-encoded), or by
        `email` query parameter (use any placeholder for `{id}` when using the query param, e.g. `_`).
      x-codeSamples:
        - lang: bash
          label: cURL (by ID)
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/clients/CLIENT_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
        - lang: bash
          label: cURL (by email in path)
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/clients/jane%40acme.test" \
              -H "Authorization: Bearer YOUR_TOKEN"
        - lang: bash
          label: cURL (by email query)
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/clients/_" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "email=jane@acme.test"
      parameters:
        - name: id
          in: path
          required: true
          description: Client ID, or client email (URL-encoded) when not using the email query param
          schema: { type: string }
        - name: email
          in: query
          description: Client email (case-insensitive exact match). Alternative to putting email in `{id}`.
          schema: { type: string, format: email }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  client: { type: object }
    patch:
      summary: Update client (partial)
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X PATCH "YOUR_APP_ORIGIN/api/v1/clients/CLIENT_ID" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"phone":"555-0100"}'
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: |
                Standard client fields, or `deleted: true` to soft-delete (hides from list).
      responses:
        "200":
          description: Same as PATCH /api/clients/update
  /orders:
    get:
      summary: List orders (projects)
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/orders" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "page=1" \
              --data-urlencode "client_id=CLIENT_UUID" \
              --data-urlencode "from=2025-01-01" \
              --data-urlencode "to=2025-12-31"
      parameters:
        - name: page
          in: query
          schema: { type: integer }
        - name: per_page
          in: query
          schema: { type: integer }
        - name: client_id
          in: query
          schema: { type: string }
        - name: status
          in: query
          description: order_status_id
          schema: { type: string }
        - name: from
          in: query
          description: Filter created_date >= (date string)
          schema: { type: string }
        - name: to
          in: query
          description: Filter created_date <= (date string)
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { type: object } }
                  pagination:
                    type: object
    post:
      summary: Create order
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X POST "YOUR_APP_ORIGIN/api/v1/orders" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"title":"Spring reorder","client_id":12345,"description":"Rush"}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, client_id]
              properties:
                title: { type: string }
                client_id: { type: number }
                description: { type: string }
                quickbooks_enabled: { type: boolean }
      responses:
        "200":
          description: createOrder result (project, invoice, …)
  /orders/{id}:
    get:
      summary: Get full order (read action)
      description: |
        Returns **project**, **invoice**, **invoice_items**, **payments**, **client**, **tax**, and **artworks**
        (print method, placements, colors, preview URL, status).
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/orders/ORDER_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
    patch:
      summary: Update order
      description: |
        Partial update: include only fields you want to change. Behavior matches `POST /api/order` with `action: "update"`.
        Project fields update the **projects** row; invoice fields update the order’s **default invoice** when one exists.
      parameters:
        - name: id
          in: path
          required: true
          description: Project (order) id
          schema: { type: string }
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X PATCH "YOUR_APP_ORIGIN/api/v1/orders/ORDER_ID" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"title":"Updated title","status_id":2,"deadline":"2026-06-15","tax_exempt":false}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderUpdate'
            examples:
              minimal:
                summary: Title only
                value:
                  title: Summer restock
              status:
                summary: Workflow status
                value:
                  status_id: 3
              scheduleAndClient:
                summary: Dates + client
                value:
                  start_date: '2026-05-01'
                  deadline: '2026-06-30'
                  client_id: 42
              invoiceTotals:
                summary: Tax and discounts (default invoice)
                value:
                  tax_id: 1
                  tax_exempt: false
                  discount_amount: 10
                  discount_amount_type: percentage
                  discount_type: before_tax
              clientVisibleNote:
                summary: Note on invoice (customer-facing)
                value:
                  client_notes: Please ship consolidated.
      responses:
        "200":
          description: Updated **project** and **invoice** (same shape as internal order update)
    delete:
      summary: Delete order (soft-delete project + related invoice; QuickBooks if enabled)
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X DELETE "YOUR_APP_ORIGIN/api/v1/orders/ORDER_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Same as /api/order action delete

  /products/search:
    get:
      summary: Search products (all sources, like invoice line search)
      description: |
        Runs the same parallel lookups as the **invoice “Search products…”** combobox:
        **`local_items`** — company **`items`** catalog (`textSearch` on title when `q` is non-empty; empty `q` returns a small recent sample);
        **`ss_styles`** — S&S **`/styles?search=`** (same as `/api/ss/search`);
        **`sanmar`** — style token extracted from `q` (same rules as the invoice UI) then full catalog rows;
        **`invoice_lines`** — **`invoice_items`** rows matching title or description (company-scoped).

        Use **`include`** to limit sources: comma list of `local`, `ss`, `sanmar`, `lines`. Supplier or DB failures for one source do not fail the whole response (empty arrays).
      parameters:
        - name: q
          in: query
          required: false
          description: Search text; omit or blank for a small sample of local catalog only
          schema: { type: string }
        - name: include
          in: query
          description: "Subset, e.g. `local,ss` or `sanmar,lines`"
          schema: { type: string }
      responses:
        "200":
          description: |
            `{ query, local_items, ss_styles, sanmar: { style, products, count, summary }, invoice_lines }`

  /products/{source}:
    get:
      summary: Product info (SanMar, S&S, or stored invoice line)
      description: |
        - **`sanmar`** — Catalog variants for a style (optional `color`, `size`). Uses the company SanMar integration (same backend as `/api/sanmar/products`).
        - **`ss`** — S&S Activewear JSON, grouped by color (same behavior as `/api/ss/products`). Pass **`sku`** or **`styleId`** (alias `styleid`).
        - **`invoice_item`** — Full **`invoice_items`** row by numeric **`id`** (must belong to your company; not deleted).
      parameters:
        - name: source
          in: path
          required: true
          schema:
            type: string
            enum: [sanmar, ss, invoice_item]
        - name: style
          in: query
          description: Required for **sanmar**. For **ss**, use as **styleId** when `sku` is omitted.
          schema: { type: string }
        - name: color
          in: query
          description: Optional SanMar filter
          schema: { type: string }
        - name: size
          in: query
          description: Optional SanMar filter
          schema: { type: string }
        - name: sku
          in: query
          description: S&S — fetch one SKU (omit if using styleId)
          schema: { type: string }
        - name: styleId
          in: query
          description: S&S style id (query alias **styleid** also accepted)
          schema: { type: string }
        - name: id
          in: query
          description: Required for **invoice_item** — `invoice_items.id`
          schema: { type: integer }
      responses:
        "200":
          description: JSON body; includes `source` and supplier-specific `products` / `query`, or `{ source, item }` for invoice_item
        "404":
          description: invoice_item not found
        "409":
          description: Supplier integration missing or disabled

components:
  schemas:
    OrderUpdate:
      type: object
      description: |
        All properties are optional; send a non-empty object. **`status_id`** is an **order_statuses.id** for your company.
        **`discount_amount_type`**: use `percentage` or `fixed_amount` (aliases `fixed` / `amount` are accepted server-side).
        **`discount_type`**: `before_tax` (default) or `after_tax`.
      properties:
        title:
          type: string
          description: Project / order title
        description:
          type: string
          nullable: true
          description: Project description (also synced to QuickBooks private note when applicable)
        start_date:
          type: string
          description: Project start date (pass-through to DB; ISO date string recommended)
        deadline:
          type: string
          description: "Project deadline; also copied to invoice due_date when an invoice exists"
        client_id:
          type: integer
          description: Client id; updates project and default invoice
        status_id:
          type: integer
          description: "order_statuses.id, stored as project order_status_id"
        client_notes:
          type: string
          nullable: true
          description: "Shown on invoice; maps to invoice note"
        tax_id:
          type: integer
          description: "Tax row id on the default invoice (0 or omit for none, per your data)"
        tax_exempt:
          type: boolean
          description: Invoice tax exempt flag
        tax_exempt_reason:
          type: string
          nullable: true
        discount_amount:
          type: number
          description: "Discount amount; interpreted together with discount_amount_type"
        discount_amount_type:
          type: string
          enum: [percentage, fixed_amount]
          description: "percentage vs fixed_amount (fixed currency amount)"
        discount_type:
          type: string
          enum: [before_tax, after_tax]
        quickbooks_enabled:
          type: boolean
          description: Override QuickBooks sync for this request; defaults to company flag when omitted
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 64 hex (no prefix)
