# Integrazione DocuSeal — GIX EvoAz

---

## Evoluzione futura: modelli di documento con Embedded Builder

L'attuale implementazione richiede che l'admin carichi un PDF già compilato per ogni invio. L'evoluzione adottata introduce un sottomenu **Modelli** che permette di definire template riutilizzabili: l'admin carica il PDF una volta sola, posiziona i campi firma con un editor WYSIWYG embedded nell'app, e da quel momento può inviare lo stesso modello a più destinatari con i dati del candidato precompilati automaticamente dal DB.

**Soluzione adottata: PDF upload + DocuSeal Embedded Builder (`@docuseal/vue`)**

Il template vive su DocuSeal (gestito via API), ma viene creato e modificato interamente dall'interno dell'app tramite il componente Vue `<DocusealBuilder>` embeddato in un drawer. L'admin non interagisce mai con l'interfaccia DocuSeal direttamente.

---

### Flusso gestione modelli

**Creazione di un nuovo modello:**
1. Admin va in "Firma elettronica" → tab "Modelli" → "Nuovo modello"
2. Carica il PDF "master" (contratto di assunzione, piano provvigionale, procedura obbligatoria, ecc.)
3. Il backend carica il PDF su DocuSeal via `POST /templates/pdf` e ottiene il `docuseal_template_id`
4. Si apre il `<DocusealBuilder>` embedded in un drawer — l'admin posiziona i campi firma, data firma, ecc. con drag-and-drop
5. Alla chiusura del builder: `docuseal_template_id` e `field_definitions` vengono salvati nella tabella locale `document_templates`

**Aggiornamento del testo del contratto (es. cambio CCNL):**
- Admin carica un nuovo PDF → backend chiama `PUT /templates/{id}/documents` con `replace: true`
- I campi firma restano esattamente dove erano posizionati; solo il documento cambia

**Modifica della posizione dei campi:**
- Admin apre il builder con `template_id` esistente → modifica visivamente → salva

**Flusso "PDF libero"** (caricamento diretto senza template) rimane identico all'attuale e convive con i modelli.

---

### Flusso invio da modello

1. Admin va su "Nuovo documento" → sceglie "Da modello" → seleziona il template
2. Compila i campi `manual` (RAL, data assunzione, mansione...); i campi `db` sono precompilati dal DB
3. Seleziona i destinatari (identico al flusso attuale)
4. Backend chiama `POST /submissions` con `template_id` + valori per ogni firmatario

```json
{
  "template_id": 4521,
  "submitters": [{
    "email": "mario.rossi@example.com",
    "name":  "Mario Rossi",
    "phone": "+39333123456",
    "require_phone_2fa": true,
    "fields": [
      { "name": "Nome e Cognome",  "default_value": "Mario Rossi",  "readonly": true },
      { "name": "Data assunzione", "default_value": "01/06/2026",   "readonly": true },
      { "name": "RAL",             "default_value": "28.000 €",     "readonly": false }
    ]
  }]
}
```

`readonly: true` → il firmatario vede il valore ma non può modificarlo. I campi con `source: 'manual'` e `readonly: false` permettono al firmatario di completare dati non noti al momento dell'invio.

---

### Cosa cambia rispetto all'attuale

| Layer | Modifica |
|---|---|
| DB | Nuova tabella `document_templates`; `contracts.minio_key` diventa nullable; nuova colonna `contracts.local_template_id` FK→`document_templates.id`; colonna `submitter_roles JSON` in `document_templates` |
| Backend | `templateController.js` CRUD + builder token; `docuseal.service.js` aggiunta `createSubmissionFromTemplate()`; `contractsController.sendContract` aggiornato; `getContracts` include `local_template_id`; `mapAppToFields` salta campi `__manual__`; `sendContract` legge `manualFields` dal body |
| Frontend | `npm install @docuseal/vue`; nuova tab "Modelli"; modal mappatura campi con opzione "Compilazione manuale" (`maps_to: __manual__`); modal pre-invio con mini-form admin per i campi manuali |
| DocuSeal | Invio da modello: `POST /submissions` con `template_id`; campi pre-compilati come `values` + `readonly_fields`; ruoli reali dal template |
| Webhook / MinIO / download ZIP / OTP | **Invariati** |

