openapi: 3.1.0
info:
  title: Wattson API
  version: 1.0.0
  summary: Estrazione bollette e simulazioni offerte.
  description: |
    API server-to-server per integrare Wattson in un gestionale o in un backend web.
    La Simulation API e disponibile esclusivamente sui piani Company ed Enterprise.

    Il flusso Simulation API e asincrono: crea un job, carica i file sugli URL
    firmati, invia il job e poi attendi il webhook o interroga lo stato. Non
    esporre mai una API key Wattson nel browser o in un'app mobile.

    Dal 18/10/2026 le API contratti (`/contracts`) sono ritirate e rispondono
    `410 ENDPOINT_RETIRED`.
  contact:
    name: Wattson
    url: https://www.getwattson.it
servers:
  - url: https://api.getwattson.it/v1
    description: Produzione
security:
  - bearerAuth: []
tags:
  - name: Offers
    description: Offerte abilitate per l'integrazione.
  - name: Simulation jobs
    description: Estrazione e simulazione asincrona di una o piu pratiche.
paths:
  /offers:
    get:
      operationId: listOffers
      tags: [Offers]
      summary: Elenca le offerte utilizzabili
      description: |
        Restituisce soltanto le offerte attive e abilitate per l'integrazione
        associata alla API key. Usa sempre l'`id` restituito qui nei job: non
        salvare nel codice ID copiati da email o documenti commerciali.
      parameters:
        - name: service
          in: query
          required: false
          description: Filtra per servizio.
          schema:
            $ref: '#/components/schemas/Service'
        - name: customer_segment
          in: query
          required: false
          description: Filtra per segmento cliente.
          schema:
            type: string
            enum: [casa, business]
      responses:
        '200':
          description: Elenco delle offerte disponibili.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfferListResponse'
              examples:
                ondaCoin:
                  value:
                    offers:
                      - id: 11111111-1111-4111-8111-111111111111
                        service: luce
                        name: LUCE Coin
                        supplier: Onda Piu
                        supplier_offer_code: AV0COI0D0626P30B
                        arera_offer_code: 025990ENVFL01XX0AV0COI0D0626P30B
                        customer_segment: casa
                        price_type: indicizzato
                        pricing:
                          fixed_unit_price: null
                          index_spread: 0.0302
                          annual_commercial_fee: 144
                          annual_bill_bonus: 6
                          first_year_discount: 0
                        valid_from: '2026-07-01'
                        valid_to: '2026-12-31'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/BillingNotEntitled'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'
  /simulation-jobs:
    post:
      operationId: createSimulationJob
      tags: [Simulation jobs]
      summary: Crea un job e gli URL di upload
      description: |
        Prepara da 1 a 10 pratiche. Ogni elemento di `bills` rappresenta una
        pratica cliente e puo contenere da 1 a 4 file. La stessa
        `Idempotency-Key`, con lo stesso body, restituisce lo stesso job senza
        duplicare il lavoro o il consumo di crediti; la stessa chiave con un
        body diverso restituisce `409`. Ogni integrazione puo mantenere al
        massimo 10 job `waiting_for_upload` non scaduti. Se un replay trova un
        job ancora `waiting_for_upload` con la finestra di upload scaduta,
        restituisce `410 JOB_EXPIRED`: crea un nuovo job con una nuova chiave.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSimulationJobRequest'
            examples:
              luceAndGas:
                value:
                  external_reference: onda-pratica-7841
                  bills:
                    - reference: cliente-rossi
                      offers:
                        luce:
                          - 11111111-1111-4111-8111-111111111111
                        gas:
                          - 22222222-2222-4222-8222-222222222222
                      files:
                        - name: bolletta-dual.pdf
                          content_type: application/pdf
                          size_bytes: 483021
                          page_count: 6
      responses:
        '200':
          description: |
            Replay idempotente di un job gia creato con lo stesso body. Oltre
            la retention puo restituire soltanto il tombstone tecnico, con i
            riferimenti esterni minimizzati a `null`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSimulationJobResponse'
        '201':
          description: Job creato. Caricare ogni file sul relativo URL firmato.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSimulationJobResponse'
              examples:
                created:
                  value:
                    job:
                      id: 5755422e-cdee-4f16-ad2f-7941b553480c
                      external_reference: onda-pratica-7841
                      status: waiting_for_upload
                      created_at: '2026-09-04T09:15:00Z'
                      upload_expires_at: '2026-09-04T11:15:00Z'
                      submitted_at: null
                      started_at: null
                      completed_at: null
                      expires_at: '2026-12-03T09:15:00Z'
                      updated_at: '2026-09-04T09:15:00Z'
                    items:
                      - id: 059eab40-a24a-4d5a-b012-45a70d5d2f5d
                        reference: cliente-rossi
                        status: waiting_for_upload
                        uploads:
                          - file_id: 2506678d-037a-472d-99d5-39d35ed7b781
                            name: bolletta-dual.pdf
                            method: PUT
                            upload_url: https://example.supabase.co/storage/v1/upload/sign/...
                            headers:
                              Content-Type: application/pdf
                            expires_in: 7200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/BillingNotEntitled'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '410':
          $ref: '#/components/responses/JobExpired'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/PendingJobLimitReached'
        '500':
          $ref: '#/components/responses/InternalError'
  /simulation-jobs/{job_id}/submit:
    post:
      operationId: submitSimulationJob
      tags: [Simulation jobs]
      summary: Avvia estrazione e simulazione
      description: |
        Verifica che tutti i file dichiarati siano stati caricati e mette in
        coda il lavoro. La chiamata e idempotente: ripeterla non crea nuove
        estrazioni e non consuma altri crediti.
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '202':
          description: Job accettato per l'elaborazione.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitSimulationJobResponse'
              examples:
                accepted:
                  value:
                    job:
                      id: 5755422e-cdee-4f16-ad2f-7941b553480c
                      external_reference: onda-pratica-7841
                      status: queued
                      submitted_at: '2026-09-04T09:16:12Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/JobExpired'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /simulation-jobs/{job_id}:
    get:
      operationId: getSimulationJob
      tags: [Simulation jobs]
      summary: Legge stato e risultati
      description: |
        Fonte di verita del job. Interroga questo endpoint anche dopo un
        webhook, o se un webhook non arriva. I dati sono disponibili per 90
        giorni dalla creazione del job.
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Stato corrente, estrazione e simulazioni disponibili.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationJobResponse'
              examples:
                completed:
                  $ref: '#/components/examples/CompletedSimulationJob'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/BillingNotEntitled'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/JobExpired'
        '500':
          $ref: '#/components/responses/InternalError'
