# Design — Modulo Questionari Fornitori + Portale Fornitore

> Stato: **DESIGN DEFINITIVO da approvare** (nessun codice scritto). Da rivedere prima dell'implementazione.
> Autore: sessione 2026-05-31. Origine: richiesta utente (Cristiano Benassati).
> **Decisioni utente confermate**: (1) categorie **predefinite + personalizzabili**; (2) auth fornitore via **magic-link / OTP email** (NO password); (3) scadenze/ricorrenze **configurabili** (nessun default imposto); (4) portale sullo **stesso dominio** `nis2.agile.software` → pagina `supplier-portal.html`.
> Base esistente riusata: `SupplyChainController` (self-assessment link-token, P3), `supplier-assessment.html`, tabella `supplier_questionnaires` (mig. 027), relay email AgileHub (`/api/emails/send-raw`, `X-Internal-Key`).

---

## 1. Obiettivo

Evolvere il self-assessment fornitori (oggi: 8 domande hardcoded, link usa-e-getta) in un **modulo questionari configurabile** con **portale fornitore** ad accesso OTP, che gestisce **categorie**, **domande estensibili** (anche per procedure interne), **scadenze/ricorrenze** e **aggiornamenti nel tempo**.

Valore: due-diligence continua dei fornitori (Art. 21.2(d) NIS2; coerente con ENISA supply chain / NIST 800-161), riutilizzabile anche per procedure interne (privacy, 231, ESG, qualità).

---

## 2. Cosa esiste già (riuso)

| Esistente | Riuso nel nuovo modulo |
|---|---|
| `SupplyChainController::sendQuestionnaire` (link-token sha256 + scadenza) | base per generare l'accesso OTP/campagna |
| `publicQuestionnaire` / `submitPublicQuestionnaire` (no-auth via token) | base compilazione online |
| `supplier-assessment.html` (pagina pubblica) | diventa il **portale fornitore** dinamico |
| relay email `/api/emails/send-raw` + `X-Internal-Key` | invio OTP e notifiche scadenza |
| pattern token `sq_` + sha256 + scadenza + single-use | riusato per OTP e accesso |
| costante `QUESTIONNAIRE` (8 domande) | migrata in DB come template "NIS2 base" di default |

---

## 3. Autenticazione fornitore — MAGIC-LINK / OTP (scelta utente)

**Principio**: nessuna password fornitore (zero credenziali da custodire/rubare). Accesso a ogni sessione tramite codice/temporale via email.

### Flusso
1. L'azienda registra il fornitore (anagrafica) con **email referente**.
2. Quando serve accesso (campagna o login spontaneo), il fornitore va su `supplier-portal.html` e inserisce la **propria email** → riceve un **magic-link** (token `sp_` + 32 byte) **e/o un codice OTP a 6 cifre**, validi ~15 minuti, single-use.
3. Cliccando il link / inserendo l'OTP, si apre una **sessione fornitore** (token di sessione separato, durata breve, es. 2h) limitata al **solo** profilo di quel fornitore.
4. Nel portale il fornitore vede: anagrafica propria, questionari assegnati (aperti/scaduti/completati), scadenze, storico.

### Sicurezza (vincolante)
- Tabella **separata** `supplier_users` — MAI in `users` interni. Nessun ruolo interno, nessun accesso a dati di altri fornitori o dell'organizzazione cliente.
- OTP: hash in DB (mai in chiaro), scadenza 15 min, max 5 tentativi, rate-limit per email/IP, invalidazione dopo uso.
- Sessione fornitore: JWT dedicato con claim `supplier_user_id` + `supplier_id` (NON `user_id`); scope whitelisted ai soli endpoint `/api/supplier-portal/*`.
- Enumerazione: risposta sempre "se l'email è registrata riceverai il codice" (opaca).
- Audit: login OTP, apertura/compilazione questionario loggati (`logAudit`).
- GDPR: l'email del referente è dato personale → retention + cancellazione su richiesta.

