openapi: 3.1.0
info:
  title: NoteToQuote API
  version: 1.0.0
  description: |
    Backend API for the NoteToQuote Dolibarr module.
    Converts site-visit notes and photos into structured proposal lines using Claude AI.

    All mutating endpoints require HMAC-SHA256 signature in the `X-HMAC-Signature` header
    (hex digest of the raw request body using the shared secret configured in `NOTETOQUOTE_SHARED_SECRET`).
  contact:
    name: lautram.com
    url: https://lautram.com

servers:
  - url: https://api.lautram.com
    description: Hosted backend
  - url: http://localhost:3010
    description: Local development

paths:
  /api/health:
    get:
      summary: Health check
      description: Returns the running status and version of the backend.
      responses:
        '200':
          description: Service is running
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: 1.0.0

  /api/generate:
    post:
      summary: Generate proposal lines from site-visit notes
      description: |
        Sends notes and optional photos to Claude AI. Returns structured proposal lines
        ready for review and insertion into Dolibarr.

        Rate limited: 10 requests per minute per licence key.
      security:
        - hmacSignature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateRequest'
      responses:
        '200':
          description: Lines generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: validation_error
        '401':
          description: Invalid or inactive licence key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: licence_invalid
        '402':
          description: Monthly quota exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: quota_exceeded
        '422':
          description: AI returned an unparseable response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: ai_parse_error
        '429':
          description: Rate limit exceeded for this licence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: rate_limit_licence
        '503':
          description: Anthropic API rate limit or overload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: anthropic_rate_limit

  /api/check-licence:
    post:
      summary: Validate a licence key
      description: |
        Checks that a licence key is valid, active, and has remaining quota.
        Used by the Dolibarr admin configuration page to verify setup.
      security:
        - hmacSignature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [licence_key]
              properties:
                licence_key:
                  type: string
                  pattern: '^LAU-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
                  example: LAU-ABCD-1234-EFGH
      responses:
        '200':
          description: Licence check result
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  plan:
                    type: string
                    example: pro
                  usage_month:
                    type: integer
                    example: 42
                  quota_month:
                    type: integer
                    example: 500
                  reason:
                    type: string
                    description: Present only when valid=false
                    enum: [invalid, inactive, quota_exceeded, ip_mismatch]

components:
  securitySchemes:
    hmacSignature:
      type: apiKey
      in: header
      name: X-HMAC-Signature
      description: |
        HMAC-SHA256 of the raw request body using the shared secret.
        Computed as: hex(hmac_sha256(NOTETOQUOTE_SHARED_SECRET, raw_body))

  schemas:
    CatalogueItem:
      type: object
      required: [ref, label, price, tva_tx]
      properties:
        ref:
          type: string
          description: Product reference used for exact matching in proposal lines
          example: PAINT-INT-1L
        label:
          type: string
          description: Human-readable label used for semantic filtering
          example: Peinture interieure blanche 1L
        price:
          type: number
          format: float
          description: Unit price excluding tax
          example: 12.50
        tva_tx:
          type: number
          format: float
          description: VAT rate in percent
          example: 20

    ProposalLine:
      type: object
      properties:
        ordre:
          type: integer
          description: Display rank in the proposal
          example: 1
        localisation:
          type: string
          description: Location within the property (e.g. "SDB r+1", "cuisine RDC")
          example: Salle de bain etage
        designation:
          type: string
          description: Short line title (max 80 chars)
          example: Remplacement joint silicone baignoire
        detail:
          type: string
          description: Full description for the client
          example: Depose de l ancien joint et application d un joint silicone sanitaire neuf.
        type_badge:
          type: string
          enum: [forfait_reparation, fourniture_pose, visite_technique]
          example: forfait_reparation
        quantite:
          type: number
          example: 1
        unite:
          type: string
          example: forfait
        prix_ht:
          type: number
          format: float
          description: Unit price excluding tax. 0 when flag_incomplet=true or offert=true.
          example: 85.00
        product_ref:
          type: string
          nullable: true
          description: Catalogue reference if matched, null for free-form lines
          example: null
        offert:
          type: boolean
          description: True for complimentary items (visite_technique)
          example: false
        flag_incomplet:
          type: boolean
          description: True when no catalogue price found — user must fill manually
          example: false
        match_score:
          type: integer
          nullable: true
          description: |
            Catalogue match confidence: 100 = exact ref found in catalogue,
            0 = ref returned by AI but not in catalogue (possible hallucination),
            null = free-form line (product_ref is null)
          example: 100

    GenerateRequest:
      type: object
      required: [licence_key, notes]
      properties:
        licence_key:
          type: string
          pattern: '^LAU-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
          example: LAU-ABCD-1234-EFGH
        notes:
          type: string
          minLength: 10
          maxLength: 8000
          description: Site-visit notes in free text (French recommended)
          example: "Salle de bain: joint baignoire decollé a refaire. Cuisine: robinet qui fuit."
        photos:
          type: array
          maxItems: 10
          items:
            type: string
            description: Base64-encoded image with data URI prefix (data:image/jpeg;base64,...)
          default: []
        catalogue:
          type: array
          maxItems: 2000
          items:
            $ref: '#/components/schemas/CatalogueItem'
          description: |
            Product catalogue for price matching. If omitted, all lines are free-form.
            The backend filters to the 50 most relevant items using TF-IDF scoring on the notes.
          default: []
        type_bien:
          type: string
          enum: [residentiel, bureau, autre]
          default: residentiel
          description: Property type — determines VAT rate (residentiel=10%, others=20%)
        verbosity:
          type: string
          enum: [concise, detailed]
          default: concise
          description: |
            concise: 1-sentence detail per line.
            detailed: 3-5 sentence professional description per line.

    GenerateResponse:
      type: object
      properties:
        lignes:
          type: array
          items:
            $ref: '#/components/schemas/ProposalLine'
        prestation_forfaitaire:
          type: boolean
          description: True when the AI determined the work is best billed as a single lump sum
        forfait_description:
          type: string
          nullable: true
        forfait_prix_ht:
          type: number
          nullable: true

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          enum:
            - validation_error
            - licence_invalid
            - quota_exceeded
            - rate_limit_licence
            - ai_parse_error
            - anthropic_rate_limit
            - internal_error
