openapi: 3.1.0
info:
  title: KARVO ERP REST API
  description: |
    External integration API for KARVO ERP, a B2B distribution and logistics platform
    built for Quebec-based businesses.

    ## Base URLs

    - **v2 endpoints** (full CRUD): `/wp-json/se-erp/v2/`
    - **v1 system endpoints** (info, health, metrics): `/wp-json/se-erp/v1/`

    ## Authentication

    Most v2 endpoints require authentication. Two methods are supported:

    1. **API Key** — pass your key in the `X-SE-ERP-Key` header. Keys are scoped
       (`read` or `write`) and rate-limited per key.
    2. **WordPress Application Passwords** (HTTP Basic Auth) — for users with
       the `manage_woocommerce` capability.

    The `/ping` and `/info` endpoints are fully public.
    The `/health` and `/metrics` endpoints require the `X-Health-Token` header.

    ## Rate Limiting

    API keys have a configurable per-minute rate limit. When using an API key,
    every response includes rate-limit headers:

    - `X-RateLimit-Limit` — requests allowed per minute
    - `X-RateLimit-Remaining` — requests remaining in the current window
    - `X-RateLimit-Reset` — Unix timestamp when the window resets

    ## Pagination

    List endpoints return a standard envelope with pagination metadata:

    ```json
    {
      "data": [...],
      "total": 842,
      "page": 1,
      "pages": 17
    }
    ```

    Custom headers `X-SE-Total`, `X-SE-Page`, and `X-SE-Per-Page` are also set.

    ## Error Format

    ```json
    { "error": "Human-readable message" }
    ```

    ## Quebec Tax Model

    All monetary amounts follow Quebec taxation:
    - `subtotal` — pre-tax amount (used for revenue calculations)
    - `tps` — GST at 5%
    - `tvq` — QST at 9.975%
    - `total` — tax-inclusive total
  version: 13.4.71
  contact:
    name: KARVO ERP Support
    email: support@karvoerp.com
    url: https://karvoerp.com
  license:
    name: Proprietary
    url: https://karvoerp.com/legal/eula

servers:
  - url: https://app.karvoerp.com/wp-json/se-erp
    description: Production

security:
  - apiKey: []
  - basicAuth: []