---

## 3.5 Caricamento anagrafica fornitori (per-azienda-cliente: CSV + API key) — requisito utente 2026-05-31

**Principio**: i fornitori NON si auto-registrano. L'**anagrafica fornitori è di proprietà dell'azienda cliente** ed è **diversa per ogni org**. L'azienda cliente popola la propria lista fornitori; solo dopo, il singolo referente fornitore accede via OTP/magic-link (§3) per compilare i questionari a lui assegnati.

### Tre canali di ingestion (lato azienda cliente)
1. **Manuale** — form esistente sulla scheda fornitore (`supply-chain.html`). Già presente.
2. **Import CSV/Excel** — upload file → preview/mapping colonne → `bulkUpsert` idempotente. **Riuso del pattern già implementato** `AssetController::import()` + `bulkUpsert()` (mig. 025 asset `external_ref`/`discovery_source`). Dedup per `(organization_id, vat_number)` o `(organization_id, external_ref)`.
3. **API key all'azienda cliente** — integrazione programmatica (es. il gestionale/ERP del cliente spinge i fornitori). **Riuso dell'infra esistente** `api_keys` + pattern `X-API-Key` di `ServicesController` (come gli endpoint `ingestAssets`/`ingestIncident` già scritti). Nuovo scope dedicato **`write:suppliers`**. Endpoint upsert idempotente per `external_ref`.

### Impatto schema (additivo su `suppliers`)
- `external_ref VARCHAR NULL` — chiave di upsert idempotente dal sistema sorgente del cliente (come `assets.external_ref`).
- `source ENUM('manual','csv','api') DEFAULT 'manual'` — provenienza del record (tracciabilità/audit).
- `UNIQUE(organization_id, external_ref)` per evitare doppioni in re-import/re-push (NULL multipli ammessi → i record manuali senza ref non confliggono).

### API (lato azienda)
- `POST /api/supply-chain/import` (JWT interno) — body CSV/JSON, `bulkUpsert` con report righe ok/scartate (pattern asset import).
- `POST /api/services/suppliers` (X-API-Key, scope `write:suppliers`) — upsert idempotente per `external_ref`; un'API key è **scoped alla singola org** (come tutte le `api_keys`), hashed, revocabile, con audit.

### Sicurezza
- API key per-org, hashed in DB, scope minimo `write:suppliers`, revocabile, scadenza; ogni write logga `logAudit`. **Mai** scope cross-org.
- CSV: validazione tipo/encoding, dimensione massima, sanitizzazione campi (no formula-injection in celle `=/+/-/@`), dedup, nessuna esecuzione lato server.
- `external_ref` è opaco dal lato cliente: non usarlo mai per costruire path/query non parametrizzate.

> **Distinzione chiave**: questo §3.5 è il caricamento **dell'anagrafica** (lato azienda, API key/CSV/manuale). Il §3 è l'**accesso del referente fornitore** (OTP/magic-link) per compilare i questionari. Due piani separati, due meccanismi di auth separati.

---

## 4. Schema dati (migrazioni nuove, additive)

```
supplier_categories
  id, organization_id (NULL = preset di sistema), name, slug, description, active
  -- SEED predefinito (personalizzabile): Cloud/IaaS, MSP/MSSP, Software/SaaS,
  --   Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro

questionnaire_templates
  id, organization_id, category_id (FK, NULL = tutte le categorie),
  name, version, scope ENUM('nis2','privacy','quality','231','esg','custom'),
  recurrence_months (NULL = una tantum, configurabile),
  due_days_default (NULL = nessun default), is_active, created_by, created_at
  -- versioning come policy_versions: snapshot a ogni pubblicazione

questionnaire_questions
  id, template_id, order_index, code, text,
  type ENUM('yes_no_partial','scale_1_5','single_choice','multi_choice','text','number','file'),
  options JSON, weight, required, help_text

questionnaire_campaigns
  id, organization_id, supplier_id, template_id, template_version,
  status ENUM('draft','sent','in_progress','completed','expired','overdue'),
  access_token_hash, sent_to_email, sent_at,
  due_at (NULL ammesso), recurrence_months, next_recurrence_at,
  reminder_offsets JSON (es. [-14,-7,-1]; configurabile), next_reminder_at, reminder_count,
  score, risk_level, completed_at

questionnaire_answers
  id, campaign_id, question_id, answer_value, answer_text, file_ref, answered_at

supplier_users           -- account fornitore (OTP, no password)
  id, supplier_id, email, display_name, status, last_login_at, created_at

supplier_otp
  id, supplier_user_id, otp_hash, magic_token_hash, purpose,
  expires_at, used_at, attempts, ip_created
```