---

### Schema DB aggiuntivo

**Nuova tabella `document_templates`:**

```sql
document_templates (
  id                   INT UNSIGNED PK AUTO_INCREMENT,
  name                 VARCHAR(255) NOT NULL,
  document_type        ENUM('employment','commission','safety','compliance','other') NOT NULL DEFAULT 'other',
  docuseal_template_id INT NOT NULL,        -- ID del template su DocuSeal EU
  field_definitions    JSON NOT NULL,       -- array descrittori campi (vedi sotto)
  created_by           INT NOT NULL,        -- FK→users.id
  created_at           DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at           DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
```

`field_definitions` — struttura elemento:
```json
{
  "key":      "data_assunzione",
  "label":    "Data assunzione",
  "source":   "manual",          // "db" = auto da applications | "manual" = input admin
  "db_field": null,              // es. "first_name", "last_name", "email", "phone", "fiscal_code"
  "type":     "date",            // "text" | "date" | "number" | "textarea"
  "required": true
}
```

Campi `source: 'db'` disponibili da `applications`: `first_name`, `last_name`, `email`, `phone`, `birth_date`, `fiscal_code`, `city`, `role`. `full_name` costruito come `first_name + ' ' + last_name` lato service.

**Migrazioni su tabella `contracts`:**
```sql
-- minio_key diventa nullable: non c'è PDF originale da caricare se si usa un template
ALTER TABLE contracts MODIFY COLUMN minio_key VARCHAR(500) NULL;
-- FK al modello locale scelto al momento della creazione del contratto
ALTER TABLE contracts ADD COLUMN local_template_id INT UNSIGNED NULL AFTER docuseal_template_id;
```

---

### Componente Vue — `<DocusealBuilder>`

```bash
npm install @docuseal/vue
```

```vue
<DocusealBuilder
  :token="builderToken"
  host="api.docuseal.eu"
  :with-send-button="false"
  :with-sign-yourself-button="false"
  :roles="['Firmatario']"
  :required-fields="[{ name: 'Firma', type: 'signature', role: 'Firmatario' }]"
  @save="onTemplateSaved"
/>
```

Il `token` è un JWT HS256 generato dal backend — mai esposto direttamente al frontend:

```javascript
// backend — genera token per aprire il builder su un template già caricato
import jwt from 'jsonwebtoken'

jwt.sign(
  { user_email: process.env.DOCUSEAL_ADMIN_EMAIL, template_id: docusealTemplateId },
  process.env.DOCUSEAL_API_KEY,
  { expiresIn: '1h' }
)
```

`@save` riceve il template con il suo `id` DocuSeal — salvato in `document_templates.docuseal_template_id`.

---

### Nuove route backend

```
# Autenticate (authMiddleware JWT)
GET    /api/admin/templates                    → lista modelli con tipo/nome
POST   /api/admin/templates                    → crea record locale (docuseal_template_id + field_definitions)
PUT    /api/admin/templates/:id                → aggiorna field_definitions
DELETE /api/admin/templates/:id                → archivia su DocuSeal + elimina localmente
POST   /api/admin/templates/:id/builder-token  → genera JWT per aprire il builder in modifica
POST   /api/admin/templates/upload-pdf         → POST /templates/pdf a DocuSeal, restituisce docuseal_template_id + builder token
```

---

## Stato generale