tags:
  - name: System
    description: Health checks, ping, and system information
  - name: Invoices
    description: Sales invoice management (CRUD + search)
  - name: Shipments
    description: Shipment tracking and status management
  - name: Products
    description: Product catalog with enrichment metadata
  - name: Customers
    description: Customer accounts, health scoring, and search
  - name: Billing
    description: Aging reports, payments, and credit notes
  - name: Warehouses
    description: Multi-warehouse stock levels and transfers
  - name: Pricing
    description: Pricing waterfall, rules, and B2B negotiations
  - name: Orders
    description: WooCommerce orders and standing orders
  - name: Quotes
    description: Quote lifecycle and conversion to invoices
  - name: Deliveries
    description: Delivery management, GPS tracking, and route optimization
  - name: Cleaning
    description: Commercial cleaning contracts and service logs
  - name: Payroll
    description: Quebec payroll — employees, timesheets, pay runs
  - name: Accounting
    description: AR, AP, GL, trial balance, and P&L
  - name: Analytics
    description: Revenue analytics, demand forecasting, churn, and BI hub
  - name: AI
    description: AI chat (text-to-SQL), product enrichment, and recommendations
  - name: Notifications
    description: In-app notification feed
  - name: Webhooks
    description: Outbound webhook subscription management
  - name: Claims
    description: Return and damage claim management
  - name: WHMIS
    description: WHMIS/SDS safety data sheets
  - name: Tenants
    description: Multi-tenant management and usage metering

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-SE-ERP-Key
      description: API key with read or write scope
    basicAuth:
      type: http
      scheme: basic
      description: WordPress Application Password (username + app password)

  parameters:
    page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
        minimum: 1
    per_page:
      name: per_page
      in: query
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 100
    id:
      name: id
      in: path
      required: true
      schema:
        type: integer
    sku_path:
      name: sku
      in: path
      required: true
      schema:
        type: string
      example: "SAN-4567"

  schemas:
    PaginatedEnvelope:
      type: object
      properties:
        data:
          type: array
          items: {}
        total:
          type: integer
          example: 842
        page:
          type: integer
          example: 1
        pages:
          type: integer
          example: 17

    Error:
      type: object
      properties:
        error:
          type: string
          example: "Invoice not found"

    Invoice:
      type: object
      properties:
        id:
          type: integer
          example: 10245
        invoice_no:
          type: string
          example: "SE-2026-04125"
        customer_code:
          type: string
          example: "LAVAL-0042"
        customer_name:
          type: string
          example: "Entretien Prestige Laval Inc."
        invoice_date:
          type: string
          format: date
          example: "2026-04-10"
        subtotal:
          type: number
          example: 1245.50
        tps:
          type: number
          example: 62.28
        tvq:
          type: number
          example: 124.24
        total:
          type: number
          example: 1432.02
        status:
          type: string
          example: "paid"

    InvoiceLine:
      type: object
      properties:
        id:
          type: integer
        invoice_id:
          type: integer
        sku:
          type: string
          example: "CAS-5678"
        description:
          type: string
          example: "Nettoyant multi-surface Cascades 4L"
        qty:
          type: number
          example: 24
        unit_price:
          type: number
          example: 12.45
        line_total:
          type: number
          example: 298.80

    Shipment:
      type: object
      properties:
        id:
          type: integer
          example: 8842
        invoice_no:
          type: string
          example: "SE-2026-04125"
        customer_code:
          type: string
          example: "LAVAL-0042"
        method:
          type: string
          enum: [carrier, pickup_store, pickup_warehouse, delivery_run, delivery_external]
          example: "delivery_run"
        status:
          type: string
          enum: [pending, ready, shipped, in_transit, delivered, picked_up]
          example: "in_transit"
        carrier:
          type: string
          example: "Purolator"

    Product:
      type: object
      properties:
        id:
          type: integer
          example: 3021
        name:
          type: string
          example: "Papier hygiénique Cascades PRO Select 2 plis"
        sku:
          type: string
          example: "CAS-B180"
        price:
          type: number
          example: 68.95
        stock_quantity:
          type: integer
          example: 144
        categories:
          type: array
          items:
            type: string
          example: ["papier-hygienique", "cascades"]
        brand:
          type: string
          example: "Cascades PRO"
        gtin:
          type: string
          example: "0067834521098"
        country_of_origin:
          type: string
          example: "CA"

    Customer:
      type: object
      properties:
        id:
          type: integer
          example: 1042
        customer_code:
          type: string
          example: "MTL-0189"
        customer_name:
          type: string
          example: "Services Ménagers Montréal-Nord Inc."
        email:
          type: string
          example: "comptabilite@smmn.ca"
        phone:
          type: string
          example: "514-555-0199"
        billing_company:
          type: string
          example: "Services Ménagers Montréal-Nord Inc."

    CreditNote:
      type: object
      properties:
        id:
          type: integer
        credit_no:
          type: string
          example: "CN-2026-00034"
        invoice_id:
          type: integer
        customer_code:
          type: string
        amount:
          type: number
          example: 245.00
        reason:
          type: string
          example: "Produit endommagé à la livraison"
        status:
          type: string
          example: "applied"

    Warehouse:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          example: "Entrepôt Montréal-Est"
        code:
          type: string
          example: "MTL-E"
        address:
          type: string
          example: "4500 rue Sherbrooke Est, Montréal, QC H1V 1A1"
        is_active:
          type: boolean

    StockItem:
      type: object
      properties:
        sku:
          type: string
          example: "CAS-B180"
        product_name:
          type: string
          example: "Papier hygiénique Cascades PRO Select 2 plis"
        warehouse:
          type: string
          example: "MTL-E"
        quantity:
          type: integer
          example: 288
        reserved:
          type: integer
          example: 24

    Webhook:
      type: object
      properties:
        id:
          type: integer
        url:
          type: string
          example: "https://erp.clientabc.ca/hooks/karvo"
        events:
          type: array
          items:
            type: string
          example: ["invoice.created", "shipment.delivered"]
        secret:
          type: string
          description: HMAC-SHA256 signing secret
        is_active:
          type: boolean

    Delivery:
      type: object
      properties:
        id:
          type: integer
          example: 5501
        invoice_no:
          type: string
          example: "SE-2026-04125"
        customer_code:
          type: string
          example: "LAVAL-0042"
        method:
          type: string
          example: "delivery_run"
        status:
          type: string
          example: "shipped"
        scheduled_date:
          type: string
          format: date
        carrier:
          type: string
          example: "Purolator"

    DeliveryRoute:
      type: object
      properties:
        id:
          type: integer
        date:
          type: string
          format: date
        driver:
          type: string
          example: "Marc Tremblay"
        status:
          type: string
          example: "in_progress"
        stop_count:
          type: integer
          example: 12

    Order:
      type: object
      properties:
        id:
          type: integer
          example: 7801
        status:
          type: string
          example: "processing"
        total:
          type: number
          example: 1432.02
        subtotal:
          type: number
          example: 1245.50
        currency:
          type: string
          example: "CAD"
        customer_id:
          type: integer
        billing_company:
          type: string
          example: "Entretien Prestige Laval Inc."
        date_created:
          type: string
          format: date-time

    Quote:
      type: object
      properties:
        id:
          type: integer
        quote_no:
          type: string
          example: "QT-000142"
        customer_code:
          type: string
          example: "MTL-0189"
        customer_name:
          type: string
        status:
          type: string
          enum: [draft, sent, accepted, rejected, expired, converted]
          example: "sent"
        subtotal:
          type: number
          example: 3200.00
        tps:
          type: number
        tvq:
          type: number
        total:
          type: number
        valid_until:
          type: string
          format: date

    StandingOrder:
      type: object
      properties:
        id:
          type: integer
        sku:
          type: string
          example: "CAS-B180"
        qty:
          type: integer
          example: 48
        frequency_days:
          type: integer
          example: 14
        status:
          type: string
          enum: [active, paused, cancelled]
        next_run:
          type: string
          format: date

    CleaningContract:
      type: object
      properties:
        id:
          type: integer
        customer_code:
          type: string
          example: "LAVAL-0042"
        site_name:
          type: string
          example: "Tour Deloitte - 3e étage"
        site_address:
          type: string
          example: "1190 avenue des Canadiens-de-Montréal, Montréal, QC"
        frequency:
          type: string
          example: "weekly"
        status:
          type: string
          enum: [draft, active, paused, completed]

    ServiceLog:
      type: object
      properties:
        id:
          type: integer
        contract_id:
          type: integer
        service_date:
          type: string
          format: date
        inspector_name:
          type: string
          example: "Jean-François Dubois"
        status:
          type: string
          example: "completed"
        rating:
          type: integer
          minimum: 1
          maximum: 5

    Employee:
      type: object
      properties:
        id:
          type: integer
        employee_number:
          type: string
          example: "EMP-0042"
        first_name:
          type: string
          example: "Marie-Claude"
        last_name:
          type: string
          example: "Gagnon"
        email:
          type: string
          example: "mc.gagnon@example.ca"
        department:
          type: string
          example: "Entrepôt"
        position:
          type: string
          example: "Manutentionnaire"
        pay_type:
          type: string
          enum: [hourly, salary]
        pay_rate:
          type: number
          example: 22.50
        pay_frequency:
          type: string
          enum: [weekly, biweekly, semi-monthly, monthly]
        status:
          type: string
          example: "active"

    Timesheet:
      type: object
      properties:
        id:
          type: integer
        employee_id:
          type: integer
        work_date:
          type: string
          format: date
        regular_hours:
          type: number
          example: 8.0
        overtime_hours:
          type: number
          example: 1.5
        holiday_hours:
          type: number
          example: 0
        type:
          type: string
          example: "regular"
        status:
          type: string
          enum: [pending, approved, rejected]

    PayRun:
      type: object
      properties:
        id:
          type: integer
        period_start:
          type: string
          format: date
          example: "2026-03-25"
        period_end:
          type: string
          format: date
          example: "2026-04-07"
        pay_date:
          type: string
          format: date
          example: "2026-04-10"
        frequency:
          type: string
          example: "biweekly"
        status:
          type: string
          enum: [draft, calculated, approved, paid]
        employee_count:
          type: integer
          example: 18
        gross_total:
          type: number
          example: 28450.00
        net_total:
          type: number
          example: 19815.75

    PayStub:
      type: object
      properties:
        id:
          type: integer
        employee_id:
          type: integer
        run_id:
          type: integer
        gross_pay:
          type: number
          example: 1580.00
        rrq:
          type: number
          example: 91.64
        ae:
          type: number
          example: 24.96
        rqap:
          type: number
          example: 7.90
        federal_tax:
          type: number
          example: 178.50
        provincial_tax:
          type: number
          example: 142.20
        net_pay:
          type: number
          example: 1134.80

    APInvoice:
      type: object
      properties:
        id:
          type: integer
        vendor:
          type: string
          example: "Cascades PRO"
        invoice_number:
          type: string
          example: "CASC-2026-8891"
        invoice_date:
          type: string
          format: date
        due_date:
          type: string
          format: date
        subtotal:
          type: number
          example: 8450.00
        tps:
          type: number
        tvq:
          type: number
        total:
          type: number
          example: 9723.11
        balance:
          type: number
          example: 9723.11
        status:
          type: string
          example: "pending"

    GLEntry:
      type: object
      properties:
        id:
          type: integer
        entry_date:
          type: string
          format: date
        account:
          type: string
          example: "4100"
        description:
          type: string
          example: "Ventes produits sanitaires - avril"
        debit:
          type: number
          example: 0
        credit:
          type: number
          example: 12500.00
        reference:
          type: string
          example: "SE-2026-04125"

    Claim:
      type: object
      properties:
        id:
          type: integer
        claim_no:
          type: string
          example: "CLM-2026-0089"
        customer_code:
          type: string
        invoice_no:
          type: string
        type:
          type: string
          example: "damaged"
        status:
          type: string
          example: "open"
        amount:
          type: number
          example: 145.00

    WhmisRecord:
      type: object
      properties:
        sku:
          type: string
          example: "DEB-HC01"
        product_name:
          type: string
          example: "Désinfectant chloré Debec 4L"
        hazard_classes:
          type: array
          items:
            type: string
          example: ["H314", "H318"]
        sds_url:
          type: string
          example: "https://app.karvoerp.com/sds/DEB-HC01.pdf"
        sds_date:
          type: string
          format: date

    Tenant:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          example: "Nettoyage ABC Québec"
        slug:
          type: string
          example: "abc-quebec"
        status:
          type: string
          example: "active"
        plan:
          type: string
          example: "pro"

    Notification:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          example: "invoice_overdue"
        title:
          type: string
          example: "Facture en retard"
        message:
          type: string
        is_read:
          type: boolean
        created_at:
          type: string
          format: date-time

    PriceNegotiation:
      type: object
      properties:
        id:
          type: integer
        customer_code:
          type: string
          example: "MTL-0189"
        sku:
          type: string
          example: "CAS-B180"
        current_price:
          type: number
          example: 68.95
        requested_price:
          type: number
          example: 62.00
        status:
          type: string
          enum: [pending, approved, rejected, counter]
          example: "pending"

  responses:
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

