{
  "openapi": "3.1.0",
  "info": {
    "title": "Base9 CRM API",
    "version": "1.0.0",
    "summary": "API multi-tenant per gestione anagrafica clienti, POD/PDR, contratti e regole pricing nel settore energia",
    "description": "API RESTful per la piattaforma Base9 CRM. Multi-tenant con isolamento database-per-tenant.\nStyle envelope Stripe-like: ogni risorsa singola in `{object, id, request_id, data}`, ogni lista in `{object: \"list\", item_object, data, has_more, next_cursor}`.\n\n## Autenticazione\n\n- **API key personale** (`dmk_*`) per integratori server-to-server — header `Authorization: Bearer dmk_...`; legata a un singolo tenant, scope `r`/`rw`.\n- **JWT** (identità unificata multi-persona) per utenti/frontend — header `Authorization: Bearer <id_token>`. Il tenant attivo si seleziona con l'header `X-Tenant-Id`.\n\nGli endpoint dati richiedono lo scope `risorsa:azione` (es. `clienti:write`); l'API key usa il suo scope r/rw, l'admin del tenant bypassa. Dettagli nelle guide *Autenticazione* e *RBAC — ruoli, scope e permessi*.\n\n## Errori\n\nTutti gli errori seguono lo schema Stripe-like: `{ error: { type, code, message, param?, request_id, doc_url } }`. I `type`/`code` sono macchina-stabili in inglese; `message` è localizzato in italiano.\n\n- `invalid_request_error` → 400 (body malformato, campi mancanti, validazione)\n- `authentication_error` → 401 (token mancante o invalido)\n- `authorization_error` → 403 (manca permesso/scope)\n- `not_found_error` → 404 (risorsa inesistente o soft-deleted)\n- `conflict_error` → 409 (stato in conflitto, es. duplicato UNIQUE)\n- `rate_limit_error` → 429 (riservato)\n- `api_error` → 500 (errore interno)\n\n## Date\n\nISO 8601 in UTC con suffisso `Z` (timestamp `2026-03-24T21:27:09Z`; date pure `2026-03-24`). Parsare in UTC e convertire al fuso utente solo per la visualizzazione.\n\n## Paginazione\n\nCursor-based: `?limit=25&starting_after=<last_id>` (oppure `ending_before`). Default `limit=25`, max `100` (valori maggiori troncati). La response include `has_more` e `next_cursor`; ordinamento predefinito `created_at DESC, id DESC`. Iterare finché `has_more` è `false`.\n\n## Convenzioni di design (chiavi e nullable)\n\n- **Chiavi primarie**: anagrafiche root API-facing in `TEXT` (`clienti.id` `C…`, `agenti.id` `A…`); entità operative/documentali in `BIGSERIAL` (`contratti.id`, `fatture.id`, `punti_fornitura.id`). Join sempre tra tipi uguali, nessun UUID.\n- **Nullable intenzionali**: fatture senza contratto (autofattura, nota di credito, acconti); POD senza intestatario corrente (relazione POD↔cliente storicizzata in `intestazioni_pf`).\n- **Audit**: tracciato lato applicativo (attore, azione, IP sulle scritture); billing e `contratti` hanno anche `creato_da`/`aggiornato_da` (email staff).\n\n## Cancellazione (soft-delete)\n\n`DELETE /v1/<resource>/{id}` esegue **soft-delete** (`deleted_at` valorizzato; la riga sparisce da list/get). Ritorna `204` se cancellata (idempotente: una seconda DELETE → `404`), `404` se inesistente o già cancellata, `409` se ha dipendenze attive. Nel `409`, `error.details.blockers` elenca ogni record bloccante con id e identificativi. Per i parent (`cliente`, `contratto`, `punto_fornitura`) è disponibile `?cascade=true` che soft-deleta l'intero sottoalbero bottom-up e ritorna un `action_result` con il riepilogo; le risorse foglia (`cte`, `intestazione`, `gruppo`) non accettano cascade.\n\n## Limiti tecnici e dati massivi\n\n- Response singolo statement: **1 MB** hard (limite Aurora Data API); Lambda 6 MB; statement SQL max 45 s.\n- Nested aggregations nei detail endpoint: **cap 100 elementi** (usare `has_more` + endpoint paginato dedicato).\n- L'API CRUD non è pensata per export/reporting massivi: per export totali (>100k record), reporting analitico o bulk import usare pipeline dedicate (job async + S3, Athena/Glue), non l'iterazione degli endpoint CRUD.\n- Nessun rate limit attualmente applicato.\n\n## Versioning & headers\n\nVersione via path `/v1/...`; i breaking change andranno su `/v2` con le versioni precedenti attive per almeno 6 mesi. Le route senza prefisso `/v1` sono la **v0 legacy** (frontend interno) in deprecazione. Ogni risposta include l'header `X-Request-Id` (da citare nei ticket di supporto).\n\n## Il database in pausa\n\nIl database delle misure si mette **in pausa** quando nessuno lo usa, per non costare: la prima richiesta dopo la pausa lo riaccende e ci mette qualche secondo. Se la riaccensione non finisce entro il tempo utile, la risposta e' **503** con codice `database_in_risveglio` e l'intestazione `Retry-After: 10`.\n\n**Non e' un guasto e non si e' perso nulla**: la stessa identica richiesta, ripetuta dopo una decina di secondi, funziona. Un client che integra queste API dovrebbe trattare questo 503 come un invito a riprovare, non come un errore da segnalare.",
    "contact": {
      "name": "Base9 Support",
      "email": "support@base9.app"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.base9.app",
      "description": "Production (eu-south-1 Milano)"
    }
  ],
  "tags": [
    {
      "name": "Clienti",
      "description": "Anagrafica clienti — 3 specializzazioni (Azienda / Persona Fisica / Ditta Individuale) con vincoli UNIQUE cross-tabella su CF/PIVA."
    },
    {
      "name": "Punti di fornitura",
      "description": "POD (energia elettrica) e PDR (gas). Entità fisica del distributore con codice UNIQUE tenant-wide e intestazione storicizzata."
    },
    {
      "name": "Intestazioni",
      "description": "Storia delle relazioni POD↔Cliente (`intestazioni_pf`). Create automaticamente dalle domain action voltura/switching/cessazione."
    },
    {
      "name": "Contratti",
      "description": "Condizioni Generali di Fornitura (CGF) firmate dal cliente — documento commerciale multi-commodity (EE + GAS) con N corrispettivi."
    },
    {
      "name": "Gruppi di fatturazione",
      "description": "Livello intermedio tra CGF e CTE per raggruppamento bollette (singola / multisito / personalizzato)."
    },
    {
      "name": "CGF anagrafica",
      "description": "Anagrafica versionata della CGF (separata dai contratti via migration 019). Endpoint `/v1/cgf/*`."
    },
    {
      "name": "CTE",
      "description": "Catalog di offerte commerciali templates (post-019). Condivisibili tra clienti, con prezzi per fascia."
    },
    {
      "name": "Offerta Commerciale",
      "description": "Istanza runtime per singolo POD (`offerta_commerciale`). Firmata, vigente fino a `data_fine`."
    },
    {
      "name": "Master data",
      "description": "Lookup tables read-only (commodities, opzioni tariffarie, fasce, modalità pagamento, ecc.) per popolare i campi normalizzati."
    },
    {
      "name": "Geo",
      "description": "Dati geografici read-only (regioni/province/comuni/CAP/nazioni). Cacheable 24h."
    },
    {
      "name": "Zone geografiche",
      "description": "Catalog di zone (composizione miste regione+provincia+comune con include/exclude) usate dalle regole di pricing. 5 zone ARERA pre-seedate."
    },
    {
      "name": "Utenti",
      "description": "Gestione utenti del tenant (frontend Cognito). Endpoint accessibile **solo via API key**."
    },
    {
      "name": "Agenti",
      "description": "Anagrafica della rete commerciale (`agenti` + specializzazioni PF/giuridico/DI + indirizzi/contatti).\nOrg-chart vivo a superiori multipli (N:M, storicizzato) via `/v1/agenti/{id}/superiori`.\n"
    },
    {
      "name": "Rete commerciale",
      "description": "Livelli gerarchici (`agenti_livelli`, LIVELLO 0..N pre-seedati) e attribuzione per-contratto\n(snapshot congelato `agenti_contratti`, read-only). Base per il futuro modulo provvigioni.\n"
    },
    {
      "name": "Portale",
      "description": "Area riservata self-service (`GET /v1/me/*`): l'utente autenticato (JWT) legge SOLO le proprie risorse — il `resource_id` è preso dal token. Provisioning e permessi si gestiscono con l'API admin grant (tag Identità, `/v1/admin/users*`)."
    },
    {
      "name": "Fatturazione",
      "description": "Billing tenant-side (API key `dmk_*` / staff). Gestione fatture e relativi pagamenti,\nrighe e scadenzario. Numerazione automatica `AAAA/NNNNNN` all'emissione, stati governati\n(bozza→emessa→pagata/scaduta), calcolo IVA multi-aliquota, audit `creato_da`/`aggiornato_da`.\nGli stessi dati alimentano il cruscotto agente (`/v1/me/agente/*`).\n"
    },
    {
      "name": "Auth",
      "description": "Autenticazione headless (login, MFA TOTP, refresh, password) — modello identità unificato multi-persona"
    },
    {
      "name": "Identità",
      "description": "Contesto identità/personas dell'utente autenticato e gestione grant (admin)"
    },
    {
      "name": "RBAC",
      "description": "Gestione accessi: ruoli (tipi-utenza), utenti, scope. Header **X-Tenant-Id** per selezionare il tenant attivo (identità multi-tenant). Richiede scope users:*/roles:* (o admin)."
    },
    {
      "name": "Metering · Ingest",
      "description": "Come le misure entrano: caricamento manuale, scarico dal SII, rielaborazione di un periodo."
    },
    {
      "name": "Metering · Consultazione",
      "description": "Copertura, consumi fatturabili, dettaglio giornaliero, curva quartoraria, quadratura e conguagli."
    },
    {
      "name": "Metering · Rettifiche",
      "description": "Correzioni e immissioni manuali dell'operatore. Non toccano il dato originale del SII e sono reversibili."
    },
    {
      "name": "Metering · Export",
      "description": "Estrazioni CSV. Oltre i 3 MB, o con formato=link, il file viene scritto su S3 e si riceve un collegamento temporaneo."
    },
    {
      "name": "Metering · Configurazione",
      "description": "Modalita di ingest e credenziali SII del tenant. Richiede il ruolo admin."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "BearerJWT": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dmk_*",
        "description": "API key personale tipo `dmk_xxxx`. Header: `Authorization: Bearer dmk_xxxx`.\nLe API key sono legate a un singolo tenant e a un set di permessi (`r` o `rw`).\n"
      },
      "BearerJWT": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT Cognito per gli utenti del frontend. Contiene `custom:tenant_id`, `custom:role`, `email`.\nHeader: `Authorization: Bearer eyJ...`.\n"
      },
      "PortalJWT": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT utente ottenuto da `POST /v1/auth/login`. Usato dalle route `/v1/me/*`: personas e resource id sono nel token, isolamento per risorsa garantito."
      },
      "jwt": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key personale dmk_ (header Authorization Bearer dmk_...)"
      }
    },
    "parameters": {
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Numero max di elementi per pagina (default 25, max 100).",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "StartingAfter": {
        "name": "starting_after",
        "in": "query",
        "description": "Cursor per la pagina successiva (id dell'ultimo elemento ricevuto).",
        "required": false,
        "schema": {
          "type": "string"
        }
      },
      "EndingBefore": {
        "name": "ending_before",
        "in": "query",
        "description": "Cursor inverso.",
        "required": false,
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validazione fallita (missing_field, invalid_value, invalid_cursor, ecc.)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Token mancante o invalido",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Scope mancante",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "example": "authorization_error"
                    },
                    "code": {
                      "type": "string",
                      "example": "permission_denied"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Risorsa non trovata o soft-deleted",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Conflict (duplicato / risorsa in uso / vincolo violato)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NoContent": {
        "description": "Operazione eseguita, no body"
      },
      "DatabaseInRisveglio": {
        "description": "Il database era in pausa e si sta riaccendendo: riprovare fra ~10 secondi",
        "headers": {
          "Retry-After": {
            "description": "secondi da attendere prima di riprovare",
            "schema": {
              "type": "integer",
              "example": 10
            }
          }
        },
        "content": {
          "application/json": {
            "example": {
              "error": {
                "type": "api_error",
                "code": "database_in_risveglio",
                "message": "Il database era in pausa e si sta riaccendendo. Riprova fra qualche secondo: nessun dato è andato perso."
              }
            }
          }
        }
      },
      "MeteringNonAttivo": {
        "description": "Il modulo metering non e' stato attivato per il tenant: il suo database non esiste ancora. Non e' un guasto: il modulo va attivato per il tenant prima dell'uso.",
        "content": {
          "application/json": {
            "example": {
              "error": {
                "type": "not_found_error",
                "code": "metering_non_attivo",
                "message": "Il modulo metering non e' attivo per questo tenant"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "type",
              "code",
              "message",
              "request_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "api_error",
                  "authentication_error",
                  "authorization_error",
                  "invalid_request_error",
                  "not_found_error",
                  "conflict_error",
                  "rate_limit_error",
                  "idempotency_error"
                ]
              },
              "code": {
                "type": "string",
                "description": "Codice machine-readable stabile (es. `missing_field`)."
              },
              "message": {
                "type": "string",
                "description": "Testo localizzato (italiano)."
              },
              "param": {
                "type": "string"
              },
              "request_id": {
                "type": "string",
                "example": "req_abc123"
              },
              "doc_url": {
                "type": "string",
                "format": "uri"
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Payload aggiuntivo (es. `blockers`, `cliente_esistente`, `intestazione_attiva`)."
              }
            }
          }
        }
      },
      "ListEnvelope": {
        "type": "object",
        "required": [
          "object",
          "item_object",
          "data",
          "has_more"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "list"
            ]
          },
          "item_object": {
            "type": "string"
          },
          "data": {
            "type": "array",
            "items": {}
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "ResourceEnvelope": {
        "type": "object",
        "required": [
          "object",
          "id",
          "data",
          "request_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "description": "Tipo della risorsa (es. agente, cliente, me)"
          },
          "id": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "description": "Payload della risorsa"
          }
        }
      },
      "PortalAccessResult": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "action_result"
            ]
          },
          "action": {
            "type": "string",
            "enum": [
              "portal_access_granted"
            ]
          },
          "request_id": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "properties": {
              "resource_type": {
                "type": "string",
                "enum": [
                  "agente",
                  "cliente"
                ]
              },
              "resource_id": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "portal_stato": {
                "type": "string",
                "enum": [
                  "invitato"
                ]
              }
            }
          }
        }
      },
      "ActionResult": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "action_result"
            ]
          },
          "action": {
            "type": "string"
          },
          "request_id": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "ClienteAzienda": {
        "type": "object",
        "properties": {
          "ragione_sociale": {
            "type": "string",
            "example": "ESEMPIO SRL"
          },
          "codice_fiscale": {
            "type": "string",
            "example": "12345678901"
          },
          "partita_iva": {
            "type": "string",
            "example": "12345678901"
          },
          "codice_ateco": {
            "type": "string",
            "example": "35.11.00"
          },
          "tipo_societa": {
            "type": "string",
            "example": "SRL"
          },
          "codice_cig": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ClientePersonaFisica": {
        "type": "object",
        "properties": {
          "nome": {
            "type": "string",
            "example": "Mario"
          },
          "cognome": {
            "type": "string",
            "example": "Rossi"
          },
          "codice_fiscale": {
            "type": "string",
            "example": "RSSMRA80A01H501Z"
          },
          "data_nascita": {
            "type": "string",
            "format": "date"
          },
          "stato_nascita": {
            "type": "string",
            "example": "IT"
          },
          "comune_nascita": {
            "type": "string",
            "nullable": true
          },
          "comune_nascita_nome": {
            "type": "string",
            "example": "Roma"
          }
        }
      },
      "ClienteDittaIndividuale": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ClientePersonaFisica"
          },
          {
            "type": "object",
            "required": [
              "ragione_sociale",
              "codice_fiscale",
              "partita_iva"
            ],
            "properties": {
              "ragione_sociale": {
                "type": "string",
                "description": "Denominazione/insegna della ditta individuale — **obbligatoria**. nome e cognome del titolare sono invece facoltativi.",
                "example": "Ditta Rossi"
              },
              "partita_iva": {
                "type": "string",
                "example": "01234567890"
              }
            }
          }
        ]
      },
      "Indirizzo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true
          },
          "tipo": {
            "type": "string",
            "enum": [
              "SEDE_LEGALE",
              "RESIDENZA",
              "OPERATIVA",
              "SPEDIZIONE",
              "FORNITURA",
              "DOMICILIO",
              "ALTRA"
            ]
          },
          "toponimo": {
            "type": "string",
            "nullable": true
          },
          "indirizzo": {
            "type": "string"
          },
          "civico": {
            "type": "string"
          },
          "cap": {
            "type": "string"
          },
          "comune": {
            "type": "string",
            "nullable": true,
            "description": "Codice ISTAT comune"
          },
          "comune_nome": {
            "type": "string"
          },
          "provincia": {
            "type": "string",
            "example": "RM"
          },
          "codice_istat": {
            "type": "string",
            "nullable": true
          },
          "nazione": {
            "type": "string",
            "default": "IT"
          },
          "is_primary": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "Contatto": {
        "type": "object",
        "properties": {
          "tipo_contatto": {
            "type": "string",
            "example": "EMAIL_PRINCIPALE",
            "description": "Lookup `tipo_contatto`: EMAIL_PRINCIPALE | EMAIL_PEC | TEL_FISSO | TEL_CELLULARE | FAX | WHATSAPP"
          },
          "valore": {
            "type": "string",
            "example": "info@TENANT_NAME.it"
          },
          "is_primary": {
            "type": "boolean",
            "default": false
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Cliente": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "C8F6189ED"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "AZIENDA",
              "PERSONA_FISICA",
              "DITTA_INDIVIDUALE",
              "CONDOMINIO",
              "PUBBLICA_ADMIN",
              "ALTRI_ENTI"
            ]
          },
          "stato": {
            "type": "string",
            "enum": [
              "lead",
              "attivo",
              "sospeso",
              "cessato",
              "cancellato"
            ]
          },
          "piano": {
            "type": "string",
            "example": "starter"
          },
          "revenue": {
            "type": "integer",
            "example": 100000
          },
          "codice_sdi": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "codice_cliente": {
            "type": "string",
            "nullable": true,
            "description": "Codice cliente tenant-facing (identificativo di business, distinto dall'id interno). UNIQUE per tenant quando valorizzato → un doppione restituisce 409 `codice_cliente_already_exists`.",
            "example": "CLI-000123"
          },
          "origine_esterna": {
            "type": "string",
            "nullable": true,
            "description": "Sistema di provenienza in caso di migrazione/import da applicazioni esterne (es. \"gestionale_x\", \"legacy_mysql\"). Campo di supporto facoltativo.",
            "example": "gestionale_legacy"
          },
          "id_esterno": {
            "type": "string",
            "nullable": true,
            "description": "Chiave del record nel sistema di origine. La coppia (origine_esterna, id_esterno) è UNIQUE per tenant → consente import idempotenti e de-duplicazione. Un doppione restituisce 409 `riferimento_esterno_already_exists`.",
            "example": "44718"
          },
          "azienda": {
            "description": "Dettaglio dell'entità giuridica. Usato — sia in scrittura che in lettura — per i `tipo` **AZIENDA, CONDOMINIO, PUBBLICA_ADMIN, ALTRI_ENTI** (tutti persone giuridiche, memorizzate insieme).",
            "allOf": [
              {
                "$ref": "#/components/schemas/ClienteAzienda"
              }
            ],
            "nullable": true
          },
          "persona_fisica": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClientePersonaFisica"
              }
            ],
            "nullable": true
          },
          "ditta_individuale": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClienteDittaIndividuale"
              }
            ],
            "nullable": true
          },
          "indirizzi": {
            "type": "array",
            "minItems": 1,
            "description": "In **creazione** è obbligatorio **almeno un indirizzo** con `indirizzo` valorizzato: senza, la POST risponde `400`. La creazione è **atomica** (cliente + anagrafica + indirizzi in un'unica transazione): se un indirizzo è invalido non viene creato alcun cliente.",
            "items": {
              "$ref": "#/components/schemas/Indirizzo"
            }
          },
          "contatti": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Contatto"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          }
        }
      },
      "PuntoFornitura": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 38
          },
          "cliente_id": {
            "type": "string",
            "example": "C8F6189ED"
          },
          "codice": {
            "type": "string",
            "example": "IT001E556678904",
            "description": "POD per EE, PDR per GAS — UNIQUE tenant-wide"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "EE",
              "GAS"
            ]
          },
          "stato": {
            "type": "string",
            "enum": [
              "attivo",
              "sospeso",
              "cessato"
            ]
          },
          "distributore": {
            "type": "string",
            "nullable": true
          },
          "potenza_impegnata": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "toponimo": {
            "type": "string",
            "nullable": true
          },
          "indirizzo": {
            "type": "string",
            "nullable": true
          },
          "civico": {
            "type": "string",
            "nullable": true
          },
          "cap": {
            "type": "string",
            "nullable": true
          },
          "comune": {
            "type": "string",
            "nullable": true
          },
          "comune_nome": {
            "type": "string",
            "nullable": true
          },
          "provincia": {
            "type": "string",
            "nullable": true
          },
          "nazione": {
            "type": "string",
            "default": "IT"
          },
          "motivo_inizio": {
            "type": "string",
            "example": "ATTIVAZIONE"
          },
          "data_inizio": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "DatiTecniciEE": {
        "type": "object",
        "properties": {
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "tipologia_uso": {
            "type": "string",
            "enum": [
              "DOMESTICO",
              "ALTRI_USI",
              "ILLUMINAZIONE_PUBBLICA",
              "POMPE_CALORE",
              "VEICOLI_ELETTRICI"
            ]
          },
          "tensione": {
            "type": "string",
            "enum": [
              "BT",
              "MT",
              "AT",
              "AAT"
            ]
          },
          "volt": {
            "type": "integer",
            "example": 230
          },
          "opzione_tariffaria": {
            "type": "string",
            "description": "Codici ARERA: BTA1..BTA6 | MTA1..MTA3 | AT | AAT | BTIP | MTIP | BTVE | TDR | TDNR"
          },
          "residente": {
            "type": "boolean"
          },
          "potenza_impegnata": {
            "type": "number",
            "format": "float"
          },
          "potenza_disponibile": {
            "type": "number",
            "format": "float"
          },
          "consumo_annuo_stimato": {
            "type": "integer"
          }
        }
      },
      "DatiTecniciGAS": {
        "type": "object",
        "properties": {
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "tipologia_uso": {
            "type": "string",
            "enum": [
              "DOMESTICO",
              "ALTRI_USI",
              "ILLUMINAZIONE_PUBBLICA",
              "CONDOMINIO_USO_DOMESTICO",
              "SERVIZIO_PUBBLICO"
            ]
          },
          "categoria_uso": {
            "type": "string",
            "enum": [
              "C1",
              "C2",
              "C3",
              "C4",
              "C5"
            ]
          },
          "residente": {
            "type": "boolean"
          },
          "classe_misuratore": {
            "type": "string",
            "description": "G1.6 | G2.5 | G4 | … | G40000 (23 valori)"
          },
          "matricola_misuratore": {
            "type": "string"
          },
          "telegestione": {
            "type": "boolean"
          },
          "disalimentabile": {
            "type": "boolean"
          },
          "codice_remi": {
            "type": "string"
          },
          "accessibilita_misuratore": {
            "type": "string",
            "enum": [
              "TOTALE",
              "PARZIALE",
              "NON_ACCESSIBILE"
            ]
          },
          "consumo_annuo": {
            "type": "integer"
          }
        }
      },
      "Intestazione": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "punto_fornitura": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "codice": {
                "type": "string"
              },
              "tipo": {
                "type": "string",
                "enum": [
                  "EE",
                  "GAS"
                ]
              }
            }
          },
          "cliente": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "nome_display": {
                "type": "string"
              }
            }
          },
          "contratto_id": {
            "type": "integer",
            "nullable": true
          },
          "tipo_relazione": {
            "type": "string",
            "enum": [
              "INTESTATARIO",
              "COINTESTATARIO",
              "PAGATORE",
              "UTILIZZATORE"
            ]
          },
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "attiva": {
            "type": "boolean",
            "readOnly": true
          },
          "motivo_inizio": {
            "type": "string",
            "enum": [
              "ATTIVAZIONE",
              "SWITCHING",
              "VOLTURA",
              "SUBENTRO",
              "MIGRAZIONE"
            ]
          },
          "motivo_fine": {
            "type": "string",
            "enum": [
              "SWITCHING_OUT",
              "VOLTURA",
              "CESSAZIONE",
              "DISDETTA"
            ],
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Corrispettivo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "codice_voce": {
            "type": "string",
            "example": "QFISSA"
          },
          "nome_voce": {
            "type": "string",
            "example": "Quota fissa"
          },
          "unita_misura": {
            "type": "string",
            "example": "€/anno"
          },
          "importo": {
            "type": "number",
            "format": "float",
            "example": 60.0
          },
          "commodity": {
            "type": "string",
            "enum": [
              "EE",
              "GAS",
              "TELCO"
            ],
            "nullable": true
          },
          "sotto_categoria": {
            "type": "string",
            "nullable": true
          },
          "punto_fornitura_id": {
            "type": "integer",
            "nullable": true
          },
          "consumo_min": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "consumo_max": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "ordine": {
            "type": "integer",
            "default": 0
          }
        }
      },
      "Contratto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "codice": {
            "type": "string",
            "example": "CGF-2026-000001",
            "readOnly": true
          },
          "cliente_id": {
            "type": "string"
          },
          "data_firma": {
            "type": "string",
            "format": "date"
          },
          "modalita_firma": {
            "type": "string",
            "enum": [
              "OTP",
              "CARTACEO",
              "REC"
            ]
          },
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "vigente": {
            "type": "boolean",
            "readOnly": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "corrispettivi": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Corrispettivo"
            }
          },
          "cascade": {
            "type": "object",
            "nullable": true,
            "description": "Solo se POST con `punto_fornitura_id` → restituisce id delle entità create in cascata",
            "properties": {
              "gruppo_fatturazione_id": {
                "type": "integer"
              },
              "cte_id": {
                "type": "integer"
              },
              "cte_codice": {
                "type": "string"
              },
              "punto_fornitura": {
                "type": "object"
              }
            }
          }
        }
      },
      "GruppoFatturazione": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "contratto_id": {
            "type": "integer"
          },
          "nome": {
            "type": "string",
            "example": "Sedi Nord"
          },
          "descrizione": {
            "type": "string",
            "nullable": true
          },
          "modalita_raggruppamento": {
            "type": "string",
            "enum": [
              "SINGOLA",
              "MULTISITO",
              "PERSONALIZZATO"
            ]
          },
          "periodo_fatt_cadenza": {
            "type": "string",
            "enum": [
              "MENSILE",
              "BIMESTRALE",
              "TRIMESTRALE",
              "SEMESTRALE"
            ]
          },
          "periodo_fatt_tipo": {
            "type": "string",
            "enum": [
              "SOLARE",
              "DA_INIZIO_FORNITURA"
            ]
          }
        }
      },
      "Cgf": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "contratto_id": {
            "type": "integer"
          },
          "contratto_codice": {
            "type": "string",
            "readOnly": true
          },
          "cliente_id": {
            "type": "string",
            "readOnly": true
          },
          "cliente_nome": {
            "type": "string",
            "readOnly": true
          },
          "codice_cgf": {
            "type": "string"
          },
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "vigente": {
            "type": "boolean",
            "readOnly": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "num_corrispettivi": {
            "type": "integer",
            "readOnly": true
          },
          "corrispettivi": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "tipo": {
                  "type": "string",
                  "enum": [
                    "cgf"
                  ]
                },
                "codice_fatturazione": {
                  "type": "string"
                },
                "nome_corrispettivo": {
                  "type": "string"
                },
                "commodity_code": {
                  "type": "string"
                },
                "unita_misura_code": {
                  "type": "string"
                },
                "corrispettivo_unitario": {
                  "type": "number",
                  "format": "float"
                },
                "data_inizio": {
                  "type": "string",
                  "format": "date"
                },
                "data_fine": {
                  "type": "string",
                  "format": "date",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "Cte": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "codice_offerta": {
            "type": "string",
            "example": "OFF_EE_FISSA_2026"
          },
          "nome_offerta": {
            "type": "string",
            "example": "EE Fissa 2026"
          },
          "descrizione": {
            "type": "string",
            "nullable": true
          },
          "commodity_code": {
            "type": "string",
            "example": "EE"
          },
          "tipologia_offerta_code": {
            "type": "string",
            "enum": [
              "FISSA",
              "VARIABILE",
              "INDICIZZATA"
            ]
          },
          "indice_variabile_code": {
            "type": "string",
            "nullable": true
          },
          "tariffazione_code": {
            "type": "string",
            "example": "BIORARIA"
          },
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "stato": {
            "type": "boolean",
            "description": "true = attivo"
          },
          "corrispettivi": {
            "type": "array",
            "items": {},
            "description": "Corrispettivi tipo='cte'"
          },
          "offerte_prezzi": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fascia_code": {
                  "type": "string",
                  "example": "F1"
                },
                "prezzo": {
                  "type": "number",
                  "format": "float"
                },
                "unita_misura_code": {
                  "type": "string",
                  "example": "EUR_KWH"
                }
              }
            }
          }
        }
      },
      "OffertaCommerciale": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "pf_id": {
            "type": "integer"
          },
          "pf_codice": {
            "type": "string",
            "readOnly": true
          },
          "pf_tipo": {
            "type": "string",
            "enum": [
              "EE",
              "GAS"
            ],
            "readOnly": true
          },
          "contratto_id": {
            "type": "integer",
            "nullable": true
          },
          "contratto_codice": {
            "type": "string",
            "readOnly": true,
            "nullable": true
          },
          "cliente_id": {
            "type": "string",
            "readOnly": true
          },
          "cte_id": {
            "type": "integer",
            "nullable": true
          },
          "cte_codice_offerta": {
            "type": "string",
            "readOnly": true,
            "nullable": true
          },
          "cte_nome_offerta": {
            "type": "string",
            "readOnly": true,
            "nullable": true
          },
          "commodity_code": {
            "type": "string",
            "readOnly": true
          },
          "tariffazione_code": {
            "type": "string",
            "readOnly": true
          },
          "data_firma": {
            "type": "string",
            "format": "date"
          },
          "modalita_firma": {
            "type": "string",
            "enum": [
              "OTP",
              "CARTACEO",
              "REC"
            ]
          },
          "esecuzione_anticipata": {
            "type": "boolean",
            "default": false
          },
          "data_inizio": {
            "type": "string",
            "format": "date"
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "vigente": {
            "type": "boolean",
            "readOnly": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Regione": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "regione"
            ]
          },
          "codice_regione": {
            "type": "string",
            "example": "03"
          },
          "denominazione": {
            "type": "string",
            "example": "Lombardia"
          },
          "ripartizione": {
            "type": "string",
            "example": "Nord-Ovest"
          },
          "tipologia": {
            "type": "string",
            "example": "statuto ordinario"
          }
        }
      },
      "Provincia": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "provincia"
            ]
          },
          "sigla": {
            "type": "string",
            "example": "MI"
          },
          "denominazione": {
            "type": "string",
            "example": "Milano"
          },
          "codice_regione": {
            "type": "string"
          },
          "regione_denominazione": {
            "type": "string",
            "readOnly": true
          }
        }
      },
      "Comune": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "comune"
            ]
          },
          "codice_istat": {
            "type": "string",
            "example": "015146"
          },
          "denominazione": {
            "type": "string",
            "example": "Milano"
          },
          "sigla_provincia": {
            "type": "string",
            "example": "MI"
          },
          "codice_belfiore": {
            "type": "string",
            "example": "F205"
          },
          "flag_capoluogo": {
            "type": "boolean"
          },
          "lat": {
            "type": "number",
            "format": "float",
            "example": 45.4642
          },
          "lon": {
            "type": "number",
            "format": "float",
            "example": 9.19
          },
          "provincia_denominazione": {
            "type": "string",
            "readOnly": true
          },
          "regione_denominazione": {
            "type": "string",
            "readOnly": true
          },
          "cap": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Solo nel detail"
          }
        }
      },
      "Nazione": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "nazione"
            ]
          },
          "sigla": {
            "type": "string",
            "example": "IT"
          },
          "denominazione": {
            "type": "string",
            "example": "ITALIA"
          },
          "cittadinanza": {
            "type": "string",
            "example": "ITALIANA"
          },
          "codice_belfiore": {
            "type": "string",
            "example": "Z000"
          }
        }
      },
      "ZonaArea": {
        "type": "object",
        "required": [
          "tipo_match"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "zona_area"
            ]
          },
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "livello": {
            "type": "string",
            "enum": [
              "regione",
              "provincia",
              "comune"
            ],
            "readOnly": true
          },
          "codice_regione": {
            "type": "string",
            "nullable": true
          },
          "sigla_provincia": {
            "type": "string",
            "nullable": true
          },
          "codice_istat": {
            "type": "string",
            "nullable": true
          },
          "tipo_match": {
            "type": "string",
            "enum": [
              "include",
              "exclude"
            ],
            "default": "include"
          },
          "denominazione": {
            "type": "string",
            "readOnly": true
          }
        }
      },
      "ZonaGeografica": {
        "type": "object",
        "required": [
          "codice",
          "nome"
        ],
        "properties": {
          "codice": {
            "type": "string",
            "pattern": "^[A-Z0-9_]{2,40}$",
            "example": "NORD_OVEST"
          },
          "nome": {
            "type": "string",
            "example": "Nord Ovest"
          },
          "descrizione": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "aree": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ZonaArea"
            }
          },
          "aree_count": {
            "type": "integer",
            "readOnly": true
          }
        }
      },
      "Utente": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "utente"
            ]
          },
          "id": {
            "type": "string",
            "description": "Email (lowercased) come identifier"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "ruolo": {
            "type": "string",
            "enum": [
              "admin",
              "user"
            ]
          },
          "stato": {
            "type": "string",
            "enum": [
              "attivo",
              "invitato",
              "sospeso"
            ]
          },
          "permissions": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "r",
                "rw"
              ]
            },
            "example": {
              "customers": "rw",
              "jobs": "rw",
              "billing": "r"
            }
          },
          "moduli": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "modulo": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "tier": {
                  "type": "string",
                  "enum": [
                    "core",
                    "premium"
                  ]
                },
                "accesso": {
                  "type": "string",
                  "enum": [
                    "r",
                    "rw"
                  ],
                  "nullable": true
                },
                "lettura": {
                  "type": "boolean"
                },
                "scrittura": {
                  "type": "boolean"
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AgenteLivello": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "example": "agente_livello"
          },
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "codice": {
            "type": "string",
            "example": "L3"
          },
          "nome": {
            "type": "string",
            "example": "Agente"
          },
          "ordine": {
            "type": "integer",
            "description": "0 = vertice della gerarchia",
            "example": 3
          },
          "descrizione": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          }
        }
      },
      "AgenteIndirizzo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "tipo": {
            "type": "string",
            "default": "SEDE"
          },
          "toponimo": {
            "type": "string",
            "nullable": true
          },
          "indirizzo": {
            "type": "string"
          },
          "civico": {
            "type": "string",
            "nullable": true
          },
          "cap": {
            "type": "string",
            "nullable": true
          },
          "comune": {
            "type": "string",
            "nullable": true
          },
          "provincia": {
            "type": "string",
            "nullable": true
          },
          "codice_istat": {
            "type": "string",
            "nullable": true
          },
          "nazione": {
            "type": "string",
            "default": "IT"
          },
          "is_primary": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "AgenteContatto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "tipo_contatto": {
            "type": "string",
            "example": "EMAIL"
          },
          "valore": {
            "type": "string"
          },
          "is_primary": {
            "type": "boolean",
            "default": false
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "readOnly": true
          }
        }
      },
      "AgenteAnagrafica": {
        "type": "object",
        "description": "Campi dipendenti da `tipo`. PF: nome/cognome/codice_fiscale. GIURIDICO: ragione_sociale/partita_iva. DI: nome/cognome/codice_fiscale/partita_iva.",
        "properties": {
          "nome": {
            "type": "string"
          },
          "cognome": {
            "type": "string"
          },
          "codice_fiscale": {
            "type": "string"
          },
          "data_nascita": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "comune_nascita": {
            "type": "string",
            "nullable": true
          },
          "ragione_sociale": {
            "type": "string"
          },
          "partita_iva": {
            "type": "string"
          },
          "codice_ateco": {
            "type": "string",
            "nullable": true
          },
          "tipo_societa": {
            "type": "string",
            "nullable": true
          },
          "denominazione": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Agente": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "example": "agente"
          },
          "id": {
            "type": "string",
            "readOnly": true,
            "example": "A470D92FA"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "PF",
              "GIURIDICO",
              "DI"
            ]
          },
          "codice": {
            "type": "string",
            "nullable": true,
            "example": "AGT-001"
          },
          "stato": {
            "type": "string",
            "enum": [
              "attivo",
              "sospeso",
              "cessato"
            ]
          },
          "mercato": {
            "type": "string",
            "enum": [
              "EE",
              "GAS",
              "ENTRAMBI"
            ]
          },
          "livello": {
            "type": "string",
            "description": "codice livello",
            "example": "L3"
          },
          "livello_id": {
            "type": "integer"
          },
          "livello_nome": {
            "type": "string",
            "readOnly": true
          },
          "data_ingresso": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "iban": {
            "type": "string",
            "nullable": true
          },
          "utente_cognito_sub": {
            "type": "string",
            "nullable": true,
            "description": "Link opzionale all'utente Cognito (area riservata)"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "anagrafica": {
            "$ref": "#/components/schemas/AgenteAnagrafica"
          },
          "indirizzi": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgenteIndirizzo"
            }
          },
          "contatti": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgenteContatto"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          }
        }
      },
      "AgenteCreate": {
        "type": "object",
        "required": [
          "tipo",
          "livello",
          "anagrafica"
        ],
        "properties": {
          "tipo": {
            "type": "string",
            "enum": [
              "PF",
              "GIURIDICO",
              "DI"
            ]
          },
          "livello": {
            "type": "string",
            "description": "codice livello (alternativa a livello_id)",
            "example": "L3"
          },
          "livello_id": {
            "type": "integer"
          },
          "codice": {
            "type": "string"
          },
          "stato": {
            "type": "string",
            "enum": [
              "attivo",
              "sospeso",
              "cessato"
            ],
            "default": "attivo"
          },
          "mercato": {
            "type": "string",
            "enum": [
              "EE",
              "GAS",
              "ENTRAMBI"
            ],
            "default": "ENTRAMBI"
          },
          "data_ingresso": {
            "type": "string",
            "format": "date"
          },
          "iban": {
            "type": "string"
          },
          "utente_cognito_sub": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "anagrafica": {
            "$ref": "#/components/schemas/AgenteAnagrafica"
          },
          "indirizzi": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgenteIndirizzo"
            }
          },
          "contatti": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgenteContatto"
            }
          }
        }
      },
      "AgenteSuperiore": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "example": "agente_superiore"
          },
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "superiore_id": {
            "type": "string"
          },
          "superiore_codice": {
            "type": "string",
            "readOnly": true,
            "nullable": true
          },
          "is_diretto": {
            "type": "boolean",
            "default": false
          },
          "data_inizio": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "is_current": {
            "type": "boolean"
          }
        }
      },
      "AgenteContrattoLink": {
        "type": "object",
        "description": "Snapshot read-only della catena commerciale congelata su un contratto.",
        "properties": {
          "object": {
            "type": "string",
            "example": "agente_contratto"
          },
          "id": {
            "type": "integer",
            "readOnly": true
          },
          "contratto_id": {
            "type": "integer"
          },
          "livello_id": {
            "type": "integer"
          },
          "livello": {
            "type": "string"
          },
          "data_inizio": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "data_fine": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "is_current": {
            "type": "boolean"
          }
        }
      },
      "PodCoverage": {
        "type": "object",
        "properties": {
          "pod": {
            "type": "string"
          },
          "giorni_attesi": {
            "type": "integer"
          },
          "giorni_coperti": {
            "type": "integer"
          },
          "anomalie": {
            "type": "integer"
          },
          "celle": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "g": {
                  "type": "integer",
                  "description": "giorno del mese"
                },
                "stato": {
                  "type": "string",
                  "enum": [
                    "best",
                    "valid",
                    "invalid",
                    "absent",
                    "awaited",
                    "interpolated",
                    null
                  ]
                }
              }
            }
          }
        }
      },
      "ConsumoMensile": {
        "type": "object",
        "properties": {
          "mese": {
            "type": "string",
            "example": "2026-08"
          },
          "kwh_f1": {
            "type": "number"
          },
          "kwh_f2": {
            "type": "number"
          },
          "kwh_f3": {
            "type": "number"
          },
          "kwh_tot": {
            "type": "number"
          },
          "metodo": {
            "type": "string",
            "enum": [
              "curva",
              "sintetico",
              "sintetico_attivazione"
            ]
          },
          "ka": {
            "type": "number"
          },
          "giorni_coperti": {
            "type": "integer"
          }
        }
      },
      "OverrideInput": {
        "type": "object",
        "required": [
          "pod",
          "data",
          "valore",
          "tipo"
        ],
        "properties": {
          "pod": {
            "type": "string"
          },
          "data": {
            "type": "string",
            "format": "date"
          },
          "fascia": {
            "type": "string",
            "enum": [
              "F1",
              "F2",
              "F3",
              "mono"
            ],
            "nullable": true
          },
          "valore": {
            "type": "number"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "rettifica",
              "immissione",
              "annullamento"
            ]
          },
          "lock": {
            "type": "string",
            "enum": [
              "sticky",
              "until_sii_update"
            ],
            "default": "until_sii_update"
          },
          "motivazione": {
            "type": "string"
          }
        }
      },
      "Override": {
        "allOf": [
          {
            "$ref": "#/components/schemas/OverrideInput"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "stato": {
                "type": "string",
                "enum": [
                  "attivo",
                  "superato_da_sii",
                  "rimosso"
                ]
              },
              "valore_sii_originale": {
                "type": "number",
                "nullable": true
              },
              "operatore": {
                "type": "string"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      }
    }
  },
  "paths": {
    "/v1/admin/users": {
      "get": {
        "tags": [
          "Identità"
        ],
        "summary": "Elenco utenti del tenant (admin)",
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Vietato"
          }
        }
      },
      "post": {
        "tags": [
          "Identità"
        ],
        "summary": "Crea identità (admin)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creato"
          },
          "403": {
            "description": "Vietato"
          }
        }
      }
    },
    "/v1/admin/users/{sub}": {
      "get": {
        "tags": [
          "Identità"
        ],
        "summary": "Dettaglio utente + grant (admin)",
        "parameters": [
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "Non trovato"
          }
        }
      }
    },
    "/v1/admin/users/{sub}/grants/{persona}": {
      "put": {
        "tags": [
          "Identità"
        ],
        "summary": "Assegna/aggiorna grant persona (admin)",
        "parameters": [
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "persona",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "CLIENTE",
                "AGENTE",
                "CRM-USER"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "resource_id": {
                    "type": "string"
                  },
                  "role": {
                    "type": "string"
                  },
                  "modules": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "enum": [
                        "r",
                        "rw"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Non valido"
          }
        }
      },
      "delete": {
        "tags": [
          "Identità"
        ],
        "summary": "Revoca grant persona (admin)",
        "parameters": [
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "persona",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/agenti": {
      "get": {
        "tags": [
          "Agenti"
        ],
        "summary": "Lista agenti (cursor pagination)",
        "operationId": "listAgenti",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "attivo",
                "sospeso",
                "cessato"
              ]
            }
          },
          {
            "name": "livello",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "codice livello (es. L3)"
          },
          {
            "name": "mercato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "EE",
                "GAS",
                "ENTRAMBI"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Agente"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Agenti"
        ],
        "summary": "Crea agente (anagrafica + indirizzi + contatti)",
        "description": "Pre-check anti-duplicato applicativo su `codice_fiscale`/`partita_iva` cross-specializzazione → 409 `agente_esistente`.\nL'`id` è generato dal server (es. `A470D92FA`).\n",
        "operationId": "createAgente",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgenteCreate"
              },
              "example": {
                "tipo": "PF",
                "livello": "L3",
                "codice": "AGT-001",
                "anagrafica": {
                  "nome": "Mario",
                  "cognome": "Rossi",
                  "codice_fiscale": "RSSMRA80A01H501U"
                },
                "contatti": [
                  {
                    "tipo_contatto": "EMAIL",
                    "valore": "mario.rossi@example.com",
                    "is_primary": true
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agente"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "agente_esistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agenti/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Agenti"
        ],
        "summary": "Detail agente (anagrafica + indirizzi + contatti)",
        "operationId": "getAgente",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agente"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Agenti"
        ],
        "summary": "Update agente (campi root)",
        "description": "Body parziale. Aggiorna stato/mercato/livello/codice/iban/note/data_ingresso/utente_cognito_sub.",
        "operationId": "updateAgente",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "stato": {
                    "type": "string"
                  },
                  "mercato": {
                    "type": "string"
                  },
                  "livello": {
                    "type": "string"
                  },
                  "codice": {
                    "type": "string"
                  },
                  "iban": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Agente"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Agenti"
        ],
        "summary": "Soft-delete agente",
        "description": "Imposta `deleted_at` e `stato=cessato`. 409 `agente_in_uso` se ha contratti attribuiti attivi.",
        "operationId": "deleteAgente",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "agente_in_uso",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agenti/{id}/contratti": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Rete commerciale"
        ],
        "summary": "Contratti attribuiti all'agente (snapshot)",
        "operationId": "listAgenteContratti",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AgenteContrattoLink"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/agenti/{id}/superiori": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Agenti"
        ],
        "summary": "Lista superiori dell'agente (correnti + storico)",
        "operationId": "listAgenteSuperiori",
        "parameters": [
          {
            "name": "solo_correnti",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Se true, solo i rapporti con is_current=true"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AgenteSuperiore"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Agenti"
        ],
        "summary": "Aggiungi un superiore (un agente può averne più di uno)",
        "operationId": "addAgenteSuperiore",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "superiore_id"
                ],
                "properties": {
                  "superiore_id": {
                    "type": "string"
                  },
                  "is_diretto": {
                    "type": "boolean",
                    "default": false
                  },
                  "data_inizio": {
                    "type": "string",
                    "format": "date"
                  }
                }
              },
              "example": {
                "superiore_id": "A44211DEC",
                "is_diretto": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lista superiori aggiornata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "agente_not_found / superiore_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "superiore_gia_presente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agenti/{id}/superiori/{rel_id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "rel_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Agenti"
        ],
        "summary": "Chiudi un rapporto di superiorità (storicizza)",
        "description": "Non cancella la riga: imposta `is_current=false` e `data_fine` (oggi se non già valorizzata).",
        "operationId": "closeAgenteSuperiore",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/agenti_livelli": {
      "get": {
        "tags": [
          "Rete commerciale"
        ],
        "summary": "Lista livelli (ordinati per ordine)",
        "operationId": "listAgentiLivelli",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AgenteLivello"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Rete commerciale"
        ],
        "summary": "Crea livello",
        "operationId": "createAgenteLivello",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "codice",
                  "nome",
                  "ordine"
                ],
                "properties": {
                  "codice": {
                    "type": "string",
                    "pattern": "^[A-Z0-9_]{1,40}$"
                  },
                  "nome": {
                    "type": "string"
                  },
                  "ordine": {
                    "type": "integer"
                  },
                  "descrizione": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "codice": "L5",
                "nome": "Procacciatore",
                "ordine": 5
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgenteLivello"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "livello_esistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agenti_livelli/{codice}": {
      "parameters": [
        {
          "name": "codice",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Rete commerciale"
        ],
        "summary": "Detail livello",
        "operationId": "getAgenteLivello",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgenteLivello"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Rete commerciale"
        ],
        "summary": "Update livello (nome/ordine/descrizione)",
        "operationId": "updateAgenteLivello",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nome": {
                    "type": "string"
                  },
                  "ordine": {
                    "type": "integer"
                  },
                  "descrizione": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgenteLivello"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Rete commerciale"
        ],
        "summary": "Cancella livello",
        "description": "409 `livello_in_uso` se referenziato da agenti.",
        "operationId": "deleteAgenteLivello",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "livello_in_uso",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Login email+password (ritorna token o challenge MFA/nuova-password). 'tenant' = hint tenant per token scoped.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  },
                  "tenant": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Logout globale (revoca refresh)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "access_token": {
                    "type": "string"
                  }
                },
                "required": [
                  "access_token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/mfa/setup": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Associa TOTP (ritorna secret + otpauth_uri per QR)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "access_token": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  }
                },
                "required": [
                  "access_token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/mfa/verify": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Completa challenge SOFTWARE_TOKEN_MFA",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "session": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "tenant": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "session",
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/mfa/verify-setup": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Verifica e abilita TOTP",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "access_token": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  }
                },
                "required": [
                  "access_token",
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/new-password": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Completa NEW_PASSWORD_REQUIRED (onboarding/migrazione)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "session": {
                    "type": "string"
                  },
                  "new_password": {
                    "type": "string"
                  },
                  "tenant": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "session",
                  "new_password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/password/change": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Cambio password (autenticato)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "access_token": {
                    "type": "string"
                  },
                  "old_password": {
                    "type": "string"
                  },
                  "new_password": {
                    "type": "string"
                  }
                },
                "required": [
                  "access_token",
                  "old_password",
                  "new_password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/password/forgot": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Invia codice di reset via email",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/password/reset": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Conferma reset password con codice",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "new_password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "code",
                  "new_password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/auth/refresh": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Rinnova i token (con 'tenant' per switch tenant scoped)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "refresh_token": {
                    "type": "string"
                  },
                  "tenant": {
                    "type": "string"
                  }
                },
                "required": [
                  "refresh_token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "Richiesta non valida"
          },
          "401": {
            "description": "Non autorizzato"
          }
        },
        "security": []
      }
    },
    "/v1/cgf": {
      "get": {
        "tags": [
          "CGF anagrafica"
        ],
        "summary": "Lista CGF (anagrafica versionata)",
        "operationId": "listCgf",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "contratto_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cliente_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vigenza",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "vigente",
                "scaduta"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Ricerca ILIKE su codice_cgf"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Cgf"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "CGF anagrafica"
        ],
        "summary": "Crea nuova revisione CGF",
        "description": "Vincolo unique: **max 1 CGF vigente per contratto** (`data_fine IS NULL`).\nCon `?auto_close_previous=true` la CGF precedente viene chiusa automaticamente.\n",
        "operationId": "createCgf",
        "parameters": [
          {
            "name": "auto_close_previous",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Cgf"
              },
              "example": {
                "contratto_id": 164,
                "codice_cgf": "CGF-2026-000162-V2",
                "data_inizio": "2027-01-01",
                "note": "Revisione 2027",
                "corrispettivi": [
                  {
                    "codice_fatturazione": "PCV_EE_2027",
                    "nome_corrispettivo": "PCV EE 2027",
                    "commodity_code": "EE",
                    "unita_misura_code": "EUR_ANNO",
                    "corrispettivo_unitario": 45.0
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cgf"
                }
              }
            }
          },
          "409": {
            "description": "CGF vigente già esistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/cgf/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "CGF anagrafica"
        ],
        "summary": "Detail CGF con corrispettivi nested",
        "operationId": "getCgf",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cgf"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "CGF anagrafica"
        ],
        "summary": "Update CGF (parziale)",
        "operationId": "updateCgf",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Cgf"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "CGF anagrafica"
        ],
        "summary": "Soft-delete CGF",
        "description": "409 `cgf_has_dependencies` se ha corrispettivi attivi. `?cascade=true` per cascade.",
        "parameters": [
          {
            "name": "cascade",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/clienti": {
      "get": {
        "tags": [
          "Clienti"
        ],
        "summary": "Lista clienti",
        "operationId": "listClienti",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search testuale su nome/ragione_sociale/CF/PIVA/email/POD"
          },
          {
            "name": "tipo",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "AZIENDA",
                "PERSONA_FISICA",
                "DITTA_INDIVIDUALE",
                "CONDOMINIO",
                "PUBBLICA_ADMIN",
                "ALTRI_ENTI"
              ]
            }
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "lead",
                "attivo",
                "sospeso",
                "cessato",
                "cancellato"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista clienti",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Cliente"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Clienti"
        ],
        "summary": "Crea cliente (solo anagrafica)",
        "description": "Crea cliente nuovo. **Non accetta** `punti_fornitura` nel body — usare endpoint dedicato.\n\nVincoli UNIQUE cross-tabella su `codice_fiscale` e `partita_iva`: tentativi su CF/PIVA duplicato\nritornano `409 codice_fiscale_already_exists` / `409 partita_iva_already_exists` con resource link.\n",
        "operationId": "createCliente",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Cliente"
              },
              "examples": {
                "azienda": {
                  "summary": "Azienda SRL",
                  "value": {
                    "tipo": "AZIENDA",
                    "stato": "attivo",
                    "piano": "starter",
                    "azienda": {
                      "ragione_sociale": "ESEMPIO SRL",
                      "partita_iva": "12345678901",
                      "codice_fiscale": "12345678901",
                      "codice_ateco": "35.11.00",
                      "tipo_societa": "SRL"
                    },
                    "indirizzi": [
                      {
                        "tipo": "SEDE_LEGALE",
                        "indirizzo": "Via Roma",
                        "civico": "1",
                        "cap": "00100",
                        "comune_nome": "Roma",
                        "provincia": "RM",
                        "is_primary": true
                      }
                    ],
                    "contatti": [
                      {
                        "tipo_contatto": "EMAIL_PRINCIPALE",
                        "valore": "info@TENANT_NAME.it",
                        "is_primary": true
                      }
                    ]
                  }
                },
                "ditta_individuale": {
                  "summary": "Ditta Individuale",
                  "value": {
                    "tipo": "DITTA_INDIVIDUALE",
                    "stato": "lead",
                    "ditta_individuale": {
                      "nome": "Anna",
                      "cognome": "Bianchi",
                      "codice_fiscale": "BNCNNA80A41H501Z",
                      "partita_iva": "01234567890",
                      "data_nascita": "1980-01-01",
                      "comune_nascita_nome": "Roma"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cliente creato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "CF / PIVA già presente in qualche anagrafica",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "conflict_error",
                    "code": "codice_fiscale_already_exists",
                    "message": "Codice Fiscale 'BNCNNA80A41H501Z' gia' presente nell'anagrafica clienti",
                    "details": {
                      "field": "codice_fiscale",
                      "value": "BNCNNA80A41H501Z",
                      "cliente_esistente": {
                        "id": "C84672900",
                        "stato": "lead",
                        "nome_display": "Anna Bianchi",
                        "url": "/v1/clienti/C84672900"
                      },
                      "hint": "Per aggiornare: PUT /v1/clienti/C84672900"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/clienti/{cliente_id}/intestazioni": {
      "parameters": [
        {
          "name": "cliente_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Intestazioni"
        ],
        "summary": "Lista intestazioni di un cliente (storico)",
        "operationId": "listIntestazioniCliente",
        "parameters": [
          {
            "name": "attive",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Filtra solo intestazioni vigenti"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Intestazione"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/clienti/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "C8F6189ED"
        }
      ],
      "get": {
        "tags": [
          "Clienti"
        ],
        "summary": "Detail cliente (anagrafica + indirizzi + contatti + LR + POD + contratti)",
        "description": "Aggregations nested cappate a 100 elementi (limite Aurora Data API 1 MB).\nQuando un sottoinsieme supera 100, la response include `has_more=true` + `note` con endpoint paginato dedicato.\n",
        "operationId": "getCliente",
        "responses": {
          "200": {
            "description": "Cliente trovato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Clienti"
        ],
        "summary": "Update cliente (anagrafica, indirizzi, contatti)",
        "description": "Body parziale: solo i campi presenti vengono aggiornati. Indirizzi/contatti supportano UPSERT\n(con `id` aggiorna, senza `id` crea). NON tocca POD — per voltura/switching usare domain action.\n",
        "operationId": "updateCliente",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Cliente"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cliente aggiornato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cliente"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Clienti"
        ],
        "summary": "Cancella cliente (soft-delete)",
        "description": "Soft delete (`deleted_at`). 409 strutturato con `details.blockers` se ha dipendenze attive\n(CGF vigenti / intestazioni attive / CTE vigenti). Con `?cascade=true` cancella l'intero sottoalbero\n(CTE → gruppi → CGF → intestazioni → POD esclusivi → cliente).\n",
        "operationId": "deleteCliente",
        "parameters": [
          {
            "name": "cascade",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "200": {
            "description": "Cascade eseguito, ritorna action_result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionResult"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Cliente con dipendenze attive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "conflict_error",
                    "code": "cliente_has_dependencies",
                    "message": "Impossibile eliminare cliente: 1 CGF vigenti, 8 intestazioni attive",
                    "details": {
                      "resource": {
                        "object": "cliente",
                        "id": "C70B9B292"
                      },
                      "blockers": [
                        {
                          "object": "contratto",
                          "id": 114,
                          "codice": "CGF-2026-000112",
                          "reason": "CGF vigente"
                        },
                        {
                          "object": "intestazione",
                          "id": 208,
                          "reason": "intestazione attiva",
                          "pf_codice": "IT001E00000016",
                          "pf_tipo": "EE"
                        }
                      ],
                      "hint": "Termina i CGF vigenti, cessa le intestazioni, oppure usa ?cascade=true"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/contratti": {
      "get": {
        "tags": [
          "Contratti"
        ],
        "summary": "Lista contratti",
        "operationId": "listContratti",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "cliente_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vigenza",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "vigente",
                "scaduto"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Contratto"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Contratti"
        ],
        "summary": "Crea contratto (con cascade opzionale)",
        "description": "Crea un nuovo contratto. `codice` (`CGF-YYYY-NNNNNN`) generato automaticamente.\n\n**Cascade opzionale**: se passi `punto_fornitura_id` nel body, l'endpoint crea in cascata\ngruppo di fatturazione \"Default\" + CTE per il POD indicato.\n",
        "operationId": "createContratto",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Contratto"
              },
              "examples": {
                "base": {
                  "summary": "Contratto base",
                  "value": {
                    "cliente_id": "C8F6189ED",
                    "data_firma": "2026-05-12",
                    "modalita_firma": "OTP",
                    "data_inizio": "2026-05-12",
                    "corrispettivi": [
                      {
                        "codice_voce": "QFISSA",
                        "nome_voce": "Quota fissa",
                        "unita_misura": "€/anno",
                        "importo": 60.0,
                        "commodity": "EE",
                        "sotto_categoria": "BT",
                        "ordine": 0
                      }
                    ]
                  }
                },
                "cascade": {
                  "summary": "Onboarding rapido con POD",
                  "value": {
                    "cliente_id": "C8F6189ED",
                    "data_firma": "2026-05-13",
                    "modalita_firma": "OTP",
                    "data_inizio": "2026-05-13",
                    "punto_fornitura_id": 100,
                    "gruppo": {
                      "nome": "Default",
                      "modalita_raggruppamento": "SINGOLA",
                      "periodo_fatt_cadenza": "BIMESTRALE",
                      "periodo_fatt_tipo": "SOLARE"
                    },
                    "cte": {
                      "esecuzione_anticipata": false,
                      "offerta_id": "OFFER-EE-001"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contratto creato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contratto"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "cliente_not_found o punto_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/contratti/{contratto_id}/gruppi": {
      "parameters": [
        {
          "name": "contratto_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Gruppi di fatturazione"
        ],
        "summary": "Lista gruppi del contratto",
        "operationId": "listGruppi",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "Gruppi di fatturazione"
        ],
        "summary": "Crea nuovo gruppo di fatturazione",
        "operationId": "createGruppo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GruppoFatturazione"
              },
              "example": {
                "nome": "Sedi Nord",
                "descrizione": "Filiali del Nord Italia, bolletta unica",
                "modalita_raggruppamento": "MULTISITO",
                "periodo_fatt_cadenza": "TRIMESTRALE",
                "periodo_fatt_tipo": "SOLARE"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Gruppo creato"
          }
        }
      }
    },
    "/v1/contratti/{contratto_id}/gruppi/{gruppo_id}": {
      "parameters": [
        {
          "name": "contratto_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "gruppo_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Gruppi di fatturazione"
        ],
        "summary": "Cancella gruppo",
        "description": "409 se ha CTE associate (anche storiche).",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/contratti/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Contratti"
        ],
        "summary": "Detail contratto (corrispettivi + intestazioni nested)",
        "operationId": "getContratto",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contratto"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Contratti"
        ],
        "summary": "Update contratto",
        "description": "Aggiorna `data_firma`, `modalita_firma`, `data_inizio`, `data_fine`, `note`.\nSe passi `corrispettivi`, l'intera lista è sostituita (DELETE+INSERT).\n`codice` non aggiornabile.\n",
        "operationId": "updateContratto",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Contratto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "Contratti"
        ],
        "summary": "Cancella contratto (soft-delete)",
        "description": "409 se ha CTE vigenti sotto. `?cascade=true` per soft-delete gruppi + CTE.",
        "parameters": [
          {
            "name": "cascade",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/cte": {
      "get": {
        "tags": [
          "CTE"
        ],
        "summary": "Lista templates CTE",
        "operationId": "listCte",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "commodity",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "EE",
                "GAS"
              ]
            }
          },
          {
            "name": "tipologia",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "FISSA",
                "VARIABILE",
                "INDICIZZATA"
              ]
            }
          },
          {
            "name": "attivi",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "CTE"
        ],
        "summary": "Crea template CTE",
        "description": "Sia `_id` che `_code` accettati per le FK lookup.\nPost-021: `cte_rete_commerciale` droppata; il campo `rete_commerciale` non è più accettato.\n",
        "operationId": "createCte",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Cte"
              },
              "example": {
                "codice_offerta": "OFF_EE_FISSA_2026",
                "nome_offerta": "EE Fissa 2026",
                "commodity_code": "EE",
                "tipologia_offerta_code": "FISSA",
                "tariffazione_code": "BIORARIA",
                "data_inizio": "2026-01-01",
                "stato": true,
                "corrispettivi": [
                  {
                    "codice_fatturazione": "PCV",
                    "nome_corrispettivo": "PCV",
                    "commodity_code": "EE",
                    "unita_misura_code": "EUR_ANNO",
                    "corrispettivo_unitario": 60.0
                  }
                ],
                "offerte_prezzi": [
                  {
                    "fascia_code": "F1",
                    "prezzo": 0.18,
                    "unita_misura_code": "EUR_KWH"
                  },
                  {
                    "fascia_code": "F23",
                    "prezzo": 0.14,
                    "unita_misura_code": "EUR_KWH"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template creato"
          }
        }
      }
    },
    "/v1/cte/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "CTE"
        ],
        "summary": "Detail template CTE con corrispettivi e prezzi",
        "operationId": "getCte",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cte"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "CTE"
        ],
        "summary": "Update CTE (replace arrays se presenti)",
        "operationId": "updateCte",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Cte"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "CTE"
        ],
        "summary": "Cancella CTE",
        "description": "409 `cte_in_use` se referenziata da `offerta_commerciale` vigente, oppure `cte_vigente` se `data_fine IS NULL`.",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/fatture": {
      "get": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Elenco fatture (filtri + paginazione)",
        "description": "Filtri: stato, cliente_id, contratto_id, agente_id, periodo (da/a).",
        "operationId": "listFatture",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cliente_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contratto_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "agente_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "da",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "a",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Crea fattura (bozza) con righe",
        "operationId": "createFattura",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cliente_id"
                ],
                "properties": {
                  "cliente_id": {
                    "type": "string"
                  },
                  "contratto_id": {
                    "type": "integer"
                  },
                  "tipo": {
                    "type": "string",
                    "enum": [
                      "acconto",
                      "saldo",
                      "nota_credito",
                      "autofattura"
                    ],
                    "default": "saldo"
                  },
                  "periodo_dal": {
                    "type": "string",
                    "format": "date"
                  },
                  "periodo_al": {
                    "type": "string",
                    "format": "date"
                  },
                  "righe": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "descrizione": {
                          "type": "string"
                        },
                        "categoria": {
                          "type": "string",
                          "enum": [
                            "energia",
                            "trasporto",
                            "oneri_sistema",
                            "imposte",
                            "canone",
                            "servizio",
                            "altro"
                          ]
                        },
                        "quantita": {
                          "type": "number"
                        },
                        "prezzo_unitario": {
                          "type": "number"
                        },
                        "importo": {
                          "type": "number"
                        },
                        "aliquota_iva": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bozza creata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "cliente_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/fatture/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Dettaglio fattura (righe, riepilogo IVA, pagamenti, scadenze)",
        "operationId": "getFattura",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Modifica fattura (solo in bozza)",
        "operationId": "updateFattura",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "fattura_non_modificabile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Elimina fattura (soft-delete, solo bozza)",
        "operationId": "deleteFattura",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "description": "fattura_non_eliminabile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/fatture/{id}/emetti": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Emetti la fattura: bozza → emessa (assegna numero AAAA/NNNNNN)",
        "operationId": "emettiFattura",
        "responses": {
          "200": {
            "description": "Emessa",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "gia_emessa",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/fatture/{id}/pagamenti": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Registra un incasso sulla fattura",
        "description": "Se gli incassi confermati coprono il totale, la fattura passa a 'pagata'.",
        "operationId": "addPagamento",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "importo"
                ],
                "properties": {
                  "importo": {
                    "type": "number"
                  },
                  "data_pagamento": {
                    "type": "string",
                    "format": "date"
                  },
                  "modalita_pagamento_id": {
                    "type": "integer"
                  },
                  "riferimento": {
                    "type": "string"
                  },
                  "stato": {
                    "type": "string",
                    "enum": [
                      "in_attesa",
                      "confermato",
                      "rifiutato",
                      "stornato"
                    ],
                    "default": "confermato"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/fatture/{id}/righe": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Aggiungi riga (solo bozza)",
        "operationId": "addRigaFattura",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/fatture/{id}/righe/{rid}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "rid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "delete": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Elimina riga (solo bozza)",
        "operationId": "deleteRigaFattura",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/comuni": {
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Ricerca comuni (filtro obbligatorio)",
        "description": "**Almeno uno** dei query param `q`, `cap`, `provincia` è obbligatorio\n(senza filtri → `400 missing_field` per evitare full-scan).\n",
        "operationId": "searchComuni",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string",
              "minLength": 2
            },
            "description": "Prefix match nome (min 2 char)"
          },
          {
            "name": "cap",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{5}$"
            },
            "description": "CAP esatto"
          },
          {
            "name": "provincia",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Sigla provincia"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "400": {
            "description": "missing_field / invalid_value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/comuni/{codice_istat}": {
      "parameters": [
        {
          "name": "codice_istat",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "058091"
        }
      ],
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Detail comune + CAP nested",
        "operationId": "getComune",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Comune"
                }
              }
            }
          },
          "404": {
            "description": "comune_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/nazioni": {
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Lista nazioni ISO",
        "operationId": "listNazioni",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search per nome"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Nazione"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/province": {
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Tutte le province (~107)",
        "operationId": "listProvince",
        "parameters": [
          {
            "name": "regione",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Codice regione opzionale"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/geo/province/{sigla}/comuni": {
      "parameters": [
        {
          "name": "sigla",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "MI"
        }
      ],
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Comuni di una provincia (paginato)",
        "operationId": "listComuniByProvincia",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Comune"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/regioni": {
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Lista delle 20 regioni italiane",
        "description": "Risposta cacheable 24h.",
        "operationId": "listRegioni",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Regione"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/geo/regioni/{codice}/province": {
      "parameters": [
        {
          "name": "codice",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "03"
        }
      ],
      "get": {
        "tags": [
          "Geo"
        ],
        "summary": "Province di una regione",
        "operationId": "listProvinceByRegione",
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "regione_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/intestazioni/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "patch": {
        "tags": [
          "Intestazioni"
        ],
        "summary": "Correzione singola intestazione",
        "description": "**NON usare** per voltura/switching. Campi NON modificabili: `punto_fornitura_id`, `cliente_id` (→ 400 immutable_field).\n",
        "operationId": "patchIntestazione",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Intestazione"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Intestazione"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Intestazioni"
        ],
        "summary": "Cancella intestazione",
        "description": "Solo se NON attiva (`data_fine IS NULL` → 409 intestazione_attiva).",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/master-data": {
      "get": {
        "tags": [
          "Master data"
        ],
        "summary": "Lista delle lookup disponibili",
        "description": "Lookup: `commodities`, `unita_misura`, `tipo_uso`, `opzioni_tariffarie`, `classi_contatore`,\n`classi_agevolazione`, `modalita_pagamento`, `modalita_spedizione`, `indici_variabili`,\n`tipologie_offerta`, `tariffazioni`, `fasce_tariffarie`.\n",
        "operationId": "listMasterDataIndex",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/v1/master-data/{lookup}": {
      "parameters": [
        {
          "name": "lookup",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "enum": [
              "commodities",
              "unita_misura",
              "tipo_uso",
              "opzioni_tariffarie",
              "classi_contatore",
              "classi_agevolazione",
              "modalita_pagamento",
              "modalita_spedizione",
              "indici_variabili",
              "tipologie_offerta",
              "tariffazioni",
              "fasce_tariffarie"
            ]
          }
        }
      ],
      "get": {
        "tags": [
          "Master data"
        ],
        "summary": "Read-only contenuto di una lookup table",
        "operationId": "getMasterData",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "item_object": "fasce_tariffarie",
                  "data": [
                    {
                      "object": "fasce_tariffarie",
                      "id": 1,
                      "codice": "F1",
                      "nome": "Fascia Unica",
                      "ordine": 1,
                      "tariffazione_code": "MONORARIA"
                    },
                    {
                      "object": "fasce_tariffarie",
                      "id": 2,
                      "codice": "F1",
                      "nome": "Fascia F1 (giorno)",
                      "ordine": 1,
                      "tariffazione_code": "BIORARIA"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Identità dell'utente-risorsa autenticato (whoami)",
        "description": "Ritorna ruolo, resource_id, tenant ed email dal token portal.",
        "operationId": "getMe",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/me/agente": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Profilo del proprio agente",
        "operationId": "getMeAgente",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "permission_denied / portal_suspended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/agente/andamento": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "KPI di andamento del portafoglio agente",
        "description": "Contratti attivi, fatturato, incassato, scaduto, da incassare, fatture per stato, andamento mensile.",
        "operationId": "getMeAgenteAndamento",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/me/agente/contratti": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Cruscotto: contratti attivati ai propri clienti (con stato pagamenti)",
        "description": "Portafoglio dell'agente: per ogni contratto attribuito (snapshot `agenti_contratti`)\nrestituisce cliente, stato contratto, stato pagamenti e gli importi\nfatturato/incassato/scaduto.\n",
        "operationId": "getMeAgenteContratti",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/me/agente/contratti/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Dettaglio di un proprio contratto (fatture + scadenze)",
        "description": "Accessibile solo se il contratto è attribuito all'agente autenticato, altrimenti 404.",
        "operationId": "getMeAgenteContrattoDetail",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "contratto_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/agente/superiori": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Propria catena gerarchica (superiori)",
        "operationId": "getMeAgenteSuperiori",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/me/cliente": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Profilo del proprio cliente",
        "operationId": "getMeCliente",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "permission_denied / portal_suspended",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/cliente/contatti": {
      "get": {
        "tags": [
          "Portale"
        ],
        "summary": "Propri contatti",
        "operationId": "getMeClienteContatti",
        "security": [
          {
            "PortalJWT": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/v1/me/context": {
      "get": {
        "tags": [
          "Identità"
        ],
        "summary": "Bootstrap frontend: tenant attivo, personas, resource id, crm_role, moduli per persona, tenant disponibili (attivo post-cutover)",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Non autorizzato"
          }
        }
      }
    },
    "/v1/metering/attivita": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Cosa sta elaborando il sistema in questo momento",
        "description": "Le altre letture dicono com'e' andata; questa dice cosa sta succedendo ADESSO. Serve a distinguere tre situazioni che dall'esterno si assomigliano e richiedono decisioni opposte: una lavorazione in corso, una bloccata, una mai partita.\n\nInterroga solo Glue e S3, mai il database: si puo' quindi richiamare ogni pochi secondi senza costo apprezzabile. Il pannello la rilegge ogni 10 secondi mentre c'e' qualcosa in corso, ogni 60 altrimenti.\n\n`coda.file_non_ancora_letti` conta i file presenti su S3 che non compaiono nel registro: o sono in lavorazione adesso, o aspettano la prossima corsa.",
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Attivita' in corso, coda e ultime lavorazioni",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "in_corso": [
                      {
                        "id": "jr_00ca3374d98b8986",
                        "fase": "lettura",
                        "stato": "RUNNING",
                        "in_corso": true,
                        "avviata": "2026-09-23T21:21:36",
                        "durata_s": 6169,
                        "durata": "1 h 42 min",
                        "errore": null,
                        "come": {
                          "sorgente": "landing",
                          "solo_nuovi": "si"
                        }
                      }
                    ],
                    "quante_in_corso": 1,
                    "coda": {
                      "file_non_ancora_letti": 2,
                      "gb_non_ancora_letti": 4.67,
                      "significato": "file presenti su S3 che non compaiono ancora nel registro"
                    },
                    "ultime_letture": [],
                    "ultimi_scompattamenti": []
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/batches/{id}": {
      "get": {
        "tags": [
          "Metering · Ingest"
        ],
        "summary": "Stato di elaborazione di un caricamento",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Stato batch",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "stato": {
                          "type": "string",
                          "enum": [
                            "in_coda",
                            "in_elaborazione",
                            "completato",
                            "errore"
                          ]
                        },
                        "file_totali": {
                          "type": "integer",
                          "example": 1240
                        },
                        "importati": {
                          "type": "integer",
                          "example": 980
                        },
                        "ignorati_duplicati": {
                          "type": "integer",
                          "example": 260
                        },
                        "errori": {
                          "type": "integer",
                          "example": 0
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "batch_id": "up_a6bb5716942c",
                    "stato": "completato",
                    "processati": 47,
                    "errori": 0
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/chiusura": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Quante bollette si possono emettere per un mese",
        "description": "**Prima di fatturare guardare `coerenza` e `pronto`, non solo `fatturabili`.**\n\n`coerenza` confronta il totale degli aggregati mensili con la somma del giornaliero. Se\n`aggregato_da_ricalcolare` è `true` lo scarto supera il 15 %: gli aggregati sono stati\ncostruiti su un mese ancora incompleto e vanno rifatti con\n`POST /v1/metering/reprocess` e `solo_aggregati: true` — pochi minuti, non rilegge nulla.\n\n`pronto` è `true` solo se ogni POD-mese è `quadrato` **e** la coerenza regge: un mese il\ncui aggregato non concorda con il giornaliero non è chiudibile nemmeno se i conteggi\ntornano.\n\nGli stati dei POD-mese, e cosa significano:\n\n| stato | cosa è successo | rimedio |\n|---|---|---|\n| `quadrato` | consumo dalla curva, oppure registri e curva coincidono | nessuno: fatturabile |\n| `periodo_incompleto` | finestra di lettura più **corta** del mese: fattura **meno** del dovuto | caricare gli archivi mancanti, poi ricalcolare |\n| `periodo_esteso` | finestra più **lunga**: fattura **più** del dovuto | correzione manuale |\n| `curva_incompleta` | non quadra e la curva copre pochi giorni | caricare gli archivi mancanti |\n| `solo_curva` | nessun dato del distributore, solo la nostra ricostruzione | caricare gli archivi con i registri |\n| `ka_non_verificata` | costante di lettura su un totale dichiarato, senza riscontro | verificare la costante sul contatore |\n| `periodo_chiuso` | la finestra e' piu' corta del mese ma coincide con un intervento dichiarato: stacco, riaggancio o cambio fornitore | nessun rimedio: il periodo e' quello |\n| `fuori_scala` | il consumo si scosta oltre dieci volte, o scende sotto un decimo, rispetto alla storia dello stesso POD | guardarlo prima di fatturare: puo' essere vero, puo' essere un errore che le due fonti condividono |\n| `registri_incoerenti` | la serie degli indici del contatore ha un salto: un flusso porta un registro di un'altra serie, e sommare le differenze giornaliere produrrebbe energia inesistente. Il valore mostrato viene dalla curva | guardare il flusso che ha rotto la serie |\n| `da_verificare` | registri e curva divergono oltre tolleranza | controllo: le due fonti non concordano |\n\nIl consumo di un mese dipende da DUE letture: la chiusura del mese precedente e quella del mese. Tutto il resto lo copre l'accumulo del contatore, quindi i giorni mancanti in mezzo non tolgono nulla al totale. Questa e' percio' l'unica domanda che conta prima di fatturare, e si risponde guardando i bordi della finestra invece del volume dei dati.\n\n`fatturabili` conta i POD-mese in stato `quadrato`; `motivi_sospensione` dice perche' gli altri non lo sono. `pronto` e' vero solo se non c'e' nulla di sospeso.",
        "parameters": [
          {
            "name": "mese",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "2026-03"
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Stato di chiusura del mese",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "mese": "2026-03",
                    "pod_mese_totali": 8836,
                    "fatturabili": 8211,
                    "non_fatturabili": 625,
                    "percentuale_fatturabile": 92.9,
                    "mwh_fatturabili": 1729.4,
                    "mwh_sospesi": 140.2,
                    "bordi": {
                      "su_pod_mese_da_registri": 8798,
                      "apertura_esatta": 8402,
                      "chiusura_esatta": 8298,
                      "finestra_esatta": 8240,
                      "percentuale_finestra_esatta": 93.3,
                      "mwh_finestra_esatta": 1741.8,
                      "nota": "l'apertura e' la lettura dell'ultimo giorno del mese precedente, la chiusura quella dell'ultimo giorno del mese"
                    },
                    "motivi_sospensione": {
                      "periodo_incompleto": 501,
                      "periodo_esteso": 0,
                      "da_verificare": 124,
                      "curva_incompleta": 0
                    },
                    "pronto": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "parametro mese mancante o non valido"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/config": {
      "get": {
        "tags": [
          "Metering · Configurazione"
        ],
        "summary": "Legge la configurazione del tenant",
        "security": [
          {
            "jwt": []
          }
        ],
        "responses": {
          "200": {
            "description": "Config tenant (credenziali mascherate)",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "configurato": true,
                    "ingest_mode": "both",
                    "piva_venditore": "01234567890",
                    "webdav_url": "https://siicloud.acquirenteunico.it/remote.php/webdav/",
                    "credenziali_impostate": true,
                    "enabled": true,
                    "hourly_hot_mesi": 6,
                    "curve_retention_mesi": 12,
                    "last_download_at": "2026-09-06T12:50:47"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/MeteringNonAttivo"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      },
      "put": {
        "tags": [
          "Metering · Configurazione"
        ],
        "summary": "Aggiorna config (ingest_mode, credenziali SII → Secrets Manager)",
        "security": [
          {
            "jwt": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ingest_mode": {
                    "type": "string",
                    "enum": [
                      "webdav",
                      "manual",
                      "both"
                    ]
                  },
                  "piva_venditore": {
                    "type": "string"
                  },
                  "sii_username": {
                    "type": "string"
                  },
                  "sii_app_password": {
                    "type": "string",
                    "description": "salvata in Secrets Manager; non viene mai restituita da GET"
                  },
                  "hourly_hot_mesi": {
                    "type": "integer"
                  },
                  "curve_retention_mesi": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Config aggiornata",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "aggiornato": true,
                    "ingest_mode": "both",
                    "credenziali_impostate": true
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/MeteringNonAttivo"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/conguagli": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Periodi già fatturati che il metering ha ricalcolato",
        "description": "ARERA richiede che la bolletta indichi i RICALCOLI quando una lettura effettiva sostituisce una stima. Questa è la coda che il modulo di fatturazione deve leggere: ogni voce è un periodo il cui consumo è cambiato dopo il consolidamento.\n\n`era_fatturato=true` significa che il periodo era già in fattura: serve un CONGUAGLIO, non una rifatturazione. Con `solo_fatturati=true` si ottengono solo quelli.",
        "parameters": [
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "aperto",
                "chiuso",
                "tutti"
              ],
              "default": "aperto"
            }
          },
          {
            "name": "mese",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "2026-07"
            }
          },
          {
            "name": "solo_fatturati",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Coda dei conguagli",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "object": "list",
                    "aperti": 1,
                    "aperti_gia_fatturati": 1,
                    "delta_kwh_totale": 99.878,
                    "conguagli": [
                      {
                        "id": 34,
                        "pod": "IT001E00000011",
                        "competenza": "2026-07",
                        "versione": 2,
                        "kwh_precedente": 12909.083,
                        "kwh_nuovo": 13008.961,
                        "delta_kwh": 99.878,
                        "motivo": "rettifica del distributore",
                        "era_fatturato": true,
                        "rif_fattura": "FT/2026/00987",
                        "stato": "aperto",
                        "rilevato_at": "2026-09-08T09:30:21"
                      }
                    ]
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/conguagli/{id}/chiudi": {
      "post": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Chiude un conguaglio dopo che è stato emesso",
        "description": "Da chiamare quando il modulo di fatturazione ha emesso il conguaglio. Il periodo torna in stato `fatturato`: la differenza è stata assorbita.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "example": 34
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rif_fattura": {
                    "type": "string",
                    "example": "FT/2026/01050-CONG",
                    "description": "riferimento della nota di conguaglio emessa"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Conguaglio chiuso",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "id": 34,
                    "stato": "chiuso",
                    "pod": "IT001E00000011",
                    "competenza": "2026-07",
                    "delta_kwh": 99.878
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conguaglio inesistente o già chiuso"
          }
        }
      }
    },
    "/v1/metering/eventi": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Il diario del tenant — cosa e' successo, quando, com'e' finito",
        "description": "Ogni fase della catena lascia una riga: caricamento, scompattamento, lettura, giornaliero, orario, mensile, registro. Si legge dal piu' recente.\n\nRisponde a una domanda che le altre letture non coprono: **cosa e' successo al mio caricamento**. `/stato` dice com'e' messo il sistema adesso, `/attivita` dice cosa sta girando in questo momento, questa dice la storia.\n\nGli esiti:\n  * `avviato` — la fase e' cominciata;\n  * `concluso` — finita bene, con i numeri in `dettagli`;\n  * `fallito` — finita male, il motivo e' nel messaggio;\n  * `saltato` — non e' partita, e il messaggio dice perche'. Il caso tipico e' una lettura gia' in corso sullo stesso tenant: i file restano in attesa e li riprende il recupero automatico entro un quarto d'ora.",
        "parameters": [
          {
            "name": "fase",
            "in": "query",
            "description": "Filtra una sola fase della catena",
            "schema": {
              "type": "string",
              "enum": [
                "caricamento",
                "scompattamento",
                "lettura",
                "giornaliero",
                "orario",
                "mensile",
                "registro"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 60,
              "maximum": 300
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Eventi dal piu' recente",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "object": "list",
                    "quanti": 4,
                    "eventi": [
                      {
                        "quando": "2026-09-24T19:02:11",
                        "fase": "mensile",
                        "esito": "concluso",
                        "messaggio": "284.335 POD-mese consolidati",
                        "riferimento": "jr_00ca3374d98b8986",
                        "dettagli": {
                          "righe": 284335
                        }
                      },
                      {
                        "quando": "2026-09-24T18:44:02",
                        "fase": "lettura",
                        "esito": "saltato",
                        "messaggio": "Una lettura era gia' in corso su questo tenant; i file restano in attesa e vengono presi dalla prossima corsa",
                        "riferimento": "up_8179d1534823",
                        "dettagli": {
                          "corsa_in_atto": "jr_3ef164cdbe9953a8"
                        }
                      }
                    ],
                    "fasi": [
                      "caricamento",
                      "scompattamento",
                      "lettura",
                      "giornaliero",
                      "orario",
                      "mensile",
                      "registro"
                    ],
                    "esiti": [
                      "avviato",
                      "concluso",
                      "fallito",
                      "saltato"
                    ]
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/export/{tipo}": {
      "get": {
        "tags": [
          "Metering · Export"
        ],
        "summary": "Export CSV (mensile | letture | consumi | tracciato)",
        "parameters": [
          {
            "name": "tipo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mensile",
                "consumi",
                "letture",
                "tracciato"
              ]
            }
          },
          {
            "name": "job",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Solo per `tipo=tracciato`. Con `job` si chiede lo stato di un'esportazione avviata;\nsenza, e con `mese`, se ne avvia una nuova.\n"
          },
          {
            "name": "mese",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "2026-08"
            }
          },
          {
            "name": "pod",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Se omesso, per consumi e letture l'estrazione riguarda TUTTI i POD e serve formato=link"
          },
          {
            "name": "da",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "2026-07"
            }
          },
          {
            "name": "a",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "2026-08"
            }
          },
          {
            "name": "solo_fatturabili",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Tiene solo i POD-mese in stato `quadrato`, cioè quelli emettibili. Per `tipo=mensile` vale **true per difetto**, perché quell'export alimenta la fatturazione e includere righe sospese significa emettere bollette sbagliate: per averle tutte si chiede esplicitamente `solo_fatturabili=no`. Per gli altri tipi resta false per difetto."
          },
          {
            "name": "formato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "link"
              ],
              "default": "csv"
            },
            "description": "link = file su S3 con collegamento temporaneo, obbligatorio per le estrazioni massive"
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Estrazioni CSV con separatore ; e virgola decimale, pronte per Excel italiano.\n\n**`tracciato` è diverso dagli altri: è asincrono.** Unisce gli aggregati mensili di Aurora\n(le fasce F1/F2/F3 su cui si fattura) con registri, potenze e reattiva **per fascia**, che\nstanno solo nel dato grezzo su S3 e si leggono con Athena. Un mese del tenant maggiore sono\noltre 150.000 righe per una cinquantina di colonne: non può tornare dentro una risposta, e\nAthena da sola supera i 29 secondi che API Gateway concede.\n\nSi usa in due passi:\n\n1. `GET /v1/metering/export/tracciato?mese=2026-08` → `{ \"job\": \"…\", \"stato\": \"in_coda\" }`\n2. `GET /v1/metering/export/tracciato?job=…` → quando `stato` è `pronto` la risposta porta\n   `url` (collegamento temporaneo, dodici ore), `righe` e i due numeri qui sotto.\n\n**I due numeri da leggere sempre.** `senza_letture` conta i POD che hanno il dato\nfatturabile ma nessuna lettura grezza nel mese: per loro registri e potenze per fascia\nrestano vuoti. `senza_aggregato` conta il caso opposto. Il file contiene l'**unione** dei\ndue insiemi, e la colonna `delivery_notes` dice riga per riga quale caso è.\n\n**Cosa il tracciato non contiene, e perché.** Lo switch non è un dato che il sistema\nriceve; un «registro di potenza» non esiste, perché la potenza è una punta e non un\ncontatore; la ripartizione della reattiva entro e oltre soglia non è implementata da\nnessuna parte nel sistema. Quelle colonne sono assenti invece che vuote o inventate.\n\n`mensile` tutti i POD di un mese · `consumi` un POD su piu mesi, oppure TUTTI i POD se si omette `pod` · `letture` dettaglio giorno/fascia.\n\n**formato=link**: invece del contenuto restituisce un collegamento temporaneo (1 ora) a un file su S3. Serve perche la risposta di questa API non puo superare i 6 MB: oltre i 3 MB si passa al link da soli. Senza `pod`, per consumi e letture, il link e obbligatorio perche l'estrazione riguarda tutti i POD.\n\nCon `formato=link` il file lo scrive **direttamente il database**, per tutti e tre i tagli: il dato non passa dall'API e non ci sono limiti di dimensione ne di tempo. 110.641 righe in 3 secondi, 652.361 in 6. Il contenuto e identico a quello dell'estrazione inline: stesse colonne, stessa virgola decimale.\n\n**Troncamento**: se l'estrazione supera il tetto di righe la risposta lo dichiara (`troncato: true`, intestazione `X-Export-Troncato`) e il CSV porta una riga finale di avviso. Un file tagliato non deve mai sembrare completo.",
        "responses": {
          "200": {
            "description": "Per `mensile`, `consumi` e `letture`: il CSV in allegato, oppure il collegamento se\n`formato=link`.\n\nPer `tipo=tracciato` la risposta è **JSON**, e cambia a seconda del passo.\n\n**All'avvio** (`?mese=`):\n\n```json\n{\n  \"job\": \"671823d9360f4c5b\",\n  \"stato\": \"in_coda\",\n  \"competenza\": \"2026-07\",\n  \"controlla\": \"/v1/metering/export/tracciato?job=671823d9360f4c5b\"\n}\n```\n\n**Interrogando lo stato** (`?job=`), finché è in lavorazione:\n\n```json\n{ \"job\": \"671823d9360f4c5b\", \"stato\": \"in_corso\", \"fase\": \"aurora\" }\n```\n\nLe fasi sono, in ordine: `risveglio` (attesa del database, che con capacità minima 0\nsi mette in pausa), `athena`, `aurora`, `unione`, `fatto`.\n\n**Quando è pronto**, la risposta porta l'**URL di scarico definitivo**:\n\n```json\n{\n  \"job\": \"671823d9360f4c5b\",\n  \"stato\": \"pronto\",\n  \"fase\": \"fatto\",\n  \"competenza\": \"2026-07\",\n  \"file\": \"consumi-TENANT_NAME-2026-07.csv\",\n  \"url\": \"https://dev1-metering-curated-ACCOUNT_ID.s3.amazonaws.com/export/…?X-Amz-…\",\n  \"righe\": 173499,\n  \"senza_letture\": 167221,\n  \"senza_aggregato\": 61,\n  \"scade_il\": \"2026-10-13T02:40:27Z\"\n}\n```\n\n`url` è un collegamento firmato: si scarica direttamente, **senza token**, e **scade\ndopo dodici ore** — la data esatta è in `scade_il`. Dopo la scadenza si rigenera\nriavviando l'esportazione; il file resta su S3 ma il collegamento non vale più.\n\nSe `stato` è `fallito` la risposta porta `errore`. Vale la pena **richiedere lo stato\nancora per qualche minuto** prima di considerarlo definitivo: le invocazioni\nasincrone vengono ritentate, e un primo tentativo caduto sul risveglio del database\ndiventa spesso un secondo riuscito.\n",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "pod;mese;kwh_f1;kwh_f2;kwh_f3;kwh_tot;giorni_coperti;origine;stato;tipo_lettura;giorni_stimati;kwh_da_curva;scarto;consumo_annuo;mesi_annuo;data_lettura_prec;data_lettura_fine;lettura_prec;lettura_fine;stato_fatturazione;kvarh_tot;pot_max\nIT001E00000011;2026-07;5400,122;3409,859;4099,102;12909,083;31;delta_registri;quadrato;effettiva;0;12909,086;0,003;69669,370;8;2026-06-30;2026-07-31;308143,183;321052,266;da_fatturare;412,880;18,240\n"
              },
              "application/json": {
                "example": {
                  "data": {
                    "url": "https://dev1-metering-curated-ACCOUNT_ID.s3.eu-south-1.amazonaws.com/export/...",
                    "chiave": "export/tenant=TENANT_NAME/2026/09/08/144402-metering-letture-2026-07.csv",
                    "righe": 652361,
                    "byte": 32006796,
                    "troncato": false,
                    "scade_tra_s": 3600,
                    "generato_da": "aurora"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tipo non valido, parametri mancanti, oppure estrazione su tutti i POD senza formato=link"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/fetch": {
      "post": {
        "tags": [
          "Metering · Ingest"
        ],
        "summary": "Trigger manuale dello scarico SII (tenant WebDAV)",
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Avvia subito uno scarico dal SII senza aspettare il giro orario. E incrementale (riprende da last_download_at) e protetto da lock: se un giro e gia in corso non viene duplicato.\n",
        "responses": {
          "202": {
            "description": "Scarico avviato",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "stato": {
                          "type": "string",
                          "example": "avviato"
                        },
                        "comando_id": {
                          "type": "string"
                        },
                        "tenant": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "stato": "avviato",
                    "comando_id": "a6f9eaa8-cc3c-4417-9080-55e44db1c67a",
                    "tenant": "TENANT_NAME",
                    "nota": "Lo scarico è incrementale: riprende da last_download_at. Un giro già in corso non viene duplicato (lock)."
                  }
                }
              }
            }
          },
          "409": {
            "description": "tenant non in modalita webdav/both"
          },
          "503": {
            "description": "il proxy SII non ha accettato il comando"
          }
        }
      }
    },
    "/v1/metering/heatmap": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Copertura POD × giorno (da aggregati caldi)",
        "parameters": [
          {
            "name": "mese",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "2026-08"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lista POD con celle di copertura",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "object": {
                          "type": "string",
                          "example": "list"
                        },
                        "pods": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PodCoverage"
                          },
                          "description": "Pagina di POD. La paginazione e obbligatoria in pratica: un tenant con 9.600 POD produce ~300.000 celle e la risposta non starebbe nel limite di 6 MB.\n"
                        },
                        "has_more": {
                          "type": "boolean"
                        },
                        "next_cursor": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "object": "list",
                    "mese": "2026-08",
                    "pods": [
                      {
                        "pod": "IT001E00000012",
                        "giorni_attesi": 31,
                        "giorni_coperti": 31,
                        "giorni_mancanti": [],
                        "celle": [
                          {
                            "g": 1,
                            "stato": "best"
                          },
                          {
                            "g": 2,
                            "stato": "best"
                          }
                        ]
                      },
                      {
                        "pod": "IT001E00000013",
                        "giorni_attesi": 31,
                        "giorni_coperti": 29,
                        "giorni_mancanti": [
                          4,
                          11
                        ],
                        "celle": [
                          {
                            "g": 4,
                            "stato": "absent"
                          },
                          {
                            "g": 11,
                            "stato": "absent"
                          }
                        ]
                      }
                    ],
                    "has_more": true,
                    "next_cursor": "IT001E00000014"
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/overrides": {
      "post": {
        "tags": [
          "Metering · Rettifiche"
        ],
        "summary": "Rettifica o immissione manuale di una misura",
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OverrideInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Override creato + aggregato ricalcolato",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Override"
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": 9001,
                    "stato": "attivo",
                    "valore_sii_originale": 88.0,
                    "aggregato_ricalcolato": true
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Metering · Rettifiche"
        ],
        "summary": "Elenco override (incl. quelli superato_da_sii)",
        "parameters": [
          {
            "name": "pod",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "attivo",
                "superato_da_sii",
                "rimosso"
              ]
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lista override",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "object": "list",
                    "overrides": [
                      {
                        "id": 9001,
                        "pod": "IT001E00000012",
                        "data": "2026-08-11",
                        "fascia": "F1",
                        "valore": 92.0,
                        "tipo": "rettifica",
                        "stato": "attivo",
                        "valore_sii_originale": 88.0,
                        "operatore": "mario.rossi@example.com"
                      }
                    ]
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/overrides/{id}": {
      "delete": {
        "tags": [
          "Metering · Rettifiche"
        ],
        "summary": "Rimuove un override → torna il dato SII",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "204": {
            "description": "Rettifica rimossa: l'aggregato torna al dato originale del SII. Nessun corpo nella risposta."
          },
          "404": {
            "description": "Rettifica inesistente o già rimossa"
          }
        }
      }
    },
    "/v1/metering/pod": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Elenco dei POD di un mese, con il consumo per fascia",
        "description": "L'elenco dei POD-mese di un mese di competenza, con il consumo gia' ripartito per\nfascia e l'esito del controllo di quadratura.\n\nIl filtro `stato` accetta i cinque esiti salvati piu' un sesto valore che non e'\nuno stato ma una domanda: **`finestra_esatta`** seleziona i POD-mese la cui finestra\ndi lettura coincide con il mese di calendario, attraverso tutti gli stati. E' il\nfiltro giusto per scegliere i POD da confrontare con una fattura reale: su quelli il\nconsumo e' calcolato sul periodo corretto, quindi una differenza e' una differenza\nvera e non un disallineamento di periodo.\n\n`finestra_esatta` e' sempre piu' ampio di `quadrato`, perche' quest'ultimo pretende\nanche che la curva confermi il registro, e la curva puo' mancare per giorni che al\nregistro non servono: il contatore accumula.\n",
        "parameters": [
          {
            "name": "mese",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            },
            "example": "2026-07"
          },
          {
            "name": "stato",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "quadrato",
                "da_verificare",
                "periodo_incompleto",
                "periodo_esteso",
                "curva_incompleta",
                "solo_curva",
                "ka_non_verificata",
                "registri_incoerenti",
                "fuori_scala",
                "periodo_chiuso",
                "finestra_esatta"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "ricerca parziale sul codice POD",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ordine",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "consumo",
                "pod",
                "scarto"
              ],
              "default": "consumo"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Elenco dei POD del mese",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "object": "list",
                    "mese": "2026-07",
                    "quanti": 2,
                    "totali": {
                      "pod_mese": 110180,
                      "mwh": 31204.6
                    },
                    "offset": 0,
                    "limit": 50,
                    "altri": 110130,
                    "pod": [
                      {
                        "pod": "IT001E00000001",
                        "kwh_f1": 2943.78,
                        "kwh_f2": 2505.4,
                        "kwh_f3": 3291.45,
                        "kwh_tot": 8740.63,
                        "stato": "quadrato",
                        "origine": "delta_registri",
                        "giorni_coperti": 31,
                        "lettura": {
                          "data_iniziale": "2026-06-30",
                          "data_finale": "2026-07-31"
                        },
                        "scarto": 0.0,
                        "tipo_lettura": "effettiva"
                      },
                      {
                        "pod": "IT001E00000002",
                        "kwh_f1": 1204.5,
                        "kwh_f2": 980.2,
                        "kwh_f3": 1533.1,
                        "kwh_tot": 3717.8,
                        "stato": "curva_incompleta",
                        "origine": "delta_registri",
                        "giorni_coperti": 25,
                        "lettura": {
                          "data_iniziale": "2026-06-30",
                          "data_finale": "2026-07-31"
                        },
                        "scarto": 245.6,
                        "tipo_lettura": "effettiva"
                      }
                    ],
                    "stati": [
                      "quadrato",
                      "da_verificare",
                      "periodo_incompleto",
                      "periodo_esteso",
                      "curva_incompleta",
                      "solo_curva",
                      "ka_non_verificata",
                      "registri_incoerenti",
                      "fuori_scala",
                      "periodo_chiuso",
                      "finestra_esatta"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "parametro mese mancante o non valido"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/pod/{pod}/consumi": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Consumo mensile per fascia — il dato fatturabile",
        "description": "Restituisce il consumo consolidato da metering_pod_monthly, con la PROVENIENZA del numero e l'esito del controllo.\n\n`origine` dice da dove viene la cifra: `totali_sii` = totali del mese per fascia dichiarati dal contatore (flusso S2G); `delta_registri` = somma dei delta giornalieri delle letture cumulative; `curva` = ricostruzione dai 96 quarti, usata solo dove mancano le altre due; `curva_non_consolidata` = mese non ancora elaborato, valore provvisorio.\n\n`stato` vale `quadrato` quando il mese è emettibile. Gli altri dicono perché non lo è: `periodo_incompleto` (finestra più corta del mese, consumo in difetto), `periodo_esteso` (finestra più lunga, consumo in eccesso), `da_verificare` (divergenza vera fra le due fonti), `curva_incompleta` (manca il termine di paragone, il dato resta valido), `solo_curva` (il valore viene solo dalla nostra ricostruzione, non da un dato del distributore), `ka_non_verificata` (contatore con costante di lettura su un totale dichiarato, senza riscontro che dica se applicarla), `registri_incoerenti` (la serie degli indici del contatore ha un salto e non e' utilizzabile per quel mese: il valore riportato viene dalla curva), `fuori_scala` (il consumo del mese si scosta oltre dieci volte dalla storia dello stesso POD). Con `solo_fatturabili=true` si ottengono solo i mesi quadrati.\n\n**Dal 2026-10-14** `quadrato` esige la provenienza, non solo l'accordo fra le fonti: prima la ricostruzione dalla curva usciva `quadrato` per costruzione, e con pochi giorni di dato valeva lo 3,3% del consumo reale. Misurato sulle fatture di luglio 2026: `quadrato` ha mediana 1,000× con il 97,1% entro il 5%.\n\nCampi richiesti da ARERA per la bolletta: `tipo_lettura` (effettiva | stimata | mista) e `giorni_stimati` — un consumo stimato va DICHIARATO come tale; `consumo_annuo` con `mesi_annuo`, che dice su quanti mesi è calcolato (sotto 12 è parziale).\n\n`fatturazione.stato` vale `da_fatturare`, `fatturato` oppure `da_conguagliare` quando il metering ha ricalcolato un periodo già fatturato: in quel caso vedi /v1/metering/conguagli.",
        "parameters": [
          {
            "name": "pod",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "IT001E00000011"
          },
          {
            "name": "da",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "2026-07"
          },
          {
            "name": "a",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "2026-07"
          },
          {
            "name": "solo_fatturabili",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "se true esclude i mesi in stato da_verificare"
          },
          {
            "name": "giorni",
            "in": "query",
            "required": false,
            "description": "con `si` aggiunge il dettaglio giorno per giorno del periodo richiesto: ogni giorno con F1/F2/F3, totale, quarti rilevati e se e' completo. Senza, la risposta e' solo mensile.",
            "schema": {
              "type": "string",
              "enum": [
                "si",
                "no"
              ],
              "default": "no"
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Consumo per mese e fascia",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "pod": "IT001E00000011",
                    "periodi": [
                      {
                        "mese": "2026-07",
                        "kwh_f1": 5400.122,
                        "kwh_f2": 3409.859,
                        "kwh_f3": 4099.102,
                        "kwh_tot": 12909.083,
                        "origine": "delta_registri",
                        "stato": "quadrato",
                        "kwh_da_curva": 12909.086,
                        "scarto": 0.003,
                        "giorni_coperti": 31,
                        "lettura": {
                          "data_iniziale": "2026-06-30",
                          "data_finale": "2026-07-31",
                          "valore_iniziale": 308143.183,
                          "valore_finale": 321052.266
                        },
                        "tipo_lettura": "effettiva",
                        "kvarh_tot": 5650.53,
                        "pot_max": 31.41,
                        "ka": 1.0,
                        "giorni_stimati": 0,
                        "consumo_annuo": 69669.37,
                        "mesi_annuo": 8,
                        "fatturazione": {
                          "stato": "da_fatturare",
                          "versione": 1,
                          "rif_fattura": null
                        }
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "pod"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/pod/{pod}/bolletta": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Bolletta in bozza di un POD, in PDF",
        "description": "Restituisce `application/pdf`: **tre pagine per mese** con la struttura prevista da\nARERA per la bolletta dell'energia elettrica.\n\n1. **Riepilogo del periodo** — consumo fatturato, di cui stimato, consumo annuo,\n   quota energia, dati della fornitura.\n2. **Scontrino dell'Energia** — quota per consumi con il dettaglio vendita/rete,\n   quota fissa, quota potenza, accise e IVA, e il Box dell'offerta.\n3. **Letture e Consumi** — registri per fascia (dove disponibili), potenza ed\n   energia reattiva con cos phi, andamento giornaliero a barre impilate, storico\n   fino a diciotto mesi, tracciabilita' del calcolo.\n\n**Il listino e' convenzionale**: finche' non esiste l'anagrafica delle offerte, la\nquota energia si calcola con `Offerta Mixian`, monoraria a 0,31 EUR/kWh su tutte le\nfasce. Oneri di rete, quota fissa, quota potenza, accise e IVA compaiono nello\nScontrino con la dicitura «da listino», **vuote**: un documento che le stimasse\nsarebbe piu' completo e meno vero.\n\n**Non e' un documento fiscale** e lo dichiara in testa. Non riporta un'intestazione\ndel venditore: arrivera' dalla configurazione per tenant.\n\nCon `da` e `a` si ottengono tre pagine per ciascun mese del periodo.\n\nSe per il periodo richiesto non c'e' nessun dato consolidato la risposta e' **404**,\nnon un PDF con i trattini al posto dei numeri.",
        "parameters": [
          {
            "name": "pod",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "IT001E00000001"
          },
          {
            "name": "mese",
            "in": "query",
            "required": false,
            "description": "mese singolo; in alternativa usare `da` e `a`",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            },
            "example": "2026-07"
          },
          {
            "name": "da",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            }
          },
          {
            "name": "a",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Il PDF della bolletta in bozza",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"bolletta-bozza-<POD>-<mese>.pdf\"",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "POD o periodo mancante o non valido"
          },
          "404": {
            "description": "nessun dato consolidato per il periodo richiesto"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/pod/{pod}/curva": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Drill-down quartorario (96 valori, da Parquet via Athena)",
        "description": "Latenza ~secondi (Athena on-demand). Uso puntuale per POD/giorno, non di massa.",
        "parameters": [
          {
            "name": "pod",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "data",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Curva quartoraria",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "pod": {
                          "type": "string"
                        },
                        "data": {
                          "type": "string",
                          "format": "date"
                        },
                        "totale_kwh": {
                          "type": "number",
                          "example": 6.983
                        },
                        "quarti": {
                          "type": "array",
                          "items": {
                            "type": "number",
                            "nullable": true
                          },
                          "description": "96 valori E1..E96 in ordine; null se il quarto manca"
                        },
                        "tipo_flusso": {
                          "type": "string",
                          "example": "PDO2G"
                        },
                        "is_rettifica": {
                          "type": "boolean",
                          "description": "true se il valore mostrato proviene da una rettifica del distributore"
                        },
                        "fonte": {
                          "type": "string",
                          "example": "parquet"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "pod": "IT001E00000012",
                    "data": "2026-08-11",
                    "totale_kwh": 6.983,
                    "quarti": [
                      0.073,
                      0.128,
                      0.121,
                      0.12,
                      "…",
                      0.026,
                      0.026,
                      0.027
                    ],
                    "tipo_flusso": "PDO2G",
                    "is_rettifica": false,
                    "fonte": "parquet"
                  }
                }
              }
            }
          },
          "400": {
            "description": "POD o data non validi"
          },
          "404": {
            "description": "nessuna curva per quel POD in quel giorno"
          },
          "504": {
            "description": "Athena non ha risposto entro il tempo previsto"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/pod/{pod}/letture": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Letture di un giorno (dato SII + eventuale override)",
        "parameters": [
          {
            "name": "pod",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "data",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Letture del giorno con flag best/override",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "pod": "IT001E00000012",
                    "data": "2026-08-11",
                    "fasce": [
                      {
                        "fascia": "F1",
                        "kwh": 3.517,
                        "tipo": "curva",
                        "is_best": true
                      },
                      {
                        "fascia": "F2",
                        "kwh": 1.842,
                        "tipo": "curva",
                        "is_best": true
                      },
                      {
                        "fascia": "F3",
                        "kwh": 1.624,
                        "tipo": "curva",
                        "is_best": true
                      }
                    ],
                    "override_attivi": []
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/quadratura": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "POD-mese che non hanno superato il controllo di quadratura",
        "description": "Il consumo del mese viene calcolato in due modi indipendenti: il dato dichiarato dal distributore e la ricostruzione dalla curva quartoraria. L'esito del confronto sta in `stato`:\n\n  * `quadrato` — i due valori coincidono entro lo 0,2%.\n  * `da_verificare` — coprono lo stesso periodo ma i numeri divergono. È una divergenza vera, da guardare prima di fatturare.\n  * `periodo_incompleto` — la finestra fra le due letture dei registri è più CORTA del mese: il consumo è in difetto di quei giorni e si fatturerebbe meno del dovuto. È il più grave dei quattro e viene valutato per primo. Il confronto con la curva non può rilevarlo, perché curva e registri nascono dagli stessi flussi: se manca il file di un giorno lo perdono entrambi e continuano a coincidere.\n  * `periodo_esteso` — lo specchio del precedente: la finestra si apre PRIMA dell'ultimo giorno del mese scorso, quindi porta dentro consumi di quel mese e il totale e' in ECCESSO. Meno intuitivo, ma davanti a un cliente e' l'errore peggiore.\n  * `curva_incompleta` — la curva copre meno del 90% del mese, quindi non esiste un termine di paragone. Non è un dubbio sulla qualità del dato fatturabile, che resta valido: il delta dei registri copre il mese intero perché il contatore accumula, la curva no.\n\nSenza il parametro `stato` si ottengono le divergenze vere. Le righe sono ordinate per scarto decrescente. I contatori nella risposta si riferiscono sempre all'intero mese, qualunque sia il filtro.",
        "parameters": [
          {
            "name": "mese",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "2026-07"
          },
          {
            "name": "stato",
            "in": "query",
            "description": "Quale coda elencare. I contatori della risposta non cambiano.",
            "schema": {
              "type": "string",
              "enum": [
                "da_verificare",
                "curva_incompleta",
                "periodo_incompleto",
                "periodo_esteso",
                "quadrato"
              ],
              "default": "da_verificare"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Riepilogo del mese e anomalie",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "object": "list",
                    "mese": "2026-07",
                    "stato": "da_verificare",
                    "quadrati": 7557,
                    "da_verificare": 534,
                    "curva_incompleta": 2104,
                    "periodo_incompleto": 811,
                    "periodo_esteso": 402,
                    "totale": 11408,
                    "anomalie": [
                      {
                        "pod": "IT001E00000015",
                        "kwh_fatturabile": 11919.603,
                        "kwh_da_curva": 397.989,
                        "scarto": 11521.614,
                        "origine": "delta_registri",
                        "giorni_coperti": 31,
                        "lettura_dal": "2026-06-30",
                        "lettura_al": "2026-07-31"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "parametro mese mancante o non valido, oppure stato non riconosciuto"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/reprocess": {
      "post": {
        "tags": [
          "Metering · Ingest"
        ],
        "summary": "Replay esplicito di file o periodo (bypassa la dedup)",
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "da": {
                    "type": "string",
                    "example": "2026-08",
                    "description": "mese iniziale YYYY-MM"
                  },
                  "a": {
                    "type": "string",
                    "example": "2026-08",
                    "description": "mese finale YYYY-MM"
                  },
                  "solo_aggregati": {
                    "type": "boolean",
                    "description": "Ricalcola **solo** gli aggregati mensili dal giornaliero già a terra, senza\nrileggere nessun file. Pochi minuti.\n\n**È il passo obbligatorio prima di fatturare un mese.** Gli aggregati vengono\ncostruiti durante la corsa che trasforma i file: se nella stessa corsa\narrivano più archivi, i primi nascono su un mese ancora incompleto e restano\ncosì. Il 2026-10-13 settembre dichiarava 9.061 MWh contro i 27.522 che il\ngiornaliero conteneva davvero.\n\n`GET /v1/metering/chiusura` segnala da solo quando serve, nel blocco\n`coerenza`.\n"
                  },
                  "orario": {
                    "type": "boolean",
                    "default": false,
                    "description": "Scrive **anche** il profilo orario (`metering_pod_hourly`). Fuori dai casi\neccezionali va lasciato a `false`, ed e' il motivo per cui e' un parametro\nesplicito invece di un comportamento di difetto.\n\nNumeri misurati il 2026-10-16: la scrittura procede in circa **1.800 parti da\n~75 secondi** l'una — oltre **30 ore** per lotto, nell'ordine dei **150 USD**.\nLa tabella occupa **29 GB**, il 66% dell'intera banca dati, piu' dei **12,3 GB**\nche bastano all'archivio analitico completo.\n\nE non aggiunge niente: il dettaglio al **quarto d'ora** e' gia' negli archivi\nanalitici, ed e' da li' che `GET /v1/metering/pod/{pod}/curva` lo legge.\n\nQuando e' `true` la risposta porta un campo `avviso` con lo stesso avvertimento.\n"
                  },
                  "da_grezzi": {
                    "type": "boolean",
                    "default": false,
                    "description": "false (default): rilegge i Parquet gia prodotti, veloce, e il caso normale quando cambiano solo le regole di aggregazione.\n\n**true e una operazione DISTRUTTIVA e richiede `confermo_svuotamento: true`.** Svuota l'archivio analitico del tenant e lo ricostruisce da zero dagli archivi grezzi su S3: se la corsa non arriva in fondo — e con decine di migliaia di file e un tetto di otto ore e gia successo — l'archivio resta parziale. Serve solo quando cambia il parsing.\n\nSenza la conferma la richiesta e rifiutata con `400 conferma_richiesta`.\n\nNon usarlo per rileggere qualche archivio letto a meta: quelli non sono marcati come elaborati e la prima rielaborazione li riprende da sola.\n"
                  },
                  "confermo_svuotamento": {
                    "type": "boolean",
                    "default": false,
                    "description": "Obbligatorio insieme a `da_grezzi: true`. Esiste perche svuotare l'archivio analitico non deve potersi ottenere per distrazione da un flag che sembra una modalita di lettura.\n"
                  }
                }
              }
            }
          }
        },
        "description": "Rielabora un periodo e riscrive gli aggregati. Gli upsert sono idempotenti.",
        "responses": {
          "202": {
            "description": "Rielaborazione avviata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "stato": {
                          "type": "string",
                          "example": "avviato"
                        },
                        "run_id": {
                          "type": "string"
                        },
                        "sorgente": {
                          "type": "string",
                          "enum": [
                            "curated",
                            "landing"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "stato": "avviato",
                    "run_id": "jr_d37b3be03b1c1c42a565841ce107bbbc7b6eff79",
                    "tenant": "TENANT_NAME",
                    "sorgente": "curated",
                    "periodo": {
                      "da": "2026-07",
                      "a": "2026-07"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "periodo non valido"
          },
          "409": {
            "description": "troppe rielaborazioni gia in corso"
          }
        }
      }
    },
    "/v1/metering/stato": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Stato dell'intera pipeline, con i problemi gia valutati",
        "description": "Restituisce in una sola chiamata la fotografia della catena, dallo scarico dal SII fino al dato fatturabile, e **l'elenco dei problemi rilevati**.\n\nE l'endpoint su cui si costruisce un pannello di controllo: la valutazione di cosa sia un'anomalia sta qui, non in chi disegna la pagina, perche le soglie sono regole del dominio (uno scarico fermo da sei ore e un problema, tre ore no).\n\n`salute` riassume: `ok` nessun problema, `attenzione` solo anomalie minori, `critica` almeno un problema grave.\n\nOgni voce di `problemi` porta `gravita` (alta | media | bassa), l'`area` interessata, il `messaggio` e quando possibile l'`azione` suggerita.\n\nLa risposta e' sempre limitata al tenant dell'utente autenticato.\n\n## Su quale periodo contano i numeri\n\nIl parametro `mese` restringe a UN mese di competenza i blocchi che contano POD-mese: `qualita`, `fatturazione`, `aggregati.pod_mese_consolidati` e `qualita.giornate_parziali`. Senza `mese` — oppure con `mese=tutto` — si conta su **tutti i mesi presenti**, comprese le rettifiche che i flussi portano su periodi vecchi.\n\nLa differenza non e marginale: su tutto l'archivio sono 1.038.801 POD-mese di cui 360.001 quadrati (34,7%), mentre il solo 2026-09 ne ha 161.694 con 25.214 quadrati (15,6%). Chiedere senza `mese` e leggere il risultato come se fosse del mese scelto e lo scambio piu facile da fare, ed e il motivo per cui la risposta dichiara sempre un blocco `periodo`.\n\n`archivio.file_troncati` conta gli archivi **letti solo in parte**: il contenuto e su S3 ma non e entrato. Non e un file illeggibile — non manca niente alla fonte, manca una rilettura — quindi finche non la si fa i consumi dei giorni coinvolti sono in difetto senza che i numeri lo dicano. Compare anche fra i `problemi` con gravita **alta** e area `archivio`. Questi file restano in stato `troncato` e NON `processed`, percio una rielaborazione li riprende da sola.\n\n**Non** dipendono da `mese`, perche sono lo stato di adesso e non appartengono a un mese di competenza: `ingest`, `archivio`, `trasformazione`, `parco_pod` e le voci di `problemi` nelle aree `ingest`, `archivio`, `trasformazione`, `configurazione` e `catalogo`. Le voci di `problemi` in area `qualita` e `fatturazione`, invece, seguono il periodo.",
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "mese",
            "in": "query",
            "required": false,
            "description": "Mese di competenza `YYYY-MM` su cui contare qualita, aggregati e fatturazione. Omesso, oppure `tutto`, conta su tutti i mesi presenti. Un valore con una forma diversa e rifiutato con `400 mese_non_valido`: accettarlo silenziosamente vorrebbe dire restituire il totale sotto l'etichetta di un mese.",
            "schema": {
              "type": "string",
              "example": "2026-09"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stato della pipeline",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "tenant": "TENANT_NAME",
                    "salute": "attenzione",
                    "periodo": {
                      "mese": "2026-09",
                      "etichetta": "2026-09"
                    },
                    "ingest": {
                      "modalita": "both",
                      "attivo": true,
                      "credenziali_impostate": true,
                      "ultimo_scarico": "2026-09-10T04:56:51",
                      "ore_da_ultimo_scarico": 0.4,
                      "file_in_coda": 0
                    },
                    "archivio": {
                      "file_registrati": 61113,
                      "file_in_errore": 1,
                      "file_troncati": 0,
                      "ultimo_elaborato": "2026-09-10T02:11:13"
                    },
                    "trasformazione": {
                      "ultima_elaborazione": "2026-09-10T02:09:32",
                      "ore_da_ultima": 3.2
                    },
                    "aggregati": {
                      "pod": 9641,
                      "righe_giornaliere": 7655970,
                      "righe_orarie": 1198144,
                      "letture_indice": 455,
                      "mesi_consolidati": 110663,
                      "primo_giorno": "2023-11-29",
                      "ultimo_giorno": "2026-09-09"
                    },
                    "qualita": {
                      "mesi_quadrati": 100364,
                      "mesi_da_verificare": 10299,
                      "percentuale_quadrati": 90.7,
                      "mwh_quadrati": 20366.0,
                      "mwh_da_verificare": 1594.0,
                      "tipo_lettura": {
                        "stimata": 2195,
                        "effettiva": 107352,
                        "mista": 1116
                      },
                      "giornate_parziali": 211
                    },
                    "fatturazione": {
                      "per_stato": {
                        "da_fatturare": 110663
                      },
                      "conguagli_aperti": 0,
                      "conguagli_su_periodi_fatturati": 0,
                      "delta_kwh_da_conguagliare": 0.0
                    },
                    "problemi": [
                      {
                        "gravita": "media",
                        "area": "archivio",
                        "messaggio": "1 file in stato di errore",
                        "azione": null
                      },
                      {
                        "gravita": "bassa",
                        "area": "qualità",
                        "messaggio": "211 giornate incomplete: il consumo di quei giorni è in difetto",
                        "azione": null
                      },
                      {
                        "gravita": "bassa",
                        "area": "qualità",
                        "messaggio": "3311 mesi con letture stimate o miste: in bolletta vanno dichiarati come tali",
                        "azione": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/uploads": {
      "post": {
        "tags": [
          "Metering · Ingest"
        ],
        "summary": "Richiede un presigned URL per caricare file misure su S3",
        "description": "Il client carica il file/bulk ZIP direttamente sull'URL restituito (scoped al prefisso del tenant).",
        "security": [
          {
            "jwt": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "URL presigned emesso",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "upload_id": {
                          "type": "string",
                          "example": "up_8f2a"
                        },
                        "url": {
                          "type": "string",
                          "description": "presigned S3 URL (scade in ~15 min)"
                        },
                        "scade_tra_s": {
                          "type": "integer",
                          "example": 900
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "upload_id": "up_a6bb5716942c",
                    "url": "https://dev1-metering-landing-ACCOUNT_ID.s3.eu-south-1.amazonaws.com/landing/tenant%3DTENANT_NAME/source%3Dmanual/2026/09/misure_agosto.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
                    "key": "landing/tenant=TENANT_NAME/source=manual/2026/09/misure_agosto.zip",
                    "scade_tra_s": 900
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "filename": {
                    "type": "string",
                    "example": "misure_agosto.zip",
                    "description": "Nome del file che verrà caricato. Se omesso ne viene generato uno dall'upload_id. Determina la chiave S3, quindi anche il nome con cui il file comparirà nel registro."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/offerta_commerciale": {
      "get": {
        "tags": [
          "Offerta Commerciale"
        ],
        "summary": "Lista offerte commerciali (istanze per POD)",
        "operationId": "listOfferta",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "pf_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "contratto_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cte_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cliente_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vigenza",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "vigente",
                "scaduta"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OffertaCommerciale"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Offerta Commerciale"
        ],
        "summary": "Crea offerta commerciale su POD",
        "operationId": "createOfferta",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OffertaCommerciale"
              },
              "example": {
                "pf_id": 382,
                "cte_id": 1,
                "contratto_id": 164,
                "data_firma": "2026-05-17",
                "modalita_firma": "OTP",
                "esecuzione_anticipata": false,
                "data_inizio": "2026-05-17"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OffertaCommerciale"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offerta_commerciale/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Offerta Commerciale"
        ],
        "summary": "Detail offerta commerciale",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OffertaCommerciale"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Offerta Commerciale"
        ],
        "summary": "Update offerta commerciale",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OffertaCommerciale"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "delete": {
        "tags": [
          "Offerta Commerciale"
        ],
        "summary": "Cancella offerta commerciale",
        "description": "409 `offerta_vigente` se `data_fine IS NULL`. Cessa prima via PUT con `data_fine`.",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/pagamenti": {
      "get": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Elenco pagamenti (filtri fattura_id, stato)",
        "operationId": "listPagamenti",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "fattura_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/pagamenti/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "put": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Aggiorna stato pagamento (conferma/storna)",
        "operationId": "updatePagamento",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResourceEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/punti_fornitura": {
      "get": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Lista POD/PDR",
        "operationId": "listPuntiFornitura",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search per codice POD (substring)"
          },
          {
            "name": "tipo",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "EE",
                "GAS"
              ]
            }
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "attivo",
                "sospeso",
                "cessato"
              ]
            }
          },
          {
            "name": "cliente_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista POD",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PuntoFornitura"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Crea POD + intestazione attiva",
        "description": "Crea un POD/PDR e contestualmente l'intestazione attiva.\n`codice` UNIQUE tenant-wide → 409 `punto_fornitura_already_exists` se duplicato.\n",
        "operationId": "createPuntoFornitura",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PuntoFornitura"
              },
              "example": {
                "cliente_id": "C8F6189ED",
                "codice": "IT001E12345678",
                "tipo": "EE",
                "distributore": "E-Distribuzione",
                "potenza_impegnata": 4.5,
                "indirizzo": "Via Roma",
                "civico": "1",
                "cap": "00100",
                "comune_nome": "Roma",
                "provincia": "RM",
                "motivo_inizio": "ATTIVAZIONE",
                "data_inizio": "2026-05-11"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "POD creato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PuntoFornitura"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "cliente_id non esistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Codice POD/PDR già esistente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "conflict_error",
                    "code": "punto_fornitura_already_exists",
                    "message": "Punto di fornitura 'IT001E12345678' gia' anagraficato a sistema",
                    "details": {
                      "punto_esistente": {
                        "id": 100,
                        "codice": "IT001E12345678",
                        "tipo": "EE",
                        "url": "/v1/punti_fornitura/100"
                      },
                      "intestazione_attiva": {
                        "cliente_id": "C8F6189ED",
                        "cliente_nome": "Mario Rossi"
                      },
                      "hint": "Cambia intestatario via POST .../voltura o /switching"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/punti_fornitura/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          },
          "example": 38
        }
      ],
      "get": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Detail POD + storia intestazioni",
        "operationId": "getPuntoFornitura",
        "responses": {
          "200": {
            "description": "POD trovato",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PuntoFornitura"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Cancella POD (soft-delete)",
        "description": "409 se ha intestazioni attive o CTE vigenti. `?cascade=true` cancella intestazioni + CTE collegate.",
        "parameters": [
          {
            "name": "cascade",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/v1/punti_fornitura/{id}/cessazione": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Cessazione totale del POD",
        "description": "Chiude tutte le intestazioni attive (`motivo_fine=CESSAZIONE`) e setta `stato=cessato`.",
        "operationId": "cessazione",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "data"
                ],
                "properties": {
                  "data": {
                    "type": "string",
                    "format": "date"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionResult"
                }
              }
            }
          }
        }
      }
    },
    "/v1/punti_fornitura/{id}/dati_tecnici": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Storia cronologica dati tecnici",
        "description": "Ogni record ha `data_inizio` / `data_fine` (NULL = vigente).\nSchema differente per `EE` (POD), `GAS` (PDR) e `REMI` (grandi industriali).\n",
        "operationId": "listDatiTecnici",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Crea nuovo record di storia dati tecnici",
        "description": "Crea nuovo record. Chiude automaticamente il precedente.",
        "operationId": "createDatiTecnici",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/DatiTecniciEE"
                  },
                  {
                    "$ref": "#/components/schemas/DatiTecniciGAS"
                  }
                ]
              },
              "examples": {
                "ee": {
                  "summary": "POD elettrico domestico",
                  "value": {
                    "data_inizio": "2026-05-13",
                    "tipologia_uso": "DOMESTICO",
                    "tensione": "BT",
                    "volt": 230,
                    "opzione_tariffaria": "TDR",
                    "residente": true,
                    "potenza_impegnata": 3.0,
                    "potenza_disponibile": 3.3,
                    "consumo_annuo_stimato": 2700
                  }
                },
                "gas": {
                  "summary": "PDR gas domestico",
                  "value": {
                    "data_inizio": "2026-05-13",
                    "tipologia_uso": "DOMESTICO",
                    "categoria_uso": "C3",
                    "residente": true,
                    "classe_misuratore": "G4",
                    "matricola_misuratore": "M12345",
                    "telegestione": false,
                    "disalimentabile": true,
                    "codice_remi": "REMI-12-345",
                    "consumo_annuo": 1200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Record creato"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/v1/punti_fornitura/{id}/dati_tecnici/corrente": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Record vigente dati tecnici",
        "operationId": "getDatiTecniciCorrente",
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "Nessun record vigente",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/punti_fornitura/{id}/switching": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Switching IN (cliente prende il POD da altro fornitore)",
        "description": "Chiude eventuale intestazione attiva (`motivo_fine=SWITCHING_OUT`) e apre la nuova (`motivo_inizio=SWITCHING`).\nSe il POD era `cessato` torna a `attivo`.\n",
        "operationId": "switching",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cliente_id",
                  "data"
                ],
                "properties": {
                  "cliente_id": {
                    "type": "string"
                  },
                  "data": {
                    "type": "string",
                    "format": "date"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionResult"
                }
              }
            }
          }
        }
      }
    },
    "/v1/punti_fornitura/{id}/voltura": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "post": {
        "tags": [
          "Punti di fornitura"
        ],
        "summary": "Voltura (cambio intestatario)",
        "description": "Chiude l'intestazione INTESTATARIO attiva (`motivo_fine=VOLTURA`) e apre la nuova\nper `cliente_id_nuovo` (`motivo_inizio=VOLTURA`).\n",
        "operationId": "voltura",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "cliente_id_nuovo",
                  "data"
                ],
                "properties": {
                  "cliente_id_nuovo": {
                    "type": "string",
                    "example": "CF5159EF0"
                  },
                  "data": {
                    "type": "string",
                    "format": "date",
                    "example": "2026-05-01"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Voltura eseguita",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionResult"
                }
              }
            }
          }
        }
      }
    },
    "/v1/roles": {
      "get": {
        "tags": [
          "RBAC"
        ],
        "summary": "Elenca i tipi-utenza (ruoli) del tenant",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      },
      "post": {
        "tags": [
          "RBAC"
        ],
        "summary": "Crea un tipo-utenza (ruolo)",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role_id": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/roles/{id}": {
      "put": {
        "tags": [
          "RBAC"
        ],
        "summary": "Modifica un ruolo",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "RBAC"
        ],
        "summary": "Elimina un ruolo",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      }
    },
    "/v1/scadenzario": {
      "get": {
        "tags": [
          "Fatturazione"
        ],
        "summary": "Elenco scadenze (filtri scadute, contratto_id, stato)",
        "operationId": "listScadenzario",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "scadute",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "contratto_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/scopes": {
      "get": {
        "tags": [
          "RBAC"
        ],
        "summary": "Catalogo scope disponibili",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      }
    },
    "/v1/users": {
      "get": {
        "tags": [
          "RBAC"
        ],
        "summary": "Elenca utenti del tenant",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      },
      "post": {
        "tags": [
          "RBAC"
        ],
        "summary": "Crea identità utente",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/users/{sub}": {
      "get": {
        "tags": [
          "RBAC"
        ],
        "summary": "Dettaglio utente + grant",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      },
      "delete": {
        "tags": [
          "RBAC"
        ],
        "summary": "Rimuove l'utente dal tenant (disabilita Cognito se orfano)",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      }
    },
    "/v1/users/{sub}/disable": {
      "post": {
        "tags": [
          "RBAC"
        ],
        "summary": "Sospende l'utente nel tenant",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      }
    },
    "/v1/users/{sub}/enable": {
      "post": {
        "tags": [
          "RBAC"
        ],
        "summary": "Riattiva l'utente nel tenant",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      }
    },
    "/v1/users/{sub}/grants/{persona}": {
      "put": {
        "tags": [
          "RBAC"
        ],
        "summary": "Assegna area (persona) + ruoli/scope",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "persona",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string"
                  },
                  "resource_id": {
                    "type": "string"
                  },
                  "roles": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "scopes_extra": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "modules": {
                    "type": "object"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "RBAC"
        ],
        "summary": "Revoca un'area (persona)",
        "parameters": [
          {
            "name": "X-Tenant-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Tenant attivo (identità multi-tenant). Se assente si usa il tenant del token."
          },
          {
            "name": "sub",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "persona",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "403": {
            "description": "Scope mancante"
          }
        }
      }
    },
    "/v1/utenti": {
      "get": {
        "tags": [
          "Utenti"
        ],
        "summary": "Lista utenti del tenant",
        "description": "Endpoint accessibile **solo via API key** (`dmk_*`).",
        "operationId": "listUtenti",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "ruolo",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "admin",
                "user"
              ]
            }
          },
          {
            "name": "stato",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "attivo",
                "invitato",
                "sospeso"
              ]
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Utente"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Utenti"
        ],
        "summary": "Crea nuovo utente (invia invito Cognito)",
        "description": "Stato iniziale `invitato` (email di invito con password temporanea).\nComportamento \"ghost user\": se l'email è in Cognito senza tenant, l'utente viene ricreato in-place.\n",
        "operationId": "createUtente",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "ruolo"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "ruolo": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "user"
                    ]
                  },
                  "permissions": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "enum": [
                        "r",
                        "rw"
                      ]
                    }
                  }
                }
              },
              "example": {
                "email": "nuovo@example.com",
                "ruolo": "user",
                "permissions": {
                  "customers": "rw",
                  "jobs": "r"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Utente creato in stato `invitato`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Utente"
                }
              }
            }
          },
          "400": {
            "description": "invalid_email / invalid_ruolo",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "email_taken (l'email appartiene a un altro tenant)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/utenti/{email}": {
      "parameters": [
        {
          "name": "email",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "email"
          },
          "description": "URL-encoded (es. `mario.rossi%40example.com`)"
        }
      ],
      "get": {
        "tags": [
          "Utenti"
        ],
        "summary": "Detail utente",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "operationId": "getUtente",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Utente"
                }
              }
            }
          },
          "404": {
            "description": "utente_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Utenti"
        ],
        "summary": "Update ruolo / stato / permissions",
        "description": "Body parziale. `permissions` SOSTITUISCE completamente la mappa precedente.\nSospendere un utente lo disabilita anche in Cognito.\n\n**Protezione \"ultimo admin attivo\"**: non puoi sospendere o degradare l'unico admin attivo → `409 last_admin`.\n",
        "operationId": "updateUtente",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ruolo": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "user"
                    ]
                  },
                  "stato": {
                    "type": "string",
                    "enum": [
                      "attivo",
                      "sospeso"
                    ]
                  },
                  "permissions": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "enum": [
                        "r",
                        "rw"
                      ]
                    }
                  }
                }
              },
              "example": {
                "ruolo": "admin",
                "permissions": {
                  "customers": "rw",
                  "jobs": "rw",
                  "billing": "rw",
                  "reports": "rw",
                  "audit": "r"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Utente"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "utente_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "last_admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Utenti"
        ],
        "summary": "Cancella utente (Cognito + DB)",
        "description": "Hard delete da Cognito e DynamoDB master. Stessa protezione \"ultimo admin attivo\" del PUT.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "description": "utente_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "last_admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/zone_geografiche": {
      "get": {
        "tags": [
          "Zone geografiche"
        ],
        "summary": "Lista zone catalog",
        "operationId": "listZone",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ListEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ZonaGeografica"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Zone geografiche"
        ],
        "summary": "Crea zona + aree",
        "description": "Validazione anti-ridondanza: l'API rifiuta payload con regione + provincia/comune di quella regione\nnello stesso `tipo_match`. La combo include+exclude (es. \"Lombardia tranne MI\") è ammessa.\n",
        "operationId": "createZona",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ZonaGeografica"
              },
              "examples": {
                "standard": {
                  "summary": "Zona con regioni multiple",
                  "value": {
                    "codice": "NORD_CUSTOM",
                    "nome": "Nord custom",
                    "aree": [
                      {
                        "codice_regione": "01",
                        "tipo_match": "include"
                      },
                      {
                        "codice_regione": "03",
                        "tipo_match": "include"
                      }
                    ]
                  }
                },
                "include_exclude": {
                  "summary": "Lombardia tranne Milano",
                  "value": {
                    "codice": "LOMB_NO_MI",
                    "nome": "Lombardia tranne Milano",
                    "aree": [
                      {
                        "codice_regione": "03",
                        "tipo_match": "include"
                      },
                      {
                        "sigla_provincia": "MI",
                        "tipo_match": "exclude"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Zona creata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ZonaGeografica"
                }
              }
            }
          },
          "400": {
            "description": "Ridondanza / area inesistente / campo mancante",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "invalid_request_error",
                    "code": "invalid_value",
                    "message": "Aree ridondanti rilevate: Ridondanza (include): provincia 'MI' e' gia' coperta dalla regione '03' nella stessa zona",
                    "param": "aree"
                  }
                }
              }
            }
          },
          "409": {
            "description": "zona_already_exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/zone_geografiche/{codice}": {
      "parameters": [
        {
          "name": "codice",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[A-Z0-9_]{2,40}$"
          }
        }
      ],
      "get": {
        "tags": [
          "Zone geografiche"
        ],
        "summary": "Detail zona con aree espanse",
        "operationId": "getZona",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ZonaGeografica"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Zone geografiche"
        ],
        "summary": "Update zona (replace-all delle aree se presenti)",
        "description": "Se `aree` non è nel body, le aree esistenti restano invariate.",
        "operationId": "updateZona",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ZonaGeografica"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ZonaGeografica"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Zone geografiche"
        ],
        "summary": "Cancella zona",
        "description": "Hard delete della zona; le aree associate sono rimosse in CASCADE.",
        "operationId": "deleteZona",
        "responses": {
          "204": {
            "$ref": "#/components/responses/NoContent"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/metering/panoramica": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Tutto lo stato in una chiamata: sistema, attivita', diario",
        "description": "Unisce in una risposta sola quello che altrimenti richiede tre chiamate:\n`/stato` (com'e' messo il sistema), `/attivita` (cosa gira adesso) e `/eventi`\n(cosa e' successo).\n\n**Pensato per chi integra**, non per il pannello. Le tre viste restano separate\nperche' hanno costi diversi: `/attivita` interroga solo Glue e S3 e si puo'\nchiamare ogni pochi secondi, `/stato` fa una dozzina di interrogazioni su\nAurora. Il pannello usa quelle; questa serve a chi vuole una fotografia\ncompleta senza ricucire tre risposte.\n\n**Cache di 30 secondi.** Il campo `da_cache` dice se la risposta arriva dalla\ncache. Non e' un'ottimizzazione: il database va in pausa quando nessuno lo usa,\ne una panoramica chiamata in ciclo stretto lo terrebbe sveglio — un'automazione\nche lo interrogava ogni quarto d'ora bastava a impedirne la pausa, e valeva\ncirca 150 USD al mese.\n\nGli eventi riportano `origine`: **`manuale`** per i file caricati dal pannello,\n**`automatico`** per quelli scaricati dal SII.",
        "responses": {
          "200": {
            "description": "La fotografia completa del tenant",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "tenant": "TENANT_NAME",
                    "salute": "attenzione",
                    "aggiornato": "2026-10-09T11:40:00+00:00",
                    "ttl_cache_s": 30,
                    "da_cache": false,
                    "adesso": {
                      "corse_in_corso": 0,
                      "in_corso": [],
                      "coda": {
                        "file_non_ancora_letti": 0,
                        "gb_non_ancora_letti": 0
                      }
                    },
                    "catena": {
                      "caricamento": {
                        "modalita": "manual",
                        "file_in_coda": 0
                      },
                      "lettura": {
                        "file_registrati": 27,
                        "file_in_errore": 0
                      },
                      "trasformazione": {
                        "ore_da_ultima": 0.4
                      },
                      "aggregati": {
                        "pod": 177919,
                        "righe_giornaliere": 48658077
                      }
                    },
                    "qualita": {
                      "percentuale_quadrati": 26.9
                    },
                    "fatturazione": {
                      "per_stato": {
                        "quadrato": 46522
                      }
                    },
                    "problemi": [],
                    "ultimi_eventi": [
                      {
                        "quando": "2026-10-09T11:13:00+00:00",
                        "fase": "caricamento",
                        "esito": "concluso",
                        "messaggio": "File caricati a mano e verificati su S3",
                        "origine": "manuale"
                      }
                    ],
                    "ultime_corse": []
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/file": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Archivi caricati, con dimensione e stato",
        "description": "Gli archivi a registro per il tenant, con la **dimensione letta da S3** (che\nnon e' conservata a registro) e lo stato di elaborazione.\n\n`presente_su_s3` a `false` significa che il file risulta elaborato ma l'oggetto\nnon c'e' piu' nel bucket: normale per i tenant con pulizia del landing, da\nguardare altrimenti.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "stato",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "processed",
                "ignored",
                "error"
              ]
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "ricerca parziale sul nome",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Elenco degli archivi",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "object": "list",
                    "quanti": 1,
                    "file": [
                      {
                        "nome": "2026-09-11_2026-09-13.zip",
                        "origine": "manuale",
                        "stato": "processed",
                        "byte": 2770731008,
                        "dimensione": "2.58 GB",
                        "ricevuto": "2026-10-09T11:13:00+00:00",
                        "elaborato": "2026-10-09T11:13:00+00:00",
                        "errore": null,
                        "presente_su_s3": true
                      }
                    ]
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    },
    "/v1/metering/file/{nome}": {
      "get": {
        "tags": [
          "Metering · Consultazione"
        ],
        "summary": "Cosa conteneva un archivio",
        "description": "Il dettaglio di un singolo archivio: oltre ai dati del caricamento,\n**cosa c'era dentro** — quanti file XML, quante letture ne sono state estratte,\nquanti POD, su quale intervallo di date, e la ripartizione per flusso SII.\n\n**Il contenuto non e' conservato da nessuna parte.** Nessuno tiene l'elenco dei\nfile dentro un archivio da due gigabyte. Si ricostruisce qui dalle letture su\nParquet: ogni lettura porta il nome dell'archivio da cui viene\n(`file_contenitore`) e quello dell'XML al suo interno (`file_nome`). Costa\nqualche secondo di Athena e si paga solo quando serve, invece di mantenere per\nsempre un registro allineato.\n\n**L'intervallo delle misure puo' andare molto indietro** rispetto al nome\ndell'archivio: i flussi del SII contengono anche le rettifiche di mesi\nprecedenti. Un archivio «2026-09-11_2026-09-13» con misure dal 2024-12-31 non\ne' un errore.\n\n`file_interni_esempio` sono i primi 40 nomi in ordine alfabetico, non tutti.",
        "parameters": [
          {
            "name": "nome",
            "in": "path",
            "required": true,
            "description": "nome dell'archivio, come a registro",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-11_2026-09-13.zip"
          }
        ],
        "responses": {
          "200": {
            "description": "Dettaglio dell'archivio",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "nome": "2026-09-11_2026-09-13.zip",
                    "origine": "manuale",
                    "stato": "processed",
                    "dimensione": "2.58 GB",
                    "ricevuto": "2026-10-09T11:13:00+00:00",
                    "elaborato": "2026-10-09T11:13:00+00:00",
                    "contenuto": {
                      "letture": 760636,
                      "file_interni": 11340,
                      "pod": 126294,
                      "dal": "2024-12-31",
                      "al": "2026-09-12"
                    },
                    "flussi": [
                      {
                        "flusso": "PDO2G",
                        "letture": 726543
                      },
                      {
                        "flusso": "RFO2G",
                        "letture": 30215
                      }
                    ],
                    "file_interni_esempio": [
                      "00000000000_00000000000_202609_PDO2G_20260909184213_0000DP0000_R.xml"
                    ],
                    "nota": "Il contenuto e' ricostruito dalle letture su Parquet."
                  }
                }
              }
            }
          },
          "404": {
            "description": "archivio non a registro per questo tenant"
          },
          "503": {
            "$ref": "#/components/responses/DatabaseInRisveglio"
          }
        }
      }
    }
  }
}