| Layer | Stato |
|---|---|
| DB migrations (inline in `index.js`) | ✅ Completato |
| `docuseal.service.js` | ✅ Completato |
| `contractsController.js` | ✅ Completato |
| Routes in `allRoutes.js` | ✅ Completato |
| `useContracts.js` composable | ✅ Completato |
| `AdminContractsPanel.vue` (rinominata "Firma elettronica") | ✅ Completato |
| Setup account DocuSeal EU | ✅ Completato |
| Webhook configurato in dashboard DocuSeal | ✅ Completato |
| Variabili `.env` in produzione | ✅ Completato |
| Test end-to-end firma reale | ✅ Completato — in produzione |
| Firmatari `in_formazione` inclusi | ✅ Completato |
| Invio individuale / firma congiunta | ✅ Completato |
| OTP SMS 2FA al momento della firma | ✅ Completato |
| Audit trail ufficiale DocuSeal | ✅ Completato |
| Normalizzazione numeri di telefono E.164 | ✅ Completato |
| Tabella `document_templates` (DB migration) | ✅ Completato |
| Colonna `submitter_roles JSON` in `document_templates` | ✅ Completato |
| Migrazioni `contracts` (`minio_key` NULL + `local_template_id`) | ✅ Completato |
| `templateController.js` CRUD + builder token | ✅ Completato |
| `getTemplateFields` endpoint + auto-sync campi e ruoli da DocuSeal | ✅ Completato |
| `createSubmissionFromTemplate()` in `docuseal.service.js` | ✅ Completato |
| `contractsController.sendContract` — supporto template | ✅ Completato |
| Auto-sync `field_definitions` + `submitter_roles` al primo invio | ✅ Completato |
| Ruoli submitter reali dal template (non hardcodati) | ✅ Completato |
| Campi pre-compilati `readonly_fields` per il firmatario | ✅ Completato |
| Tab "Modelli" in pannello admin + `<DocusealBuilder>` embedded | ✅ Completato |
| Mappatura esplicita campi DocuSeal → colonne `applications` (`maps_to`) | ✅ Completato |
| Modal UI mappatura campi con dropdown per ogni campo DocuSeal | ✅ Completato |
| Fix webhook crash su contratti da modello (`minio_key` null) | ✅ Completato |
| Rinomina sezione "Contratti" → "Firme" in sidebar admin | ✅ Completato |
| Icona "Modelli" sidebar: `fa-copy` (fa-rectangles-mixed non in FA Free) | ✅ Completato |
| Opzione "Compilazione manuale" nella mappatura campi (`maps_to: __manual__`) | ✅ Completato |
| Modal pre-invio con mini-form per i campi manuali (admin) | ✅ Completato |
| Fix `getContracts`: aggiunto `local_template_id` nella SELECT lista | ✅ Completato |
| `external_id` sui submitter (lookup webhook robusto via ID interno) | ✅ Completato |
| Webhook: `form.declined` aggiunto (evento moderno, era solo `submitter.declined`) | ✅ Completato |
| Webhook: `form.completed` usa `external_id` come lookup primario, fallback su `docuseal_submitter_id` | ✅ Completato |
| Webhook: IP warning per richieste non da DocuSeal EU (52.30.226.117, 99.80.245.224) | ✅ Completato |

---

## Contesto

**Obiettivo**: modulo di firma elettronica avanzata (FEA) con OTP per far firmare contratti di lavoro, piani provvigionali e procedure obbligatorie ai candidati nelle fasi finali del recruiting.

**Provider**: DocuSeal Cloud EU — `https://api.docuseal.eu` (dati in Europa, GDPR compliant).

**Stack**: Node.js + Express + MySQL (raw queries, non Sequelize) — MinIO per storage PDF — Vue 3 + composable pattern (non Pinia store).

---

## Architettura effettiva (come implementata)

### Modalità invio individuale (default)