paths:

  # ═══════════════════════════════════════════════
  # SYSTEM
  # ═══════════════════════════════════════════════

  /v2/ping:
    get:
      tags: [System]
      summary: Health check ping
      operationId: ping
      security: []
      responses:
        "200":
          description: Server is alive
          content:
            application/json:
              schema:
                type: object
                properties:
                  pong:
                    type: boolean
                    example: true
                  version:
                    type: string
                    example: "13.4.71"
                  time:
                    type: string
                    format: date-time

  /v2/stripe/webhook:
    post:
      tags: [System]
      summary: Stripe webhook receiver
      operationId: stripeWebhook
      security: []
      description: |
        Receives Stripe webhook events. Signature-verified internally via
        the Stripe-Signature header. Rate-limited to 30 events/min per IP.
      parameters:
        - name: Stripe-Signature
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Event processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        "401":
          description: Invalid signature
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded

  # ═══════════════════════════════════════════════
  # INVOICES
  # ═══════════════════════════════════════════════

  /v2/invoices:
    get:
      tags: [Invoices]
      summary: List invoices
      operationId: listInvoices
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: customer_code
          in: query
          schema:
            type: string
          example: "LAVAL-0042"
        - name: status
          in: query
          schema:
            type: string
        - name: date_from
          in: query
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Paginated invoice list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Invoice"
    post:
      tags: [Invoices]
      summary: Create an invoice
      operationId: createInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code, customer_name, lines]
              properties:
                customer_code:
                  type: string
                  example: "LAVAL-0042"
                customer_name:
                  type: string
                  example: "Entretien Prestige Laval Inc."
                invoice_date:
                  type: string
                  format: date
                lines:
                  type: array
                  items:
                    type: object
                    properties:
                      sku:
                        type: string
                      description:
                        type: string
                      qty:
                        type: number
                      unit_price:
                        type: number
      responses:
        "201":
          description: Invoice created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  invoice_id:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"

  /v2/invoices/{id}:
    get:
      tags: [Invoices]
      summary: Get invoice by ID
      operationId: getInvoice
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Invoice detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Invoices]
      summary: Update an invoice
      operationId: updateInvoice
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                notes:
                  type: string
                due_date:
                  type: string
                  format: date
      responses:
        "200":
          description: Invoice updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Invoices]
      summary: Delete an invoice
      operationId: deleteInvoice
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Invoice deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/invoices/{id}/lines:
    get:
      tags: [Invoices]
      summary: Get invoice line items
      operationId: getInvoiceLines
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Line items for the invoice
          content:
            application/json:
              schema:
                type: object
                properties:
                  invoice_id:
                    type: integer
                  lines:
                    type: array
                    items:
                      $ref: "#/components/schemas/InvoiceLine"
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/invoices/search:
    get:
      tags: [Invoices]
      summary: Full-text search invoices
      operationId: searchInvoices
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: q
          in: query
          required: true
          schema:
            type: string
          example: "Cascades"
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Invoice"

  # ═══════════════════════════════════════════════
  # SHIPMENTS
  # ═══════════════════════════════════════════════

  /v2/shipments:
    get:
      tags: [Shipments]
      summary: List shipments
      operationId: listShipments
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: status
          in: query
          schema:
            type: string
        - name: customer_code
          in: query
          schema:
            type: string
        - name: method
          in: query
          schema:
            type: string
            enum: [carrier, pickup_store, pickup_warehouse, delivery_run, delivery_external]
      responses:
        "200":
          description: Paginated shipment list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Shipment"
    post:
      tags: [Shipments]
      summary: Create a shipment
      operationId: createShipment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_no]
              properties:
                invoice_no:
                  type: string
                customer_code:
                  type: string
                method:
                  type: string
                  enum: [carrier, pickup_store, pickup_warehouse, delivery_run, delivery_external]
                carrier:
                  type: string
                tracking_number:
                  type: string
      responses:
        "201":
          description: Shipment created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  shipment_id:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"

  /v2/shipments/{id}:
    get:
      tags: [Shipments]
      summary: Get shipment by ID
      operationId: getShipment
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Shipment detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Shipment"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Shipments]
      summary: Update a shipment
      operationId: updateShipment
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                carrier:
                  type: string
                tracking_number:
                  type: string
                notes:
                  type: string
      responses:
        "200":
          description: Shipment updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/shipments/{id}/advance:
    post:
      tags: [Shipments]
      summary: Advance shipment to next status
      operationId: advanceShipment
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
      responses:
        "200":
          description: Status advanced
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  new_status:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"

  # ═══════════════════════════════════════════════
  # PRODUCTS
  # ═══════════════════════════════════════════════

  /v2/products:
    get:
      tags: [Products]
      summary: List products
      operationId: listProducts
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: sku
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated product list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Product"
    post:
      tags: [Products]
      summary: Create a product
      operationId: createProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, sku]
              properties:
                name:
                  type: string
                  example: "Sac poubelle Frost King 26x36 noir"
                sku:
                  type: string
                  example: "FK-2636-BK"
                regular_price:
                  type: number
                description:
                  type: string
                categories:
                  type: array
                  items:
                    type: string
      responses:
        "201":
          description: Product created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  product_id:
                    type: integer

  /v2/products/{id}:
    get:
      tags: [Products]
      summary: Get product by ID
      operationId: getProduct
      description: Returns full product data including enrichment fields (brand, GTIN, MPN, model, country of origin, videos, PDF specs).
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Product detail with enrichment metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Products]
      summary: Update a product
      operationId: updateProduct
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                regular_price:
                  type: number
                description:
                  type: string
                stock_quantity:
                  type: integer
      responses:
        "200":
          description: Product updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/products/sku/{sku}:
    get:
      tags: [Products]
      summary: Get product by SKU
      operationId: getProductBySku
      description: Resolves SKU aliases before lookup.
      parameters:
        - $ref: "#/components/parameters/sku_path"
      responses:
        "200":
          description: Product detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Product"
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/products/categories:
    get:
      tags: [Products]
      summary: List product categories
      operationId: listProductCategories
      responses:
        "200":
          description: Category list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        name:
                          type: string
                          example: "Papier hygiénique"
                        slug:
                          type: string
                          example: "papier-hygienique"
                        count:
                          type: integer

  # ═══════════════════════════════════════════════
  # CUSTOMERS
  # ═══════════════════════════════════════════════

  /v2/customers:
    get:
      tags: [Customers]
      summary: List customers
      operationId: listCustomers
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated customer list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Customer"
    post:
      tags: [Customers]
      summary: Create a customer
      operationId: createCustomer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code, customer_name]
              properties:
                customer_code:
                  type: string
                  example: "QC-0210"
                customer_name:
                  type: string
                  example: "Nettoyage Industriel Québec Inc."
                email:
                  type: string
                phone:
                  type: string
                billing_company:
                  type: string
      responses:
        "201":
          description: Customer created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  customer_id:
                    type: integer

  /v2/customers/{id}:
    get:
      tags: [Customers]
      summary: Get customer by ID
      operationId: getCustomer
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Customer detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Customers]
      summary: Update a customer
      operationId: updateCustomer
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_name:
                  type: string
                email:
                  type: string
                phone:
                  type: string
      responses:
        "200":
          description: Customer updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
    delete:
      tags: [Customers]
      summary: Delete a customer
      operationId: deleteCustomer
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Customer deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/customers/{id}/health:
    get:
      tags: [Customers]
      summary: Get customer health score
      operationId: getCustomerHealth
      description: Returns churn risk, purchase frequency, and engagement metrics.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Health score data
          content:
            application/json:
              schema:
                type: object
                properties:
                  customer_id:
                    type: integer
                  health_score:
                    type: number
                    example: 0.82
                  churn_risk:
                    type: string
                    example: "low"
                  last_order_days:
                    type: integer
                    example: 12
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/customers/{id}/invoices:
    get:
      tags: [Customers]
      summary: List invoices for a customer
      operationId: getCustomerInvoices
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Customer invoices
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Invoice"

  /v2/customers/search:
    post:
      tags: [Customers]
      summary: Search customers
      operationId: searchCustomers
      description: Searches across WordPress users, customer map, and invoice records.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [q]
              properties:
                q:
                  type: string
                  example: "Prestige"
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Customer"

  # ═══════════════════════════════════════════════
  # BILLING
  # ═══════════════════════════════════════════════

  /v2/billing/aging:
    get:
      tags: [Billing]
      summary: Accounts receivable aging report
      operationId: billingAging
      responses:
        "200":
          description: Aging buckets (current, 30, 60, 90, 120+ days)
          content:
            application/json:
              schema:
                type: object
                properties:
                  current:
                    type: number
                    example: 45200.00
                  days_30:
                    type: number
                    example: 12800.00
                  days_60:
                    type: number
                    example: 3200.00
                  days_90:
                    type: number
                    example: 800.00
                  days_120_plus:
                    type: number
                    example: 150.00

  /v2/billing/payments:
    post:
      tags: [Billing]
      summary: Record a payment
      operationId: recordPayment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_id, amount]
              properties:
                invoice_id:
                  type: integer
                amount:
                  type: number
                  example: 1432.02
                method:
                  type: string
                  example: "cheque"
                reference:
                  type: string
                  example: "CHQ-44210"
      responses:
        "200":
          description: Payment recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  remaining_balance:
                    type: number

  /v2/billing/credits:
    get:
      tags: [Billing]
      summary: List credit notes
      operationId: listCredits
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated credit note list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/CreditNote"
    post:
      tags: [Billing]
      summary: Create a credit note
      operationId: createCredit
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_id, amount, reason]
              properties:
                invoice_id:
                  type: integer
                amount:
                  type: number
                  example: 245.00
                reason:
                  type: string
                  example: "Produit endommagé à la livraison"
      responses:
        "201":
          description: Credit note created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  credit_id:
                    type: integer

  # ═══════════════════════════════════════════════
  # WAREHOUSES
  # ═══════════════════════════════════════════════

  /v2/warehouses:
    get:
      tags: [Warehouses]
      summary: List warehouses
      operationId: listWarehouses
      responses:
        "200":
          description: Warehouse list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Warehouse"
    post:
      tags: [Warehouses]
      summary: Create a warehouse
      operationId: createWarehouse
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, code]
              properties:
                name:
                  type: string
                  example: "Entrepôt Laval"
                code:
                  type: string
                  example: "LAV-1"
                address:
                  type: string
      responses:
        "201":
          description: Warehouse created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  warehouse_id:
                    type: integer

  /v2/warehouses/stock:
    get:
      tags: [Warehouses]
      summary: Stock overview across all warehouses
      operationId: stockOverview
      responses:
        "200":
          description: Aggregated stock levels
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/StockItem"

  /v2/warehouses/stock/{sku}:
    get:
      tags: [Warehouses]
      summary: Stock levels for a specific SKU
      operationId: stockBySku
      parameters:
        - $ref: "#/components/parameters/sku_path"
      responses:
        "200":
          description: Per-warehouse stock for the SKU
          content:
            application/json:
              schema:
                type: object
                properties:
                  sku:
                    type: string
                  warehouses:
                    type: array
                    items:
                      $ref: "#/components/schemas/StockItem"

  /v2/warehouses/transfers:
    get:
      tags: [Warehouses]
      summary: List stock transfers
      operationId: listTransfers
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated transfer list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope"
    post:
      tags: [Warehouses]
      summary: Create a stock transfer
      operationId: createTransfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sku, from_warehouse, to_warehouse, qty]
              properties:
                sku:
                  type: string
                  example: "CAS-B180"
                from_warehouse:
                  type: string
                  example: "MTL-E"
                to_warehouse:
                  type: string
                  example: "LAV-1"
                qty:
                  type: integer
                  example: 48
      responses:
        "201":
          description: Transfer created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  transfer_id:
                    type: integer

  # ═══════════════════════════════════════════════
  # PRICING
  # ═══════════════════════════════════════════════

  /v2/pricing/{sku}:
    get:
      tags: [Pricing]
      summary: Get resolved price for a SKU
      operationId: getPrice
      description: |
        Returns the price from the 4-tier pricing waterfall:
        1. Price policies (B2BKing)
        2. Price rules
        3. Last invoice price
        4. Catalog price
      parameters:
        - $ref: "#/components/parameters/sku_path"
        - name: customer_code
          in: query
          schema:
            type: string
          example: "LAVAL-0042"
      responses:
        "200":
          description: Resolved price with source tier
          content:
            application/json:
              schema:
                type: object
                properties:
                  sku:
                    type: string
                  price:
                    type: number
                    example: 62.00
                  source:
                    type: string
                    enum: [polprix, price_rules, last_invoice, catalog]
                    example: "price_rules"

  /v2/pricing/rules:
    get:
      tags: [Pricing]
      summary: List price rules
      operationId: listPricingRules
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated price rules
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope"

  /v2/pricing/negotiations:
    get:
      tags: [Pricing]
      summary: List price negotiations
      operationId: listNegotiations
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated negotiation list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/PriceNegotiation"
    post:
      tags: [Pricing]
      summary: Create a price negotiation request
      operationId: createNegotiation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code, sku, requested_price]
              properties:
                customer_code:
                  type: string
                  example: "MTL-0189"
                sku:
                  type: string
                  example: "CAS-B180"
                requested_price:
                  type: number
                  example: 62.00
                justification:
                  type: string
                  example: "Volume de 200 caisses/mois"
      responses:
        "201":
          description: Negotiation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  negotiation_id:
                    type: integer

  /v2/pricing/negotiations/{id}:
    put:
      tags: [Pricing]
      summary: Update a negotiation (approve, reject, counter)
      operationId: updateNegotiation
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [approved, rejected, counter]
                counter_price:
                  type: number
                note:
                  type: string
      responses:
        "200":
          description: Negotiation updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  # ═══════════════════════════════════════════════
  # CLAIMS
  # ═══════════════════════════════════════════════

  /v2/claims:
    get:
      tags: [Claims]
      summary: List claims
      operationId: listClaims
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated claim list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Claim"
    post:
      tags: [Claims]
      summary: Create a claim
      operationId: createClaim
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code, invoice_no, type]
              properties:
                customer_code:
                  type: string
                  example: "LAVAL-0042"
                invoice_no:
                  type: string
                  example: "SE-2026-04125"
                type:
                  type: string
                  enum: [damaged, missing, wrong_item, quality]
                  example: "damaged"
                description:
                  type: string
                  example: "3 boites de gants endommagees durant le transport"
                amount:
                  type: number
                  example: 145.00
      responses:
        "201":
          description: Claim created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  claim_id:
                    type: integer

  /v2/claims/{id}:
    get:
      tags: [Claims]
      summary: Get claim by ID
      operationId: getClaim
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Claim detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Claim"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Claims]
      summary: Update a claim
      operationId: updateClaim
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [open, investigating, resolved, closed]
                resolution:
                  type: string
                credit_amount:
                  type: number
      responses:
        "200":
          description: Claim updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  # ═══════════════════════════════════════════════
  # WHMIS / SDS
  # ═══════════════════════════════════════════════

  /v2/whmis:
    get:
      tags: [WHMIS]
      summary: List WHMIS/SDS records
      operationId: listWhmis
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated WHMIS records
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/WhmisRecord"

  /v2/whmis/{sku}:
    get:
      tags: [WHMIS]
      summary: Get WHMIS/SDS data for a SKU
      operationId: getWhmis
      parameters:
        - $ref: "#/components/parameters/sku_path"
      responses:
        "200":
          description: WHMIS record with SDS link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhmisRecord"
        "404":
          $ref: "#/components/responses/NotFound"

  # ═══════════════════════════════════════════════
  # TENANTS
  # ═══════════════════════════════════════════════

  /v2/tenants:
    get:
      tags: [Tenants]
      summary: List tenants
      operationId: listTenants
      responses:
        "200":
          description: Tenant list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Tenant"
    post:
      tags: [Tenants]
      summary: Create a tenant
      operationId: createTenant
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, slug]
              properties:
                name:
                  type: string
                  example: "Nettoyage ABC Québec"
                slug:
                  type: string
                  example: "abc-quebec"
                plan:
                  type: string
                  enum: [starter, pro, enterprise]
                  example: "pro"
      responses:
        "201":
          description: Tenant created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  tenant_id:
                    type: integer

  /v2/tenants/{id}/usage:
    get:
      tags: [Tenants]
      summary: Get tenant usage metrics
      operationId: getTenantUsage
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Usage metrics (API calls, storage, users)
          content:
            application/json:
              schema:
                type: object
                properties:
                  tenant_id:
                    type: integer
                  api_calls:
                    type: integer
                    example: 12450
                  storage_mb:
                    type: number
                    example: 245.8
                  active_users:
                    type: integer
                    example: 8

  # ═══════════════════════════════════════════════
  # WEBHOOKS
  # ═══════════════════════════════════════════════

  /v2/webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook subscriptions
      operationId: listWebhooks
      responses:
        "200":
          description: Webhook subscription list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Webhook"
    post:
      tags: [Webhooks]
      summary: Create a webhook subscription
      operationId: createWebhook
      description: Payloads are signed with HMAC-SHA256 using the returned secret.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  example: "https://erp.clientabc.ca/hooks/karvo"
                events:
                  type: array
                  items:
                    type: string
                  example: ["invoice.created", "shipment.delivered", "payment.received"]
      responses:
        "201":
          description: Webhook created with signing secret
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  webhook_id:
                    type: integer
                  secret:
                    type: string
                    description: HMAC-SHA256 signing secret (shown once)

  /v2/webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Delete a webhook subscription
      operationId: deleteWebhook
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  # ═══════════════════════════════════════════════
  # DELIVERIES
  # ═══════════════════════════════════════════════

  /v2/deliveries:
    get:
      tags: [Deliveries]
      summary: List deliveries
      operationId: listDeliveries
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: status
          in: query
          schema:
            type: string
        - name: customer_code
          in: query
          schema:
            type: string
        - name: date_from
          in: query
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          schema:
            type: string
            format: date
        - name: method
          in: query
          schema:
            type: string
            enum: [carrier, pickup_store, pickup_warehouse, delivery_run, delivery_external]
        - name: run_id
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: Paginated delivery list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Delivery"
    post:
      tags: [Deliveries]
      summary: Create a delivery
      operationId: createDelivery
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                invoice_no:
                  type: string
                  example: "SE-2026-04125"
                order_id:
                  type: integer
                customer_code:
                  type: string
                method:
                  type: string
                  enum: [carrier, pickup_store, pickup_warehouse, delivery_run, delivery_external]
                  example: "delivery_run"
                carrier:
                  type: string
                scheduled_date:
                  type: string
                  format: date
                notes:
                  type: string
      responses:
        "201":
          description: Delivery created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  delivery_id:
                    type: integer

  /v2/deliveries/{id}:
    get:
      tags: [Deliveries]
      summary: Get delivery with history and packages
      operationId: getDelivery
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Delivery detail with status history and packages
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivery:
                    $ref: "#/components/schemas/Delivery"
                  history:
                    type: array
                    items:
                      type: object
                      properties:
                        status:
                          type: string
                        timestamp:
                          type: string
                          format: date-time
                        note:
                          type: string
                  packages:
                    type: array
                    items:
                      type: object
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/deliveries/{id}/status:
    put:
      tags: [Deliveries]
      summary: Update delivery status
      operationId: updateDeliveryStatus
      description: Status transitions are validated. Invalid transitions return allowed transitions.
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [pending, ready, shipped, in_transit, delivered, picked_up]
                note:
                  type: string
      responses:
        "200":
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  new_status:
                    type: string
        "400":
          description: Invalid transition
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  current_status:
                    type: string
                  allowed_transitions:
                    type: array
                    items:
                      type: string

  /v2/deliveries/{id}/tracking:
    get:
      tags: [Deliveries]
      summary: Get GPS tracking trail
      operationId: getDeliveryTracking
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: GPS position trail
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivery_id:
                    type: integer
                  positions:
                    type: array
                    items:
                      type: object
                      properties:
                        lat:
                          type: number
                          example: 45.5017
                        lng:
                          type: number
                          example: -73.5673
                        speed:
                          type: number
                        recorded_at:
                          type: string
                          format: date-time
                  count:
                    type: integer

  /v2/deliveries/{id}/proof:
    post:
      tags: [Deliveries]
      summary: Submit proof of delivery
      operationId: submitDeliveryProof
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                recipient:
                  type: string
                  example: "Jean Tremblay"
                note:
                  type: string
                  example: "Laissé à la réception"
                lat:
                  type: number
                  example: 45.5017
                lng:
                  type: number
                  example: -73.5673
                signature_data:
                  type: string
                  description: Base64-encoded signature image
      responses:
        "200":
          description: Proof recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  proof:
                    type: object
                    properties:
                      recipient:
                        type: string
                      confirmed_at:
                        type: string
                        format: date-time

  /v2/deliveries/routes:
    get:
      tags: [Deliveries]
      summary: List delivery routes
      operationId: listDeliveryRoutes
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: date
          in: query
          schema:
            type: string
            format: date
        - name: status
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Delivery routes
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/DeliveryRoute"

  /v2/deliveries/routes/optimize:
    post:
      tags: [Deliveries]
      summary: Optimize delivery routes
      operationId: optimizeRoutes
      description: Uses VROOM + OSRM engine for multi-stop route optimization.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                run_id:
                  type: integer
                  description: Optimize a specific delivery run
                date:
                  type: string
                  format: date
                  description: Auto-assign shipments for a date
      responses:
        "200":
          description: Optimization result
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  result:
                    type: object

  # ═══════════════════════════════════════════════
  # ORDERS
  # ═══════════════════════════════════════════════

  /v2/orders:
    get:
      tags: [Orders]
      summary: List WooCommerce orders
      operationId: listOrders
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: status
          in: query
          schema:
            type: string
        - name: customer_id
          in: query
          schema:
            type: integer
        - name: date_from
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Paginated order list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Order"
    post:
      tags: [Orders]
      summary: Create a WooCommerce order
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                customer_id:
                  type: integer
                status:
                  type: string
                  example: "pending"
                items:
                  type: array
                  items:
                    type: object
                    required: [product_id]
                    properties:
                      product_id:
                        type: integer
                      quantity:
                        type: integer
                        example: 12
                      subtotal:
                        type: number
                      total:
                        type: number
                billing:
                  type: object
                  properties:
                    first_name:
                      type: string
                    last_name:
                      type: string
                    company:
                      type: string
                    address_1:
                      type: string
                    city:
                      type: string
                    state:
                      type: string
                    postcode:
                      type: string
                    country:
                      type: string
                      example: "CA"
                note:
                  type: string
      responses:
        "201":
          description: Order created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  order_id:
                    type: integer
                  order:
                    $ref: "#/components/schemas/Order"

  /v2/orders/{id}:
    get:
      tags: [Orders]
      summary: Get order by ID
      operationId: getOrder
      description: Returns order detail with line items and order notes.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Order with items and notes
          content:
            application/json:
              schema:
                type: object
                properties:
                  order:
                    allOf:
                      - $ref: "#/components/schemas/Order"
                      - type: object
                        properties:
                          items:
                            type: array
                            items:
                              type: object
                              properties:
                                product_id:
                                  type: integer
                                name:
                                  type: string
                                sku:
                                  type: string
                                quantity:
                                  type: integer
                                subtotal:
                                  type: number
                                total:
                                  type: number
                          notes:
                            type: array
                            items:
                              type: object
                              properties:
                                id:
                                  type: integer
                                content:
                                  type: string
                                date:
                                  type: string
                                  format: date-time
                                author:
                                  type: string
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/orders/standing:
    get:
      tags: [Orders]
      summary: List standing orders
      operationId: listStandingOrders
      description: Auto-replenishment orders with configurable frequency.
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Standing order list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/StandingOrder"
    post:
      tags: [Orders]
      summary: Create a standing order
      operationId: createStandingOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sku, qty, frequency_days]
              properties:
                sku:
                  type: string
                  example: "CAS-B180"
                qty:
                  type: integer
                  minimum: 1
                  example: 48
                frequency_days:
                  type: integer
                  minimum: 1
                  example: 14
                first_run:
                  type: string
                  format: date
                notes:
                  type: string
      responses:
        "201":
          description: Standing order created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  standing_order_id:
                    type: integer

  /v2/orders/standing/{id}:
    put:
      tags: [Orders]
      summary: Update or pause/resume/cancel a standing order
      operationId: updateStandingOrder
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [active, paused, cancelled]
      responses:
        "200":
          description: Standing order updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  standing_order_id:
                    type: integer

  # ═══════════════════════════════════════════════
  # QUOTES
  # ═══════════════════════════════════════════════

  /v2/quotes:
    get:
      tags: [Quotes]
      summary: List quotes
      operationId: listQuotes
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated quote list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Quote"
    post:
      tags: [Quotes]
      summary: Create a quote
      operationId: createQuote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code, items]
              properties:
                customer_code:
                  type: string
                  example: "MTL-0189"
                customer_name:
                  type: string
                valid_until:
                  type: string
                  format: date
                notes:
                  type: string
                items:
                  type: array
                  items:
                    type: object
                    properties:
                      sku:
                        type: string
                        example: "CAS-B180"
                      description:
                        type: string
                      qty:
                        type: number
                        example: 100
                      unit_price:
                        type: number
                        example: 62.00
      responses:
        "201":
          description: Quote created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  quote_id:
                    type: integer
                  quote_no:
                    type: string
                    example: "QT-000142"

  /v2/quotes/{id}:
    get:
      tags: [Quotes]
      summary: Get quote with line items
      operationId: getQuote
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Quote detail with lines
          content:
            application/json:
              schema:
                type: object
                properties:
                  quote:
                    $ref: "#/components/schemas/Quote"
                  lines:
                    type: array
                    items:
                      type: object
                      properties:
                        sku:
                          type: string
                        description:
                          type: string
                        qty:
                          type: number
                        unit_price:
                          type: number
                        line_total:
                          type: number
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Quotes]
      summary: Update quote status or notes
      operationId: updateQuote
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [draft, sent, accepted, rejected, expired, converted]
                notes:
                  type: string
                valid_until:
                  type: string
                  format: date
      responses:
        "200":
          description: Quote updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/quotes/{id}/convert:
    post:
      tags: [Quotes]
      summary: Convert quote to invoice
      operationId: convertQuote
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Quote converted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  quote_id:
                    type: integer
                  status:
                    type: string
                    example: "converted"
        "400":
          description: Quote already converted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ═══════════════════════════════════════════════
  # CLEANING
  # ═══════════════════════════════════════════════

  /v2/cleaning/contracts:
    get:
      tags: [Cleaning]
      summary: List cleaning contracts
      operationId: listCleaningContracts
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, active, paused, completed]
        - name: customer_code
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated contract list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/CleaningContract"
    post:
      tags: [Cleaning]
      summary: Create a cleaning contract
      operationId: createCleaningContract
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code, site_name]
              properties:
                customer_code:
                  type: string
                  example: "LAVAL-0042"
                customer_name:
                  type: string
                site_name:
                  type: string
                  example: "Tour Deloitte - 3e étage"
                site_address:
                  type: string
                  example: "1190 avenue des Canadiens-de-Montréal, Montréal, QC"
                contact_name:
                  type: string
                contact_phone:
                  type: string
                contact_email:
                  type: string
                  format: email
                frequency:
                  type: string
                  enum: [daily, weekly, biweekly, monthly]
                  example: "weekly"
                start_date:
                  type: string
                  format: date
                end_date:
                  type: string
                  format: date
                notes:
                  type: string
                lines:
                  type: array
                  items:
                    type: object
                    properties:
                      service_type:
                        type: string
                        example: "floor_strip_wax"
                      sqft:
                        type: number
                        example: 5000
                      frequency:
                        type: string
                      unit_price:
                        type: number
                        example: 0.12
      responses:
        "201":
          description: Contract created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  contract_id:
                    type: integer

  /v2/cleaning/contracts/{id}:
    get:
      tags: [Cleaning]
      summary: Get cleaning contract with lines
      operationId: getCleaningContract
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Contract detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract:
                    $ref: "#/components/schemas/CleaningContract"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Cleaning]
      summary: Update cleaning contract
      operationId: updateCleaningContract
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                site_name:
                  type: string
                site_address:
                  type: string
                contact_name:
                  type: string
                contact_phone:
                  type: string
                contact_email:
                  type: string
                  format: email
                frequency:
                  type: string
                end_date:
                  type: string
                  format: date
                notes:
                  type: string
      responses:
        "200":
          description: Contract updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/cleaning/contracts/{id}/pause:
    post:
      tags: [Cleaning]
      summary: Pause or resume a cleaning contract
      operationId: pauseResumeCleaningContract
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [pause, resume]
      responses:
        "200":
          description: Contract paused/resumed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  action:
                    type: string

  /v2/cleaning/contracts/{id}/log:
    post:
      tags: [Cleaning]
      summary: Log a cleaning service visit
      operationId: logCleaningService
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                service_date:
                  type: string
                  format: date
                inspector_name:
                  type: string
                  example: "Jean-François Dubois"
                status:
                  type: string
                  example: "completed"
                notes:
                  type: string
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
      responses:
        "201":
          description: Service logged
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  log_id:
                    type: integer

  /v2/cleaning/contracts/{id}/logs:
    get:
      tags: [Cleaning]
      summary: Get service history for a contract
      operationId: getCleaningServiceLogs
      parameters:
        - $ref: "#/components/parameters/id"
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 200
      responses:
        "200":
          description: Service log history
          content:
            application/json:
              schema:
                type: object
                properties:
                  contract_id:
                    type: integer
                  logs:
                    type: array
                    items:
                      $ref: "#/components/schemas/ServiceLog"
                  count:
                    type: integer

  /v2/cleaning/prices:
    get:
      tags: [Cleaning]
      summary: Get cleaning pricing grid
      operationId: getCleaningPrices
      description: Returns all 21 service types with per-sqft pricing.
      responses:
        "200":
          description: Pricing grid
          content:
            application/json:
              schema:
                type: object
                properties:
                  prices:
                    type: object
                  categories:
                    type: object
    put:
      tags: [Cleaning]
      summary: Update cleaning price overrides
      operationId: updateCleaningPrices
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [overrides]
              properties:
                overrides:
                  type: object
                  additionalProperties:
                    type: object
                    properties:
                      price_per_sqft:
                        type: number
                        example: 0.12
      responses:
        "200":
          description: Prices updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  updated:
                    type: integer

  # ═══════════════════════════════════════════════
  # PAYROLL
  # ═══════════════════════════════════════════════

  /v2/payroll/employees:
    get:
      tags: [Payroll]
      summary: List employees
      operationId: listEmployees
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: status
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
          description: Search by name or employee number
      responses:
        "200":
          description: Paginated employee list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Employee"
    post:
      tags: [Payroll]
      summary: Create an employee
      operationId: createEmployee
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [first_name, last_name, pay_rate]
              properties:
                first_name:
                  type: string
                  example: "Marie-Claude"
                last_name:
                  type: string
                  example: "Gagnon"
                email:
                  type: string
                  format: email
                phone:
                  type: string
                hire_date:
                  type: string
                  format: date
                department:
                  type: string
                  example: "Entrepôt"
                position:
                  type: string
                  example: "Manutentionnaire"
                pay_type:
                  type: string
                  enum: [hourly, salary]
                  example: "hourly"
                pay_rate:
                  type: number
                  example: 22.50
                pay_frequency:
                  type: string
                  enum: [weekly, biweekly, semi-monthly, monthly]
                  example: "biweekly"
      responses:
        "201":
          description: Employee created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  employee_id:
                    type: integer
        "422":
          $ref: "#/components/responses/BadRequest"

  /v2/payroll/employees/{id}:
    get:
      tags: [Payroll]
      summary: Get employee by ID
      operationId: getEmployee
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Employee detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Employee"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Payroll]
      summary: Update an employee
      operationId: updateEmployee
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                first_name:
                  type: string
                last_name:
                  type: string
                email:
                  type: string
                phone:
                  type: string
                department:
                  type: string
                position:
                  type: string
                pay_type:
                  type: string
                  enum: [hourly, salary]
                pay_rate:
                  type: number
                pay_frequency:
                  type: string
                status:
                  type: string
                termination_date:
                  type: string
                  format: date
      responses:
        "200":
          description: Employee updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/payroll/timesheets:
    get:
      tags: [Payroll]
      summary: List timesheets
      operationId: listTimesheets
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: employee_id
          in: query
          schema:
            type: integer
        - name: date_from
          in: query
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          schema:
            type: string
            format: date
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, approved, rejected]
      responses:
        "200":
          description: Paginated timesheet list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Timesheet"
    post:
      tags: [Payroll]
      summary: Create a timesheet entry
      operationId: createTimesheet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [employee_id, work_date]
              properties:
                employee_id:
                  type: integer
                work_date:
                  type: string
                  format: date
                regular_hours:
                  type: number
                  example: 8.0
                overtime_hours:
                  type: number
                  example: 0
                holiday_hours:
                  type: number
                  example: 0
                type:
                  type: string
                  example: "regular"
                notes:
                  type: string
      responses:
        "201":
          description: Timesheet created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  timesheet_id:
                    type: integer

  /v2/payroll/timesheets/{id}/approve:
    post:
      tags: [Payroll]
      summary: Approve a timesheet
      operationId: approveTimesheet
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Timesheet approved
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/payroll/runs:
    get:
      tags: [Payroll]
      summary: List pay runs
      operationId: listPayRuns
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Paginated pay run list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/PayRun"
    post:
      tags: [Payroll]
      summary: Create a pay run
      operationId: createPayRun
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [period_start, period_end, pay_date]
              properties:
                period_start:
                  type: string
                  format: date
                  example: "2026-03-25"
                period_end:
                  type: string
                  format: date
                  example: "2026-04-07"
                pay_date:
                  type: string
                  format: date
                  example: "2026-04-10"
                frequency:
                  type: string
                  enum: [weekly, biweekly, semi-monthly, monthly]
                  example: "biweekly"
      responses:
        "201":
          description: Pay run created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  run_id:
                    type: integer

  /v2/payroll/runs/{id}/calculate:
    post:
      tags: [Payroll]
      summary: Calculate pay run deductions
      operationId: calculatePayRun
      description: |
        Calculates all Quebec statutory deductions (RRQ, AE, RQAP),
        federal and provincial taxes, and employer contributions (CPEEP)
        for all employees in the run.
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Calculation complete
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  gross_total:
                    type: number
                  net_total:
                    type: number
                  employer_cost_total:
                    type: number

  /v2/payroll/runs/{id}/approve:
    post:
      tags: [Payroll]
      summary: Approve a pay run
      operationId: approvePayRun
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Pay run approved
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/payroll/runs/{id}/paid:
    post:
      tags: [Payroll]
      summary: Mark pay run as paid
      operationId: markPayRunPaid
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Pay run marked as paid
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/payroll/runs/{id}/stubs:
    get:
      tags: [Payroll]
      summary: Get pay stubs for a run
      operationId: getPayRunStubs
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Pay stubs with employee details
          content:
            application/json:
              schema:
                type: object
                properties:
                  run:
                    $ref: "#/components/schemas/PayRun"
                  stubs:
                    type: array
                    items:
                      $ref: "#/components/schemas/PayStub"
                  count:
                    type: integer
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/payroll/rates:
    get:
      tags: [Payroll]
      summary: Get current Quebec payroll rates
      operationId: getPayrollRates
      description: Returns 2026 statutory rates for RRQ, AE, RQAP, and tax brackets.
      responses:
        "200":
          description: Current payroll rates
          content:
            application/json:
              schema:
                type: object
                properties:
                  year:
                    type: integer
                    example: 2026
                  rrq_rate:
                    type: number
                    example: 0.064
                  ae_rate:
                    type: number
                    example: 0.0158
                  rqap_rate:
                    type: number
                    example: 0.00494

  /v2/payroll/cpeep:
    get:
      tags: [Payroll]
      summary: CPEEP employer contribution report
      operationId: getCpeepReport
      description: Quebec employer contribution (contribution patronale) summary.
      parameters:
        - name: year
          in: query
          schema:
            type: integer
            default: 2026
      responses:
        "200":
          description: CPEEP report
          content:
            application/json:
              schema:
                type: object

  # ═══════════════════════════════════════════════
  # ACCOUNTING
  # ═══════════════════════════════════════════════

  /v2/accounting/ar/aging:
    get:
      tags: [Accounting]
      summary: AR aging report
      operationId: arAging
      parameters:
        - name: customer_code
          in: query
          schema:
            type: string
          description: Filter to a specific customer
      responses:
        "200":
          description: Aging buckets
          content:
            application/json:
              schema:
                type: object
                properties:
                  current:
                    type: number
                  days_30:
                    type: number
                  days_60:
                    type: number
                  days_90:
                    type: number
                  days_120_plus:
                    type: number

  /v2/accounting/ar/clients:
    get:
      tags: [Accounting]
      summary: AR balances by client
      operationId: arClients
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
      responses:
        "200":
          description: Client AR balances
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        customer_code:
                          type: string
                        customer_name:
                          type: string
                        balance:
                          type: number
                        overdue:
                          type: number

  /v2/accounting/ar/payments:
    post:
      tags: [Accounting]
      summary: Record an AR payment
      operationId: arRecordPayment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice_id, amount]
              properties:
                invoice_id:
                  type: integer
                amount:
                  type: number
                  example: 1432.02
                method:
                  type: string
                  example: "virement"
                reference:
                  type: string
      responses:
        "200":
          description: Payment recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/accounting/ar/remind:
    post:
      tags: [Accounting]
      summary: Send AR payment reminder
      operationId: arSendReminder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_code]
              properties:
                customer_code:
                  type: string
                  example: "LAVAL-0042"
      responses:
        "200":
          description: Reminder sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/accounting/ap/invoices:
    get:
      tags: [Accounting]
      summary: List AP invoices
      operationId: apListInvoices
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: vendor
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
        - name: date_from
          in: query
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          schema:
            type: string
            format: date
      responses:
        "200":
          description: Paginated AP invoice list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/APInvoice"
    post:
      tags: [Accounting]
      summary: Create an AP invoice
      operationId: apCreateInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [vendor, invoice_number, total]
              properties:
                vendor:
                  type: string
                  example: "Cascades PRO"
                invoice_number:
                  type: string
                  example: "CASC-2026-8891"
                invoice_date:
                  type: string
                  format: date
                due_date:
                  type: string
                  format: date
                subtotal:
                  type: number
                tps:
                  type: number
                tvq:
                  type: number
                total:
                  type: number
                  example: 9723.11
                notes:
                  type: string
      responses:
        "201":
          description: AP invoice created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  invoice_id:
                    type: integer

  /v2/accounting/ap/aging:
    get:
      tags: [Accounting]
      summary: AP aging report
      operationId: apAging
      responses:
        "200":
          description: AP aging buckets
          content:
            application/json:
              schema:
                type: object
                properties:
                  current:
                    type: number
                  days_30:
                    type: number
                  days_60:
                    type: number
                  days_90:
                    type: number

  /v2/accounting/gl/entries:
    get:
      tags: [Accounting]
      summary: List GL journal entries
      operationId: glListEntries
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: account
          in: query
          schema:
            type: string
          example: "4100"
        - name: date_from
          in: query
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          schema:
            type: string
            format: date
        - name: reference
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated GL entries
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/GLEntry"
    post:
      tags: [Accounting]
      summary: Create a GL journal entry
      operationId: glCreateEntry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account, description]
              properties:
                entry_date:
                  type: string
                  format: date
                account:
                  type: string
                  example: "4100"
                description:
                  type: string
                  example: "Ventes produits sanitaires - avril"
                debit:
                  type: number
                  example: 0
                credit:
                  type: number
                  example: 12500.00
                reference:
                  type: string
      responses:
        "201":
          description: Entry created
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  entry_id:
                    type: integer

  /v2/accounting/gl/trial-balance:
    get:
      tags: [Accounting]
      summary: Trial balance
      operationId: glTrialBalance
      parameters:
        - name: as_of
          in: query
          schema:
            type: string
            format: date
          description: Date for the trial balance (defaults to today)
      responses:
        "200":
          description: Trial balance report
          content:
            application/json:
              schema:
                type: object

  /v2/accounting/gl/chart:
    get:
      tags: [Accounting]
      summary: Chart of accounts
      operationId: glChartOfAccounts
      responses:
        "200":
          description: Account list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        account_number:
                          type: string
                          example: "4100"
                        name:
                          type: string
                          example: "Ventes"
                        type:
                          type: string
                          example: "revenue"

  /v2/accounting/pl:
    get:
      tags: [Accounting]
      summary: Profit and loss statement
      operationId: profitAndLoss
      parameters:
        - name: date_from
          in: query
          schema:
            type: string
            format: date
          description: Start date (defaults to Jan 1 of current year)
        - name: date_to
          in: query
          schema:
            type: string
            format: date
          description: End date (defaults to today)
      responses:
        "200":
          description: P&L report
          content:
            application/json:
              schema:
                type: object
                properties:
                  revenue:
                    type: number
                    example: 245000.00
                  cost_of_goods:
                    type: number
                    example: 142000.00
                  gross_profit:
                    type: number
                    example: 103000.00
                  expenses:
                    type: number
                  net_income:
                    type: number

  # ═══════════════════════════════════════════════
  # ANALYTICS
  # ═══════════════════════════════════════════════

  /v2/analytics/dashboard:
    get:
      tags: [Analytics]
      summary: Executive dashboard KPIs
      operationId: executiveDashboard
      description: Returns ARR, MRR, NRR%, gross margin, and active client count.
      responses:
        "200":
          description: Executive KPIs
          content:
            application/json:
              schema:
                type: object
                properties:
                  arr:
                    type: number
                    example: 1240000.00
                  mrr:
                    type: number
                    example: 103333.33
                  nrr_pct:
                    type: number
                    example: 108.5
                  gross_margin_pct:
                    type: number
                    example: 42.1
                  active_clients:
                    type: integer
                    example: 187

  /v2/analytics/revenue:
    get:
      tags: [Analytics]
      summary: Revenue analytics
      operationId: revenueAnalytics
      parameters:
        - name: months
          in: query
          schema:
            type: integer
            default: 12
            maximum: 60
        - name: customer_code
          in: query
          schema:
            type: string
        - name: group_by
          in: query
          schema:
            type: string
            enum: [day, week, month, quarter]
            default: month
      responses:
        "200":
          description: Revenue breakdown
          content:
            application/json:
              schema:
                type: object

  /v2/analytics/forecast:
    get:
      tags: [Analytics]
      summary: Revenue forecast
      operationId: revenueForecast
      responses:
        "200":
          description: Revenue forecast data
          content:
            application/json:
              schema:
                type: object

  /v2/analytics/demand/{sku}:
    get:
      tags: [Analytics]
      summary: Demand forecast for a SKU
      operationId: demandForecast
      description: Holt-Winters triple exponential smoothing with confidence intervals.
      parameters:
        - $ref: "#/components/parameters/sku_path"
        - name: horizon
          in: query
          schema:
            type: integer
            default: 6
            maximum: 24
          description: Number of months to forecast
      responses:
        "200":
          description: Forecast data
          content:
            application/json:
              schema:
                type: object
                properties:
                  sku:
                    type: string
                  forecast:
                    type: array
                    items:
                      type: object
                      properties:
                        month:
                          type: string
                        predicted_qty:
                          type: number
                        lower_ci:
                          type: number
                        upper_ci:
                          type: number

  /v2/analytics/churn:
    get:
      tags: [Analytics]
      summary: At-risk client churn report
      operationId: churnReport
      parameters:
        - name: threshold
          in: query
          schema:
            type: number
            default: 0.5
          description: Churn risk threshold (0.0 to 1.0)
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
      responses:
        "200":
          description: At-risk clients
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        customer_code:
                          type: string
                        customer_name:
                          type: string
                        churn_score:
                          type: number
                          example: 0.72
                        last_order_days:
                          type: integer
                          example: 89

  /v2/analytics/profits:
    get:
      tags: [Analytics]
      summary: Profit analysis
      operationId: profitAnalysis
      parameters:
        - name: group_by
          in: query
          schema:
            type: string
            enum: [product, client, category]
            default: product
        - name: months
          in: query
          schema:
            type: integer
            default: 12
            maximum: 60
      responses:
        "200":
          description: Profit breakdown
          content:
            application/json:
              schema:
                type: object

  /v2/analytics/growth:
    get:
      tags: [Analytics]
      summary: Growth metrics
      operationId: growthMetrics
      description: YoY growth, client acquisition rate, and revenue velocity.
      responses:
        "200":
          description: Growth metrics
          content:
            application/json:
              schema:
                type: object

  /v2/analytics/bi:
    get:
      tags: [Analytics]
      summary: BI hub — run or list analyses
      operationId: biHub
      description: Without the analysis parameter, returns available analysis types. With it, runs the specified analysis.
      parameters:
        - name: analysis
          in: query
          schema:
            type: string
          description: Analysis name to run (omit to list available analyses)
      responses:
        "200":
          description: Analysis result or available analyses list
          content:
            application/json:
              schema:
                type: object

  # ═══════════════════════════════════════════════
  # AI
  # ═══════════════════════════════════════════════

  /v2/ai/chat:
    post:
      tags: [AI]
      summary: AI conversational analytics (text-to-SQL)
      operationId: aiChat
      description: |
        Send a natural language question and receive SQL-backed answers.
        Rate-limited to 15 requests/min per user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  example: "Quels sont mes 5 meilleurs clients ce mois-ci?"
                conversation_id:
                  type: string
                  description: Continue an existing conversation
                language:
                  type: string
                  enum: [fr, en]
                  default: fr
      responses:
        "200":
          description: AI response
          content:
            application/json:
              schema:
                type: object
                properties:
                  answer:
                    type: string
                  sql_query:
                    type: string
                  data:
                    type: array
                    items: {}
                  conversation_id:
                    type: string

  /v2/ai/enrich:
    post:
      tags: [AI]
      summary: AI product enrichment
      operationId: aiEnrich
      description: |
        Generate or extract product descriptions, categories, and SEO metadata
        using AI or by scraping a supplier URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                product_id:
                  type: integer
                sku:
                  type: string
                  example: "CAS-B180"
                type:
                  type: string
                  enum: [ai, url]
                  default: ai
                url:
                  type: string
                  format: uri
                  description: Supplier URL for URL-based enrichment
      responses:
        "200":
          description: Enrichment result
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  product_id:
                    type: integer
                  enriched_fields:
                    type: array
                    items:
                      type: string

  /v2/ai/recommendations:
    get:
      tags: [AI]
      summary: Proactive AI recommendations
      operationId: aiRecommendations
      description: |
        Returns seasonal, pricing, stockout, and upsell recommendations.
        Filter by customer or get global top recommendations.
      parameters:
        - name: customer_code
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: Recommendation list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                          enum: [seasonal, pricing, stockout, upsell]
                        title:
                          type: string
                        description:
                          type: string
                        customer_code:
                          type: string
                        sku:
                          type: string
                        confidence:
                          type: number

  # ═══════════════════════════════════════════════
  # NOTIFICATIONS
  # ═══════════════════════════════════════════════

  /v2/notifications:
    get:
      tags: [Notifications]
      summary: List notifications
      operationId: listNotifications
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/per_page"
        - name: unread
          in: query
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Notification list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Notification"
                  total:
                    type: integer
                  page:
                    type: integer

  /v2/notifications/mark-read:
    post:
      tags: [Notifications]
      summary: Mark notifications as read
      operationId: markNotificationsRead
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ids:
                  type: array
                  items:
                    type: integer
                  description: Specific notification IDs to mark read
                all:
                  type: boolean
                  description: Set to true to mark all as read
      responses:
        "200":
          description: Notifications marked as read
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /v2/notifications/count:
    get:
      tags: [Notifications]
      summary: Get unread notification count
      operationId: notificationCount
      responses:
        "200":
          description: Unread count
          content:
            application/json:
              schema:
                type: object
                properties:
                  unread:
                    type: integer
                    example: 7