`suppliers` esistente: aggiungere `category_id` (FK) — `contact_email` già presente.

---

## 5. API

### Lato azienda (JWT interno)
- `GET/POST/PUT/DELETE /api/supply-chain/categories`
- `GET/POST/PUT /api/supply-chain/templates` (+ versioning)
- `GET/POST/PUT/DELETE /api/supply-chain/templates/{id}/questions`
- `POST /api/supply-chain/{supplierId}/campaigns` (scegli template per categoria, due_at, ricorrenza, reminder)
- `GET /api/supply-chain/campaigns` (cruscotto stato/scadenze)

### Lato fornitore (portale, sessione OTP — namespace dedicato `/api/supplier-portal/*`)
- `POST /api/supplier-portal/request-otp` (body: email) → invia OTP/magic-link (risposta opaca)
- `POST /api/supplier-portal/verify-otp` (email + codice) → sessione fornitore
- `GET /api/supplier-portal/me` → anagrafica + questionari assegnati + scadenze
- `GET /api/supplier-portal/questionnaire/{campaignId}` → template dinamico da compilare
- `POST /api/supplier-portal/questionnaire/{campaignId}/submit` → salva risposte + scoring

### Cron scadenze (`scripts/supplier-questionnaire-cron.sh`, TZ Europe/Rome)
- invia reminder agli offset configurati, marca `overdue`, apre la ricorrenza alla scadenza.

---

## 6. UI

- **supply-chain.html** (lato azienda): tab "Categorie", "Template questionari" (editor domande ordinabile), "Campagne/scadenze" (cruscotto con semaforo in-regola/in-scadenza/scaduto). Sulla scheda fornitore: dropdown categoria + "Avvia campagna".
- **supplier-portal.html** (NUOVA, stesso dominio): step OTP (inserisci email → inserisci codice) → dashboard fornitore (questionari assegnati, scadenze, storico) → compilazione dinamica del template. `noindex,nofollow`, nessun link all'app interna.
- **supplier-assessment.html**: confluisce nel portale o resta come fallback link-token diretto.

---

## 7. Fasi incrementali

| Fase | Contenuto | Rischio |
|---|---|---|
| **0 (PREREQUISITO)** | `EmailService::sendViaRelay()` via relay AgileHub (`X-Internal-Key`, cURL come `SsoHelper::postInternal`, env multi-source per clear_env FPM). Ripuntare `sendQuestionnaire`/`forgotPassword` (oggi usano `mail()` → falliscono in silenzio). **Bloccante: senza, OTP/reminder/inviti non recapitano.** | Basso ma bloccante |
| **1** | Categorie (seed + CRUD) + template/domande configurabili in DB (migra le 8 domande) + render dinamico. Scrivere già le risposte in `questionnaire_answers` (campaign-stub) per non migrare in Fase 2. | Basso |
| **2** | Campagne con scadenze/ricorrenze configurabili + reminder (cron) + cruscotto. | Medio |
| **3** | **Portale fornitore + auth OTP/magic-link** (`supplier_users`, `supplier_otp`, `/api/supplier-portal/*`, supplier-portal.html). | Alto (auth esterna) |
| **4** | Tipi domanda avanzati (allegati/scale/multi) + scoring configurabile per peso. | Medio |