```
Admin (AdminContractsPanel.vue — tab "Firma elettronica")
  │  1. Carica PDF + seleziona destinatari + tipo documento + scadenza
  │     [ ] Firma congiunta (default: off → invio individuale)
  ▼
POST /api/admin/contracts            → salva PDF su MinIO (contracts/original/)
                                       crea record contracts (status: draft, joint_signature: 0)
                                       crea record contract_signers per ogni destinatario

POST /api/admin/contracts/:id/send  → scarica PDF da MinIO
                                     → per ogni firmatario:
                                          docuseal.createSubmissionFromPdf() con 1 signer
                                          POST /submissions/pdf
                                       salva contract_signers.docuseal_submission_id per ognuno
                                       salva docuseal_submitter_id + slug
                                       contracts.status → 'sent', contracts.docuseal_submission_id → NULL

Firmatario (email DocuSeal → OTP SMS 2FA → firma)
  ▼
POST /webhooks/docuseal (PUBLIC)
   ├─ submitter.completed            → contract_signers.status = 'completed'
   ├─ submission.completed           → (abbina su contract_signers.docuseal_submission_id)
   │                                    scarica PDF firmato → MinIO (contracts/signed/{base}_s{id}.pdf)
   │                                    scarica audit DocuSeal → MinIO (contracts/audit/{base}_s{id}.pdf)
   │                                    se tutti i firmatari completati → contracts.status = 'completed'
   ├─ submission.expired             → signer.status = 'expired'; se tutti → contract.status = 'expired'
   └─ submitter.declined             → signer.status = 'declined'

GET /api/admin/contracts/:id/download → ZIP con N coppie (firmato + audit) una per destinatario
```

### Modalità firma congiunta (opt-in)

```
POST /api/admin/contracts/:id/send  → unica chiamata createSubmissionFromPdf() con tutti i firmatari
                                       contracts.docuseal_submission_id = submission.id
                                       contract_signers.docuseal_submission_id = NULL

POST /webhooks/docuseal
   ├─ submitter.completed            → contract_signers.status = 'completed' (via docuseal_submitter_id)
   └─ submission.completed           → (abbina su contracts.docuseal_submission_id)
                                        contracts.status = 'completed'
                                        scarica PDF firmato → MinIO (contracts/signed/)
                                        scarica audit DocuSeal → MinIO (contracts/audit/)

GET /api/admin/contracts/:id/download → ZIP con 1 firmato + 1 audit
```

---

## Scelte implementative rispetto al piano originale

### 1. `POST /submissions/pdf` invece di upload template → submission

**Piano originale**: `POST /templates/uploads` → `POST /submissions` (due step).

**Implementazione reale**: `POST /submissions/pdf` — un solo step, PDF inviato come base64 nel body JSON. DocuSeal non fa auto-detection dei campi del PDF (nessun `RequiredFieldError` su campi nascosti). I campi firma vengono definiti esplicitamente nel payload con coordinate `x/y/w/h`.

**Posizionamento campi**: griglia a 2 colonne nella parte inferiore dell'**ultima pagina** del documento (non la prima). Il numero di pagine viene rilevato dinamicamente con `getPdfPageCount()` (pdf-lib) prima di creare la submission.

### 2. MinIO invece di filesystem locale

**Piano originale**: `uploads/contracts/` su disco locale.

**Implementazione reale**: MinIO (già in uso per i documenti onboarding). Chiavi oggetto:

**Invio individuale** (per-firmatario):
- `contracts/original/{filename}` — PDF originale caricato dall'admin (unico per contratto)
- `contracts/signed/{base}_s{signer_id}.pdf` — PDF firmato per singolo destinatario
- `contracts/audit/{base}_s{signer_id}.pdf` — Audit trail per singolo destinatario

**Firma congiunta**:
- `contracts/original/{filename}` — PDF originale
- `contracts/signed/{filename}` — PDF firmato congiunto
- `contracts/audit/{filename}` — Audit trail congiunto