webhooks:
  simulationJobTerminal:
    post:
      operationId: simulationJobTerminalWebhook
      tags: [Simulation jobs]
      summary: Job arrivato a uno stato terminale
      description: |
        Wattson invia `simulation.job.completed`,
        `simulation.job.completed_with_errors` oppure `simulation.job.failed`
        all'URL configurato per l'integrazione.
        Verifica `X-Wattson-Signature` sul body grezzo prima di decodificare il
        JSON. Considera `X-Wattson-Event-Id` una chiave di idempotenza: le
        consegne possono essere ripetute o arrivare fuori ordine. Rispondi con
        un codice `2xx`: qualsiasi risposta non-`2xx`, inclusi i `4xx`, viene
        ritentata da Wattson.
      parameters:
        - name: X-Wattson-Event-Id
          in: header
          required: true
          schema:
            type: string
            format: uuid
        - name: X-Wattson-Timestamp
          in: header
          required: true
          description: Unix timestamp in secondi usato nella firma.
          schema:
            type: integer
            format: int64
        - name: X-Wattson-Signature
          in: header
          required: true
          description: '`v1=<hex>`, HMAC-SHA256 di `<timestamp>.<rawBody>`.'
          schema:
            type: string
            pattern: '^v1=[a-f0-9]{64}$'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulationWebhookEvent'
      responses:
        '204':
          description: Evento accettato.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Wattson API key
      description: API key con prefisso `wattson_sk_`, da usare soltanto server-to-server.
  headers:
    RequestId:
      description: Identificatore della richiesta, utile per assistenza e log.
      schema:
        type: string
        format: uuid
    IdempotentReplayed:
      description: Indica che la stessa Idempotency-Key e lo stesso body avevano gia creato il job.
      schema:
        type: string
        const: 'true'
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Chiave unica per la creazione logica del job. Conservala insieme al riferimento pratica.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        example: onda-pratica-7841-v1
    JobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: Richiesta non valida.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: INPUT_INVALID
              message: Il job deve contenere da 1 a 10 pratiche.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
              details:
                field: bills
    Unauthorized:
      description: API key mancante, revocata o non valida.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: AUTH_INVALID
              message: API key non valida.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    BillingNotEntitled:
      description: La Simulation API e disponibile soltanto sui piani Company ed Enterprise.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: BILLING_NOT_ENTITLED
              message: Piano o stato di fatturazione non abilitato.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    PaymentRequired:
      description: Piano Company/Enterprise non disponibile o quota di simulazione esaurita.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            billingNotEntitled:
              value:
                error:
                  code: BILLING_NOT_ENTITLED
                  message: Piano o stato di fatturazione non abilitato.
                  request_id: 617c52a8-cac0-491d-8765-e138287fffc9
            quotaExceeded:
              value:
                error:
                  code: QUOTA_EXCEEDED
                  message: Quota di simulazioni esaurita.
                  request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    Forbidden:
      description: La API key non ha lo scope richiesto.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Risorsa non trovata nel tenant della API key.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    JobExpired:
      description: |
        Il job non e piu utilizzabile: la finestra di upload e scaduta oppure
        il risultato ha superato la retention ed e rimasto solo il tombstone tecnico.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: JOB_EXPIRED
              message: Il job e scaduto.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    Conflict:
      description: Stato della risorsa incompatibile con l'operazione.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    IdempotencyConflict:
      description: La Idempotency-Key e gia associata a un body differente.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: IDEMPOTENCY_CONFLICT
              message: Idempotency-Key gia usata con una richiesta diversa.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    PayloadTooLarge:
      description: Uno dei limiti di file o pratica e stato superato.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UnprocessableEntity:
      description: File, offerta o pratica non elaborabile.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    PendingJobLimitReached:
      description: L'integrazione ha gia 10 job non scaduti in attesa di upload.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          description: Secondi minimi da attendere prima di ritentare.
          schema:
            type: integer
            minimum: 1
          example: 60
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: PENDING_JOB_LIMIT_REACHED
              message: Troppi job in attesa di upload per questa integrazione. Completa un job esistente o riprova piu tardi.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    RateLimited:
      description: Troppe estrazioni contemporanee; applicare backoff prima di ritentare.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: RATE_LIMITED
              message: Troppe estrazioni in corso. Riprova piu tardi.
              request_id: 617c52a8-cac0-491d-8765-e138287fffc9
    InternalError:
      description: Errore interno.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    Service:
      type: string
      enum: [luce, gas]
    Offer:
      type: object
      additionalProperties: false
      required: [id, external_code, supplier_offer_code, arera_offer_code, name, supplier, service, customer_segment, price_type, tariff_structure, duration_months, valid_from, valid_to, pricing, disclaimer]
      properties:
        id:
          type: string
          format: uuid
        external_code:
          type: [string, 'null']
        supplier_offer_code:
          type: [string, 'null']
        arera_offer_code:
          type: [string, 'null']
        name:
          type: string
        supplier:
          type: [string, 'null']
        service:
          $ref: '#/components/schemas/Service'
        customer_segment:
          type: string
          enum: [casa, business, entrambi]
        price_type:
          type: string
          enum: [fisso, indicizzato]
        tariff_structure:
          type: [string, 'null']
        duration_months:
          type: [integer, 'null']
        valid_from:
          type: [string, 'null']
          format: date
        valid_to:
          type: [string, 'null']
          format: date
        pricing:
          $ref: '#/components/schemas/OfferPricing'
        disclaimer:
          type: [string, 'null']
    OfferPricing:
      type: object
      additionalProperties: false
      required: [fixed_unit_price, index_spread, annual_commercial_fee, annual_bill_bonus, first_year_discount]
      properties:
        fixed_unit_price:
          type: [number, 'null']
        index_spread:
          type: [number, 'null']
        annual_commercial_fee:
          type: [number, 'null']
        annual_bill_bonus:
          type: [number, 'null']
        first_year_discount:
          type: [number, 'null']
    OfferListResponse:
      type: object
      additionalProperties: false
      required: [offers]
      properties:
        offers:
          type: array
          items:
            $ref: '#/components/schemas/Offer'
    CreateSimulationJobRequest:
      type: object
      additionalProperties: false
      required: [external_reference, bills]
      properties:
        external_reference:
          type: string
          minLength: 1
          maxLength: 160
          description: Riferimento del job nel sistema chiamante.
        bills:
          type: array
          minItems: 1
          maxItems: 10
          items:
            $ref: '#/components/schemas/BillInput'
    BillInput:
      type: object
      additionalProperties: false
      required: [reference, offers, files]
      properties:
        reference:
          type: string
          minLength: 1
          maxLength: 160
          description: Riferimento univoco della pratica nel sistema chiamante.
        offers:
          $ref: '#/components/schemas/OfferSelection'
        files:
          type: array
          minItems: 1
          maxItems: 4
          description: Massimo 30 MiB e 35 pagine complessive per pratica.
          items:
            $ref: '#/components/schemas/FileInput'
    OfferSelection:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        luce:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            format: uuid
        gas:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            type: string
            format: uuid
      anyOf:
        - required: [luce]
        - required: [gas]
    FileInput:
      type: object
      additionalProperties: false
      required: [name, content_type, size_bytes, page_count]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 160
        content_type:
          type: string
          enum: [application/pdf, image/jpeg, image/png, image/webp, image/gif]
        size_bytes:
          type: integer
          minimum: 1
          maximum: 15728640
        page_count:
          type: integer
          minimum: 1
          maximum: 35
    CreateSimulationJobResponse:
      type: object
      additionalProperties: false
      required: [job, items]
      properties:
        job:
          $ref: '#/components/schemas/SimulationJobSummary'
        items:
          type: array
          items:
            $ref: '#/components/schemas/SimulationUploadItem'
    SimulationJobSummary:
      type: object
      additionalProperties: false
      required: [id, external_reference, status, created_at, upload_expires_at, submitted_at, started_at, completed_at, expires_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        external_reference:
          description: Puo essere null nel replay di un job ridotto a tombstone oltre la retention.
          type: [string, 'null']
        status:
          $ref: '#/components/schemas/JobStatus'
        created_at:
          type: string
          format: date-time
        upload_expires_at:
          type: [string, 'null']
          format: date-time
        submitted_at:
          type: [string, 'null']
          format: date-time
        started_at:
          type: [string, 'null']
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        expires_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SubmitSimulationJobResponse:
      type: object
      additionalProperties: false
      required: [job]
      properties:
        job:
          type: object
          additionalProperties: false
          required: [id, external_reference, status, submitted_at]
          properties:
            id:
              type: string
              format: uuid
            external_reference:
              type: string
            status:
              $ref: '#/components/schemas/JobStatus'
            submitted_at:
              type: string
              format: date-time
    SimulationUploadItem:
      type: object
      additionalProperties: false
      required: [id, reference, status, uploads]
      properties:
        id:
          type: string
          format: uuid
        reference:
          description: Puo essere null nel replay di un item ridotto a tombstone oltre la retention.
          type: [string, 'null']
        status:
          $ref: '#/components/schemas/ItemStatus'
        uploads:
          type: array
          items:
            $ref: '#/components/schemas/SignedUpload'
    SignedUpload:
      type: object
      additionalProperties: false
      required: [file_id, name, method, upload_url, headers, expires_in]
      properties:
        file_id:
          type: string
          format: uuid
        name:
          type: string
        method:
          type: string
          const: PUT
        upload_url:
          type: string
          format: uri
        headers:
          type: object
          additionalProperties:
            type: string
          example:
            Content-Type: application/pdf
        expires_in:
          type: integer
          description: Durata dell'URL in secondi.
    JobStatus:
      type: string
      enum: [waiting_for_upload, queued, processing, completed, completed_with_errors, failed, expired]
    ItemStatus:
      type: string
      enum:
        - waiting_for_upload
        - extracting
        - simulation_queued
        - simulating
        - completed
        - needs_review
        - failed
        - quota_exceeded
    SimulationJobResponse:
      type: object
      additionalProperties: false
      required: [job]
      properties:
        job:
          $ref: '#/components/schemas/SimulationJob'
    SimulationJob:
      type: object
      additionalProperties: false
      required: [id, external_reference, status, created_at, upload_expires_at, submitted_at, started_at, completed_at, expires_at, updated_at, items]
      properties:
        id:
          type: string
          format: uuid
        external_reference:
          type: string
        status:
          $ref: '#/components/schemas/JobStatus'
        created_at:
          type: string
          format: date-time
        upload_expires_at:
          type: [string, 'null']
          format: date-time
        submitted_at:
          type: [string, 'null']
          format: date-time
        started_at:
          type: [string, 'null']
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        expires_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/SimulationItem'
    SimulationItem:
      type: object
      additionalProperties: false
      required: [id, reference, status, selected_scope, attempt_count, extraction, simulation, review_required, errors, created_at, started_at, completed_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        reference:
          type: string
        status:
          $ref: '#/components/schemas/ItemStatus'
        selected_scope:
          type: string
          enum: [luce, gas, dual]
        attempt_count:
          type: integer
          minimum: 0
        extraction:
          oneOf:
            - $ref: '#/components/schemas/ExtractionResult'
            - type: 'null'
        simulation:
          oneOf:
            - $ref: '#/components/schemas/SimulationOutput'
            - type: 'null'
        review_required:
          type: boolean
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ItemError'
        created_at:
          type: string
          format: date-time
        started_at:
          type: [string, 'null']
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: string
          format: date-time
    ExtractionResult:
      type: object
      additionalProperties: false
      required: [detected_scope, customer_segment, shared_customer, billing_profile, supplies, confidence]
      properties:
        detected_scope:
          type: string
          enum: [luce, gas, luce_gas]
        customer_segment:
          type: string
          enum: [casa, business]
        shared_customer:
          $ref: '#/components/schemas/ExtractedCustomer'
        billing_profile:
          type: object
          additionalProperties: true
        supplies:
          type: array
          items:
            $ref: '#/components/schemas/ExtractedSupply'
        confidence:
          type: string
          enum: [alta, media, bassa, non_simulabile]
        extraction_notes:
          type: string
        missing_fields:
          type: object
          additionalProperties: true
        total_pages:
          type: integer
          minimum: 1
        file_count:
          type: integer
          minimum: 1
    ExtractedCustomer:
      type: object
      additionalProperties: true
      properties:
        customer_full_name:
          type: [string, 'null']
        customer_fiscal_code:
          type: [string, 'null']
        customer_vat_number:
          type: [string, 'null']
        customer_email:
          type: [string, 'null']
        customer_phone:
          type: [string, 'null']
    ExtractedSupply:
      type: object
      additionalProperties: true
      required: [service_type]
      properties:
        service_type:
          $ref: '#/components/schemas/Service'
        pod:
          type: [string, 'null']
        pdr:
          type: [string, 'null']
        original_provider:
          type: [string, 'null']
        billing_period_start:
          type: [string, 'null']
          format: date
        billing_period_end:
          type: [string, 'null']
          format: date
        total_consumption_kwh:
          type: [number, 'null']
        total_consumption_smc:
          type: [number, 'null']
        original_total:
          type: [number, 'null']
    SimulationOutput:
      type: object
      additionalProperties: false
      required: [results, selected, totals]
      properties:
        results:
          type: array
          description: Tutte le offerte simulate, per fornitura.
          items:
            $ref: '#/components/schemas/SimulationResult'
        selected:
          type: array
          description: Migliore offerta selezionata per ciascuna fornitura.
          items:
            $ref: '#/components/schemas/SimulationResult'
        totals:
          $ref: '#/components/schemas/SimulationTotals'
    SimulationResult:
      type: object
      additionalProperties: true
      required: [supply_index, service, offer_id, offer_name, original_total_period_eur, simulated_total_period_eur, annual_original_eur, annual_simulated_eur, annual_expected_savings_eur, savings_percentage, is_convenient]
      properties:
        supply_index:
          type: integer
          minimum: 0
        service:
          $ref: '#/components/schemas/Service'
        pod:
          type: [string, 'null']
        pdr:
          type: [string, 'null']
        offer_id:
          type: string
          format: uuid
        offer_name:
          type: string
        supplier_label:
          type: [string, 'null']
        original_total_period_eur:
          type: number
        simulated_total_period_eur:
          type: number
        annual_original_eur:
          type: number
        annual_simulated_eur:
          type: number
        annual_expected_savings_eur:
          type: number
        savings_percentage:
          type: number
        is_convenient:
          type: boolean
        period_breakdown:
          type: object
          additionalProperties: true
        annual_breakdown:
          type: object
          additionalProperties: true
        cost_breakdown:
          type: object
          additionalProperties: true
        tax:
          type: object
          additionalProperties: true
        projection_12m:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [month_index, simulated_total]
            properties:
              month_index:
                type: integer
                minimum: 1
                maximum: 12
              simulated_total:
                type: number
    SimulationTotals:
      type: object
      additionalProperties: false
      required: [annual_original_eur, annual_simulated_eur, annual_expected_savings_eur]
      properties:
        annual_original_eur:
          type: number
        annual_simulated_eur:
          type: number
        annual_expected_savings_eur:
          type: number
    ItemError:
      type: object
      additionalProperties: false
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
        service:
          oneOf:
            - $ref: '#/components/schemas/Service'
            - type: 'null'
        supply_index:
          type: [integer, 'null']
          minimum: 0
        offer_id:
          description: ID dell'offerta esclusa, valorizzato per `OFFER_NOT_ELIGIBLE` quando disponibile.
          type: [string, 'null']
          format: uuid
    SimulationWebhookEvent:
      type: object
      additionalProperties: false
      required: [id, type, created_at, data]
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [simulation.job.completed, simulation.job.completed_with_errors, simulation.job.failed]
        created_at:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: false
          required: [job_id, status, external_reference]
          properties:
            job_id:
              type: string
              format: uuid
            status:
              type: string
              enum: [completed, completed_with_errors, failed]
            external_reference:
              type: string
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message, request_id]
          properties:
            code:
              type: string
              example: INPUT_INVALID
            message:
              type: string
            request_id:
              type: string
              format: uuid
            details:
              description: Contesto opzionale, senza dati interni o credenziali.
  examples:
    CompletedSimulationJob:
      value:
        job:
          id: 5755422e-cdee-4f16-ad2f-7941b553480c
          external_reference: onda-pratica-7841
          status: completed
          created_at: '2026-09-04T09:15:00Z'
          upload_expires_at: '2026-09-04T11:15:00Z'
          submitted_at: '2026-09-04T09:16:12Z'
          started_at: '2026-09-04T09:16:13Z'
          completed_at: '2026-09-04T09:18:40Z'
          expires_at: '2026-12-03T09:15:00Z'
          updated_at: '2026-09-04T09:18:40Z'
          items:
            - id: 059eab40-a24a-4d5a-b012-45a70d5d2f5d
              reference: cliente-rossi
              status: completed
              selected_scope: luce
              attempt_count: 1
              review_required: false
              extraction:
                detected_scope: luce
                customer_segment: casa
                shared_customer:
                  customer_full_name: Mario Rossi
                  customer_fiscal_code: RSSMRA80A01H501U
                billing_profile: {}
                confidence: alta
                supplies:
                  - service_type: luce
                    pod: IT001E12345678
                    pdr: null
                    original_provider: Fornitore attuale
                    billing_period_start: '2026-06-01'
                    billing_period_end: '2026-07-31'
                    total_consumption_kwh: 512
                    total_consumption_smc: null
                    original_total: 168.4
              simulation:
                results:
                  - supply_index: 0
                    service: luce
                    pod: IT001E12345678
                    offer_id: 11111111-1111-4111-8111-111111111111
                    offer_name: LUCE Coin
                    supplier_label: Onda Piu
                    original_total_period_eur: 168.4
                    simulated_total_period_eur: 143.8
                    annual_original_eur: 1010.4
                    annual_simulated_eur: 862.8
                    annual_expected_savings_eur: 147.6
                    savings_percentage: 14.61
                    is_convenient: true
                selected:
                  - supply_index: 0
                    service: luce
                    pod: IT001E12345678
                    offer_id: 11111111-1111-4111-8111-111111111111
                    offer_name: LUCE Coin
                    supplier_label: Onda Piu
                    original_total_period_eur: 168.4
                    simulated_total_period_eur: 143.8
                    annual_original_eur: 1010.4
                    annual_simulated_eur: 862.8
                    annual_expected_savings_eur: 147.6
                    savings_percentage: 14.61
                    is_convenient: true
                totals:
                  annual_original_eur: 1010.4
                  annual_simulated_eur: 862.8
                  annual_expected_savings_eur: 147.6
              errors: []
              created_at: '2026-09-04T09:15:00Z'
              started_at: '2026-09-04T09:16:13Z'
              completed_at: '2026-09-04T09:18:40Z'
              updated_at: '2026-09-04T09:18:40Z'