Fasi 1-2 danno già categorie + questionari configurabili + scadenze. Fase 3 aggiunge il portale OTP persistente.

---

## 8. Punti di sicurezza non negoziabili
- `supplier_users`/sessione fornitore totalmente isolati da `users` interni (no IDOR cross-fornitore, no accesso dati org).
- OTP hashed, scadenza breve, rate-limit, risposta opaca.
- Segreti/eventuali integrazioni nel vault, non in DB/Git.
- Tutti gli endpoint `/api/supplier-portal/*` validano che il `campaignId`/risorsa appartenga al `supplier_id` della sessione.

---

## 9. Stima (multi-sessione)
- Fase 1: ~1 sessione · Fase 2: ~1 sessione · Fase 3 (portale+OTP): ~1.5-2 sessioni · Fase 4: ~0.5.
Tutto additivo; nessuna modifica distruttiva ai moduli esistenti.

---

## 10. Decisioni APPROVATE (utente, 2026-05-31)
1. ✅ Set categorie predefinite confermato (Cloud/IaaS, MSP/MSSP, Software/SaaS, Hardware/Manutenzione, Consulenza, Logistica, Telecomunicazioni, Altro) — personalizzabili.
2. ✅ Auth: **OTP a 6 cifre + magic-link, ENTRAMBI** (il fornitore può usare l'uno o l'altro).
3. ✅ **Sessione fornitore = 4 ore**; **validità OTP/magic-link = 15 minuti**, single-use, max 5 tentativi, rate-limit.
4. ✅ Processo: prima **review del design da 5 agenti** (COMPLETATA 2026-05-31, esiti §12), poi implementazione partendo dalla **Fase 0** (prerequisito email relay) → **Fase 1**.
5. ✅ **DECISO (utente, 2026-05-31, post-review)**:
   - (a) **Migrazione + sostituzione**: `questionnaire_campaigns` sostituisce `supplier_questionnaires` (027). Migrazione dati `INSERT…SELECT`, retrocompat dei link `sq_` già inviati, 027 deprecata. Una sola fonte di verità.
   - (b) **Template "NIS2 base" completo GV.SC** già in Fase 1: 8 domande attuali + domande mappate ai 5 fattori GV.SC-07 e agli ambiti GV.SC-01 §2 (con `nis2_ref` e fonti certe). Modulo "compliance-grade" dall'inizio.
   - (c) **Anagrafica fornitori via manuale + CSV + API key cliente** (§3.5) inclusa nel piano.
6. ✅ **Contesto normativo + AI consulente (come TRPG)** — vedi §13. Pilastro trasversale: corpus normativo NIS2 incluso e mantenuto aggiornato nella KB, AI istruita a rispondere come consulente NIS2 reale, sempre ancorata alle fonti certe.

## 11. Promemoria post-implementazione (regola progetto — VINCOLANTE per ogni fase)
Ad ogni fase che cambia funzionalità, aggiornare SEMPRE:
- **Guida** (`public/guida.html`): nuovo capitolo/sezione "Portale fornitori" + àncora; allineare help.js `_guideAnchor`.
- **Help online** (`public/js/help.js`): sezione contestuale per supply-chain (questionari/portale) e per la pagina portale.
- **Traduzioni** (`public/js/i18n.js`): chiavi IT + EN per le nuove UI (categorie, template, campagne, portale, OTP).
- **KB AI / product_knowledge**: aggiornare la knowledge base AI (RAG + product_knowledge AgileHub) con la nuova funzionalità, così l'assistente la conosce.
- `version.json` bump, commit per fase, push da host.

---

## 12. Esiti review 5 agenti (2026-05-31) + correzioni al design