DB: colonne `minio_key` / `signed_minio_key` / `audit_minio_key` a livello `contracts` (firma congiunta), più le stesse tre colonne a livello `contract_signers` (invio individuale).

### 3. Audit trail ufficiale DocuSeal (non pdf-lib)

**Piano originale**: `GET /submissions/{id}/documents/combined`.

**Versione intermedia**: `generateAuditTrailPdf()` in `docuseal.service.js` generava un PDF personalizzato con pdf-lib. Abbandonata perché il risultato era povero rispetto all'audit ufficiale.

**Implementazione attuale**: `downloadAuditTrailPdf()` scarica l'audit trail ufficiale DocuSeal tramite il campo `audit_log_url` dalla response `GET /submissions/{id}`. Include logo azienda, hash SHA256 del documento, firma visiva, log eventi completo, verifica telefono. Disponibile solo quando la submission è `completed`.

```javascript
// docuseal.service.js
export async function downloadAuditTrailPdf(submissionId) {
  const sub = await fetch(`${API_URL}/submissions/${submissionId}`, { headers: authHeaders() })
  const { audit_log_url } = await sub.json()
  const pdf = await fetch(audit_log_url)
  return Buffer.from(await pdf.arrayBuffer())
}
```

### 4. OTP SMS: autenticazione 2FA, non invito

**Versione precedente**: `DOCUSEAL_SEND_SMS=true` aggiungeva `send_sms: true` al submitter — DocuSeal inviava il link di firma via SMS (come `send_email` ma su SMS). Non era autenticazione, era notifica.

**Implementazione attuale**: `DOCUSEAL_REQUIRE_PHONE_2FA=true` aggiunge `require_phone_2fa: true` al submitter — DocuSeal richiede verifica OTP via SMS **prima di accedere al form di firma**. Il firmatario deve inserire il codice ricevuto sul numero fornito. Il campo `phone` viene sempre incluso se disponibile.

```javascript
// docuseal.service.js — payload submitter
...(s.phone ? {
  phone: normalizePhone(s.phone),
  ...(require2fa ? { require_phone_2fa: true } : {})
} : {})
```

Webhook events correlati: `send_2fa_sms`, `phone_verified`, `start_verification`, `complete_verification`.

### 5. Normalizzazione telefono E.164

DocuSeal rifiuta numeri di telefono non in formato `+<country_code><digits>`. Implementata in due livelli:

**`adminController.js`** — validazione all'inserimento/modifica anagrafica candidato. Blocca con HTTP 400 se il numero non è normalizzabile.

**`docuseal.service.js`** — `normalizePhone()` come ultima difesa prima della chiamata API. Gestisce: `333 123 4567`, `0039 333...`, `39333...`, `+39333...`, `0XX...` (fissi italiani).

Formato target: `+39XXXXXXXXXX` (E.164).

### 6. Invio individuale vs firma congiunta

**Piano originale / versione precedente**: sempre multi-signer in una sola submission (de facto firma congiunta obbligatoria — il documento si completava solo quando tutti avevano firmato).

**Implementazione attuale**: scelta esplicita in fase di creazione del contratto.

- **Invio individuale** (default): N submission indipendenti, una per destinatario. Ogni persona riceve la propria copia, firma autonomamente. L'archivio conterrà un documento firmato + audit trail per ciascun destinatario. Il contratto si completa quando tutti hanno firmato.
- **Firma congiunta**: unica submission con N firmatari. Il documento è completato solo quando l'ultimo ha firmato. Adatto per contratti che richiedono co-firma.

`contracts.joint_signature TINYINT(1)` controlla il comportamento in ogni step (send, cancel, delete, download, webhook).

### 7. Gestione risposta DocuSeal

`POST /submissions/pdf` può restituire sia un oggetto `{ id, submitters: [...] }` sia un array `[{ id, submitters: [...] }]`. Il codice gestisce entrambi i casi:

```javascript
const obj = Array.isArray(data) ? data[0] : data
const list = Array.isArray(obj.submitters) ? obj.submitters : (obj.submitters ? [obj.submitters] : [])
```

Log diagnostico attivo: ogni risposta DocuSeal viene loggata (`[DocuSeal] createSubmissionFromPdf raw response: ...`) per facilitare il debugging.

### 8. Firmatari da `applications`

**Piano originale**: firmatari da `users` table.

**Implementazione reale**: firmatari da `applications` con `pipeline_status = 'proposta_accettata'` oppure `final_outcome IN ('assunto', 'in_formazione')`. `contract_signers.application_id` FK su `applications.id`.

### 9. HMAC: formato `timestamp.sha256_hex`

`X-Docuseal-Signature: {timestamp}.{sha256_hex}` — HMAC calcolato su `"${timestamp}.${rawBody}"` con chiave `DOCUSEAL_WEBHOOK_SECRET` (UTF-8).

Debug mode: `DOCUSEAL_HMAC_DEBUG=true` testa più varianti (raw / stripped `whsec_` / base64 decoded) — utile al primo setup.

`express.raw({ type: '*/*' })` sulla route webhook è indispensabile per il calcolo HMAC corretto (il rawBody non deve essere parsato prima).

### 10. Frontend: tab integrata, non view separata