Verdetto comune dei 5 reviewer (sicurezza OTP, UX, architettura/DB, fattibilità stack, GDPR/supply-chain): **impianto solido, NON implementabile "as-is"**. Le correzioni 🔴 qui sotto sono da chiudere prima della Fase 1; le ⚠️ in corso d'opera. Convergenze forti tra più agenti = priorità massima.

### 🔴 Bloccanti (recepiti nel design)
1. **Fase 0 — Email relay** *(fattibilità)*: `EmailService::send()` usa `mail()` (rotto nel container). Aggiungere `sendViaRelay()` via AgileHub `POST /api/emails/send` con `X-Internal-Key`, env multi-source (`getenv()?:$_SERVER?:$_ENV?:default` per clear_env FPM). **L'OTP va inviato via template `/api/emails/send` (NON `send-raw`)** così il codice non finisce nei log `email_log` *(convergenza sicurezza+fattibilità)*.
2. **Auth fornitore = libreria SEPARATA** *(sicurezza+architettura+fattibilità, 3 agenti)*: `SUPPLIER_JWT_SECRET` dedicato (da vault/.env), claim `aud:"supplier-portal"` SENZA `user_id`, `requireSupplierSession()` proprio. **MAI** passare per `BaseController::requireAuth()`/`active_sessions`. Sessione revocabile via tabella `supplier_sessions` (jti verificato a DB). Lockout persistente su `supplier_otp.attempts` + rate-limit DB (non solo `/tmp`), invalidazione OTP precedenti, `hash_equals`.
3. **`questionnaire_answers` con `organization_id`** denormalizzato *(architettura)* + `UNIQUE(campaign_id, question_id)` + FK `ON DELETE CASCADE` → no leak cross-org/IDOR.
4. **Schema → DDL completa** *(architettura)*: PK/FK/indici/tipi reali prima di codare. Indici sargable cron: `(status, next_reminder_at)`, `(status, due_at)`. `UNIQUE(organization_id, slug)` categorie + sentinel `organization_id=0` per i preset.
5. **Coesistenza vs migrazione `supplier_questionnaires` (027)** *(architettura+fattibilità)*: `questionnaire_campaigns` è superset di 027. **DECISIONE NEL DESIGN** (vedi §10 punto 5). Migrazioni nuove numerate **032+**, idempotenti via `information_schema` (no `ADD COLUMN IF NOT EXISTS`), applicate `mysql -h localhost` da Hetzner. Retrocompat token `sq_` + endpoint `public-questionnaire` in produzione.
6. **Versioning template = snapshot dedicato** *(architettura+UX, F17)*: tabella `questionnaire_template_versions(template_id, organization_id, version, questions_snapshot JSON incl. weight, change_note, created_by, UNIQUE(template_id,version))` come `policy_versions`. Le campagne in corso restano congelate alla loro versione.
7. **Contenuto normativo del template "NIS2 base"** *(GDPR/supply-chain)*: coprire i **5 fattori GV.SC-07** + ambiti **GV.SC-01 §2 a-q** applicabili (cancellazione dati a fine fornitura, subfornitura, secure development, ruoli personale GV.SC-02); campo `nis2_ref` per mapping; scoring **per-vulnerabilità** (flag "no MFA"/"no patch"/…) oltre al totale (Art.21.3), modulato per `criticality`. Fonti da aggiungere a `nis2_sources.php`: ENISA Good Practices SC + NIST SP 800-161r1.
8. **GDPR** *(GDPR/supply-chain)*: base giuridica = legittimo interesse Art.6(1)(f) + obbligo legale (c) (LIA); **informativa Art.13/14** in email OTP + landing portale (titolare=cliente, Agile=responsabile Art.28 + sub-processor disclosure); retention quantificata (purge `supplier_otp` 24-48h via cron; risposte/`supplier_users` per durata relazione + ≥5 anni audit); endpoint cancellazione/anonimizzazione Art.17 con tombstone (no distruzione evidenze).
9. **Immutabilità evidenze** *(GDPR/supply-chain)*: `questionnaire_answers` append-only + log submit/scoring in `AuditService` (hash-chain); ogni campagna conserva il proprio snapshot risposte+score (no sovrascrittura); **soft-delete** fornitore (`deleted_at`, no hard `Database::delete`).
10. **Salvataggio parziale (autosave)** *(UX, F11 — priorità assoluta)*: stato `in_progress` + endpoint `PATCH .../answers` (autosave per-domanda con `debounce`), "salva bozza" vs "invia". Le risposte parziali sopravvivono a logout/scadenza sessione.

### 🔴/⚠️ Routing (architettura+fattibilità)
Aggiungere `'supplier-portal' => 'SupplierPortalController'` a `controllerMap`. Modellare gli URL sulle forme che il router **produce davvero** (id numerico primo segmento): `POST:requestOtp`, `POST:verifyOtp`, `GET:me`, `GET:{id}` (campagna), `POST:{id}/submit`, `PATCH:{id}/answers`. La notazione `{campaignId}` del doc NON è riconosciuta.

### ⚠️ UX da chiudere nel design (mini-spec §6)
- **Email invito progettata** (F6): nome committente in oggetto/corpo, motivo, ~tempo, scadenza in chiaro, CTA singola, branding firm.
- **Gestione "email non arriva" + errori OTP umani** (F2/F3): countdown reinvio, "controlla spam", messaggi specifici allo step verifica (errato/scaduto/lockout/già usato) ognuno con via d'uscita; preservare rigenera-link manuale lato azienda.
- **Magic-link primario, OTP fallback** (F1) + niente doppio-consumo da prefetch mail mobile (F4): consumo solo all'apertura sessione esplicita.
- **Editor template no-code** (F16): nascondere slug/code/order_index/options JSON/reminder_offsets/recurrence_months/weight dietro chip/dropdown/drag + preview "come lo vede il fornitore".
- **i18n portale** (F20): lingua IT/EN impostabile dal cliente o selezionabile (fornitori esteri).
- **Ricevuta** (F22): email conferma + PDF risposte scaricabile. **Multi-referente/delega** (F24). **Score al fornitore configurabile, default OFF** (F23). **Progress bar** (F13). **Scadenza sempre visibile** (F7). **Mobile-first + a11y** (F10/F21): elementi nativi, ARIA, touch ≥44px, `autocomplete="one-time-code"`.

### ⚠️ Operativo (fattibilità)
Cron `supplier-questionnaire-cron.sh` (primo `.sh` del progetto): wrapper sottile + runner PHP CLI, `TZ=Europe/Rome`, fuori finestra DST 02:00-03:00, idempotente (UPDATE condizionato), entry in `CRON_REGISTRY.md`, crontab via richiesta utente. Disciplina: `kill -USR2 1` dopo ogni edit PHP, semaforo `/tmp/agent-working.lock`, commit per fase, push da host.

### Stima realistica rivista
~**6-7 sessioni** (vs 4): Fase 0 ~0.5 · Fase 1 ~1-1.5 · Fase 2 ~1.5 · Fase 3 ~2-2.5 · Fase 4 ~0.5-1.

---

## 13. Pilastro trasversale — Contesto normativo incluso/aggiornato + AI "vero consulente" (richiesta utente 2026-05-31, modello TRPG)

**Obiettivo**: NIS2 Agile deve comportarsi come un **consulente NIS2 reale** — conoscere e citare la normativa vigente (NIS2, D.Lgs. 138/2024, Determine/Allegati ACN, ENISA, NIST), tenerla **aggiornata**, e rispondere **sempre ancorata a fonti certe** (mai inventate). Allineamento al pattern già in uso su TRPG.

**Cosa esiste già in NIS2** (da censire e potenziare):
- RAG completo: Qdrant `nis2_kb` (512-dim Voyage), `VectorService`/`EmbedService`/`RagService`, `AIService::askWithRag()`.
- Registro fonti certe `application/config/nis2_sources.php` + `AIService::authoritativeSourcesBlock()` (vieta riferimenti inventati).
- Ingest corpus normativo: `scripts/ingest-nis2-sources.php` (PDF normativi → `nis2_kb` scope SYSTEM).
- Corpus locale: `docs/nis2/` (direttiva ITA, allegati ACN 1-4, ecc.).