Tab `contracts` dentro `AdminView.vue` → `AdminContractsPanel.vue` (rinominata "Firma elettronica" nella sidebar e nell'intestazione). Composable `useContracts.js`, no Pinia store.

### 11. Download ZIP

`GET /api/admin/contracts/:id/download` costruisce uno ZIP con `jszip`:
- **Individuale**: `{signer_name}_firmato.pdf` + `{signer_name}_audit_trail.pdf` per ogni destinatario
- **Congiunto**: `{titolo}_firmato.pdf` + `{titolo}_audit_trail.pdf`

Sia firmato che audit vengono prima cercati su MinIO (cache); se assenti, scaricati on-demand da DocuSeal.

### 12. Delete in qualsiasi stato

`deleteContract` permette eliminazione da qualsiasi stato. Se `sent`:
- individuale → archivia ogni `contract_signers.docuseal_submission_id`
- congiunto → archivia `contracts.docuseal_submission_id`

Rimuove sempre da MinIO: original + tutti i signed + tutti gli audit (sia a livello contract sia a livello signer).

---

## Schema DB effettivo

### Tabella `contracts`

```sql
contracts (
  id                     INT UNSIGNED PK AUTO_INCREMENT
  title                  VARCHAR(255) NOT NULL
  description            TEXT NULL
  document_type          ENUM('employment','commission','safety','compliance','other') DEFAULT 'other'
  status                 ENUM('draft','sent','completed','cancelled','expired') DEFAULT 'draft'
  joint_signature        TINYINT(1) NOT NULL DEFAULT 0       -- 0=individuale, 1=congiunta
  minio_key              VARCHAR(500) NULL                    -- contracts/original/{filename}; NULL se il contratto è da template
  signed_minio_key       VARCHAR(500) NULL                   -- solo in modalità congiunta
  audit_minio_key        VARCHAR(500) NULL                   -- solo in modalità congiunta
  docuseal_template_id   INT NULL                            -- ID DocuSeal del template usato (valorizzato solo per contratti da modello)
  local_template_id      INT UNSIGNED NULL                   -- FK→document_templates.id (null = PDF libero)
  docuseal_submission_id INT NULL                            -- solo in modalità congiunta
  created_by             INT NOT NULL FK→users.id
  expires_at             DATETIME NULL
  completed_at           DATETIME NULL
  created_at / updated_at DATETIME
)
```

### Tabella `contract_signers`

```sql
contract_signers (
  id                      INT UNSIGNED PK AUTO_INCREMENT
  contract_id             INT UNSIGNED NOT NULL FK→contracts.id ON DELETE CASCADE
  application_id          INT NOT NULL FK→applications.id
  signer_name             VARCHAR(255) NOT NULL
  signer_email            VARCHAR(255) NOT NULL
  signer_phone            VARCHAR(30) NULL                   -- deve essere E.164 per OTP SMS
  docuseal_submitter_id   INT NULL
  docuseal_submitter_slug VARCHAR(255) NULL                  -- link diretto: docuseal.eu/s/{slug}
  docuseal_submission_id  INT NULL                           -- solo in modalità individuale
  signed_minio_key        VARCHAR(500) NULL                  -- solo in modalità individuale
  audit_minio_key         VARCHAR(500) NULL                  -- solo in modalità individuale
  status                  ENUM('pending','completed','declined','expired') DEFAULT 'pending'
  signed_at               DATETIME NULL
  created_at / updated_at DATETIME
)
```

### Tabella `docuseal_webhook_logs`

```sql
docuseal_webhook_logs (
  id                     INT UNSIGNED PK AUTO_INCREMENT
  docuseal_submission_id INT NULL
  event_type             VARCHAR(100) NOT NULL
  payload                JSON NOT NULL
  processed              TINYINT(1) DEFAULT 0
  error_message          TEXT NULL
  created_at / updated_at DATETIME
)
```

> Le migrations sono inline in `backend/src/index.js` (pattern `CREATE TABLE IF NOT EXISTS` + `ALTER TABLE` con catch `ER_DUP_FIELDNAME`).

---

## API Routes effettive

```
# Autenticate (authMiddleware JWT)
GET    /api/admin/contracts              → lista con filtri status/type/search + paginazione
GET    /api/admin/contracts/:id          → dettaglio + signers con slug DocuSeal
POST   /api/admin/contracts              → multipart/form-data (campo "pdf" + campo "meta" JSON) oppure da template (solo "meta" JSON con local_template_id)
POST   /api/admin/contracts/:id/send     → draft → sent; individuale o congiunto; usa POST /submissions/pdf o POST /submissions in base a local_template_id
POST   /api/admin/contracts/:id/cancel   → sent → cancelled; archivia submission(s) su DocuSeal
DELETE /api/admin/contracts/:id          → elimina in qualsiasi stato (MinIO + DocuSeal + DB)
GET    /api/admin/contracts/:id/download → ZIP (firmato + audit per ogni firmatario/congiunto)

GET    /api/admin/templates                    → lista modelli
POST   /api/admin/templates                    → crea record locale (docuseal_template_id + field_definitions)
PUT    /api/admin/templates/:id                → aggiorna field_definitions
DELETE /api/admin/templates/:id                → archivia su DocuSeal + elimina localmente
POST   /api/admin/templates/:id/builder-token  → genera JWT per aprire il builder in modifica
POST   /api/admin/templates/upload-pdf         → POST /templates/pdf a DocuSeal, restituisce docuseal_template_id + builder token

# PUBLIC — nessun JWT
POST   /webhooks/docuseal                → express.raw, verifica HMAC, gestisce eventi
```

> Non esiste `PUT /api/admin/contracts/:id` — i contratti non si modificano dopo la creazione.

---

## Configurazione `.env`

```env
# DocuSeal
DOCUSEAL_API_KEY=your_api_key_here
DOCUSEAL_API_URL=https://api.docuseal.eu
DOCUSEAL_WEBHOOK_SECRET=your_webhook_secret_here
DOCUSEAL_REQUIRE_PHONE_2FA=true     # true → OTP SMS obbligatorio prima della firma
DOCUSEAL_HMAC_DEBUG=false           # true per debug HMAC al primo setup webhook
DOCUSEAL_ADMIN_EMAIL=admin@evoluzioneazienda.it  # email dell'owner dell'account DocuSeal EU (usata nel payload JWT per il builder)
```

> `DOCUSEAL_SEND_SMS` e `SEND_2FA_SMS` sono **obsoleti** — rimossi. Usare `DOCUSEAL_REQUIRE_PHONE_2FA`.

---

## Setup DocuSeal Cloud — completato

Account EU attivo, webhook configurato, variabili `.env` impostate in produzione, test end-to-end superato (firma + OTP SMS + audit trail ufficiale scaricato).

Webhook endpoint: `https://evoluzioneazienda.it/api/webhooks/docuseal`
Events attivi: `submission.completed`, `submission.expired`, `submitter.completed`, `submitter.declined`

---

## Dipendenze backend

```bash
pdf-lib        # rilevamento numero pagine PDF (getPdfPageCount) per posizionamento firma sull'ultima pagina
jszip          # ZIP download firmato + audit
jsonwebtoken   # generazione JWT per il DocuSeal Embedded Builder (token builder)
```

`multer` **non è usato** — upload PDF gestito da `uploadContractPdf` middleware in `middleware/upload.js` (memoryStorage, limit 20MB, solo PDF).

---

## Note tecniche

- **rawBody per HMAC**: `express.raw({ type: '*/*' })` configurato su `/api/webhooks/docuseal` in `index.js` prima di qualsiasi `express.json()` — indispensabile per il calcolo HMAC corretto.
- **EU region API**: sempre `https://api.docuseal.eu` (non `.com`) — impostato come default in `docuseal.service.js`.
- **`docuseal_template_id` su `contracts`**: valorizzata solo per contratti creati da modello (contiene l'ID DocuSeal del template usato al momento dell'invio). Per contratti da PDF libero rimane NULL.
- **`local_template_id` su `contracts`**: FK→`document_templates.id`. Se NULL il contratto è da PDF libero (flusso attuale); se valorizzata il contratto usa un template e l'invio avviene con `POST /submissions` invece di `POST /submissions/pdf`.
- **Builder token**: JWT HS256 firmato con `DOCUSEAL_API_KEY`, valido 1h. Generato dal backend su richiesta esplicita del frontend (mai hardcodato o esposto). Payload: `{ user_email, template_id }`.
- **Slug firmatario**: `docuseal_submitter_slug` permette di costruire il link firma diretto (`https://docuseal.eu/s/{slug}`) per invio manuale via WhatsApp/SMS se l'email non arriva. Esposto nella UI nel modal "Stato firmatari".
- **Audit on-demand**: se `audit_minio_key` è NULL al download (webhook non ancora arrivato o fallito), viene scaricato live da `audit_log_url`. Richiede che la submission sia `completed`.
- **Firma sull'ultima pagina**: `getPdfPageCount()` legge il numero di pagine del PDF con pdf-lib prima di creare la submission. Il campo firma viene posizionato sulla pagina finale (`page: lastPage`), non sulla prima.
- **Normalizzazione telefono**: `normalizePhone()` in `docuseal.service.js` (permissiva, converte al volo) e in `adminController.js` (strict, blocca l'inserimento con HTTP 400 se il numero non è valido).


---

## Roadmap — funzionalità future

| # | Funzione | Priorità | Note |
|---|---|---|---|
| 1 | **Promemoria manuale (Nudge)** | Alta | Bottone sulla riga firmatario in attesa — chiama `PATCH /submitters/{id}` con `{ send_invitation: true }` per ri-inviare email+SMS in un click. |
| 2 | **Controfirma aziendale** | Media | Aggiungere un secondo firmatario fisso (rappresentante legale Evoluzione Azienda) a ogni submission. Fattibile come firma congiunta con l'azienda come secondo slot. Il documento finale porta sia la firma del collaboratore che quella dell'azienda. |
| 3 | **Firma embedded in-app** | Bassa | Iframe DocuSeal nella pagina di onboarding candidato (o nel pannello admin). Il firmatario non esce dal dominio evoluzioneazienda.it. Da valutare impatto UX vs complessità. |