**Gap da colmare** (output dell'agente di design dedicato):
1. **Completezza corpus**: verificare che TUTTE le fonti di `nis2_sources.php` siano effettivamente ingerite in `nis2_kb`; aggiungere ENISA Good Practices SC e NIST SP 800-161r1 (citati nel design ma assenti dal registro — F-12).
2. **Aggiornamento**: meccanismo per mantenere il corpus aggiornato (nuove Determine ACN, feed normativo già presente come `normative_updates`) → ingest incrementale + tracciamento versione fonte.
3. **AI "consulente"**: rafforzare i system prompt (persona consulente NIS2, tono, scope, citazione obbligatoria fonti) coerente con lo standard `persona-conversational-rules v2.0` (persona ARIA_SUPPORT_NIS2 già esistente). Ogni risposta normativa cita §/articolo della fonte.
4. **Copertura supply-chain**: il consulente deve saper rispondere su GV.SC-01..07, art.21.2(d)/21.3, e guidare la compilazione dei questionari fornitori (collega §7 fasi a questo pilastro).

**Nota**: questo pilastro è parzialmente indipendente dal modulo questionari ma li potenzia (help in-context durante la compilazione, spiegazione dei `nis2_ref`). Sequenziabile in parallelo alle Fasi.

---

## 14. Stato lavorazione (2026-05-31)
- ✅ Design + 5 review + decisioni utente consolidate (§10, §12, §13).
- ✅ 4 agenti di produzione-design completati e **materializzati come artefatti** (nessun codice applicato, nessun commit):
  - (A) Contenuto template GV.SC → `docs/supplier-portal/template-nis2-base.questions.json` (26 domande, `nis2_ref`/`vuln_flag`, fonti certe verbatim).
  - (B) Pilastro AI consulente + corpus → `docs/supplier-portal/AI_CONSULENTE_NORMATIVO.md`.
  - (C) DDL migrazioni → `docs/sql/032_supplier_questionnaires_module.sql`, `033_suppliers_category_source.sql`, `034_supplier_portal_auth.sql`, `035_migrate_027_to_campaigns.sql` (idempotenti, NON applicate).
  - (D) Mini-spec UX §6 → `docs/supplier-portal/UX_MINI_SPEC.md`.
- ⏳ Prossimo (su conferma utente): commit del pacchetto design su Gitea → implementazione **Fase 0** (email relay) → **Fase 1** (categorie + template GV.SC + import CSV/API + render dinamico), con `kill -USR2 1`/smoke/commit per fase e aggiornamento guida/help.js/i18n/KB AI.

### Roadmap implementazione consolidata (ordine)
| # | Fase | Output | Migrazioni | Rischio |
|---|---|---|---|---|
| 0 | Email relay | `EmailService::sendViaRelay()` + ripuntare sendQuestionnaire/forgotPassword | — | Basso (sblocca + fixa bug prod) |
| 1 | Categorie + template GV.SC + anagrafica (CSV/API) + render | seed 10 categorie + template NIS2 base (26 dom.) + import suppliers | 032, 033 | Basso |
| 2 | Campagne + scadenze/ricorrenze + reminder cron + cruscotto | — | (035 migrazione 027) | Medio |
| 3 | Portale fornitore + auth OTP/magic-link separata + sessioni revocabili | — | 034 | Alto (auth esterna) |
| 4 | Tipi domanda avanzati (file/scale/multi) + scoring per-vulnerabilità + ricevuta PDF | — | — | Medio |
| ‖ | Pilastro AI consulente (parallelo): corpus completo + persona + feed→KB + AI in-context | — | (KB su Hetzner) | Medio |
