# Piano di integrazione EasyForma ↔ FTWEB (Forma.Temp)

Stato: **fasi 1-4 implementate** il 16/09/2026 sulle assunzioni A1-A7 di `specs/formatemp-ftweb.md`, in attesa del
collaudo contro `ftweb-col` e delle risposte alle domande aperte (§8). Fase 5 fuori perimetro.
Riferimento tecnico: `docs/formatemp/api-ftweb.md`. Spec harness: `specs/formatemp-ftweb.md`.

## 1. Obiettivo e perimetro

Portare in FTWEB, con le credenziali del cliente, il registro presenze delle lezioni FAD sincrona svolte su
BigBlueButton, senza far rifare a mano all'ente ciò che EasyForma già conosce (partecipanti, orari, entrate/uscite).

Dentro il perimetro: autenticazione, collegamento aula → progetto e lezione → fascia oraria, invio presenze per
partecipante, docente come personale coinvolto, consolidamento su richiesta esplicita, tracciamento di ogni chiamata.
Fuori perimetro (per ora): creazione progetti, moduli, preventivi, rendicontazione, PFA, verifiche ex-ante,
variazioni, iscrizione della piattaforma FAD (amministrativa).

## 2. Scelta di fondo: usare i servizi "rilevazione presenze" (cap. 6)

Due strade possibili:

|                 | A. Servizi app presenze (6.x)                                      | B. Servizi progetto (3.x)                                                |
| --------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Identificazione | `idFascia` + `codiceProgetto` + `idTipoProgetto`                   | `path` + `idProgetto` + oggetto progetto intero                          |
| Lettura fasce   | `GET /fasce` restituisce le fasce assegnate all'utente             | serve `GET progetto/{path}/id/{id}` (payload di centinaia di KB)         |
| Invio presenze  | una `PUT .../partecipante/firma` per partecipante                  | non esiste un servizio di scrittura puntuale: 3.41 è GET, 3.42 consolida |
| Consolidamento  | `PUT /visitare/consolida/...`                                      | `PUT .../invia/registropresenze/...`                                     |
| Prerequisito    | l'utenza deve essere referente rilevazione presenze delle giornate | utenza APL                                                               |

Raccomandazione: **strada A come percorso principale**, con la strada B solo per lettura del progetto
(3.9) e come consolidamento alternativo se la 6.6 non fosse abilitata all'utenza. Motivi: payload piccoli,
identificatori stabili, semantica identica all'app ufficiale che Forma.Temp già accetta come fonte del registro.
Conseguenza: il cliente deve censire l'utenza usata da EasyForma come referente rilevazione presenze (3.43) sulle
giornate, oppure fornire a EasyForma le credenziali del referente. Da chiarire (§8, D2).

## 3. Architettura (laravel-patterns)

Tutto dietro l'interfaccia già esistente `App\Services\Formatemp\FormatempClient`, che passa da un generico
`send(SyncType, payload)` a metodi tipizzati. I driver `fake` e `api` restano intercambiabili via `FORMATEMP_DRIVER`.

```
app/Services/Formatemp/
├── FormatempClient.php            interfaccia tipizzata (vedi sotto)
├── FakeFormatempClient.php        simulatore in memoria/cache: fasce, registri, consolidamento
├── Ftweb/
│   ├── FtwebHttpClient.php        Http::baseUrl + autenticazione (token CSRF + JSESSIONID), cache sessione
│   │                              per company (40 min), re-auth e retry singolo su sessione scaduta
│   ├── FtwebResponse.php          parsing dell'involucro; returnCode KO → FormatempException(koMessages)
│   ├── ApiFormatempClient.php     implementa FormatempClient usando FtwebHttpClient
│   └── Data/                      DTO readonly: Fascia, Registro, Presenza, PersonaleCoinvolto, Discente
├── FormatempCredentials.php       + dominio (SM_SDOMAIN) + ambiente (collaudo/produzione)
├── AttendanceToPresenzaMapper.php intervalli BBB → orePresenza / oraFirmaInizio / oraFirmaFine
├── FasciaMatcher.php              lezione ↔ fascia (data + orari + codiceProgetto)
└── FormatempSyncService.php       orchestrazione + log su formatemp_syncs (già esiste)
```

Interfaccia proposta:

```php
interface FormatempClient
{
    public function testConnection(FormatempCredentials $c): ConnectionInfo;      // GET autenticationcsrftoken
    /** @return list<Fascia> */
    public function fasce(FormatempCredentials $c): array;                        // 6.1
    public function registro(FormatempCredentials $c, FasciaRef $f): Registro;    // 6.3 (fallback 3.41)
    public function attivaFascia(FormatempCredentials $c, FasciaRef $f): void;    // 6.2
    public function inviaPresenza(FormatempCredentials $c, string $idRegistro, Presenza $p): Presenza; // 6.5
    public function aggiungiPersonale(FormatempCredentials $c, string $idRegistro, PersonaleCoinvolto $p): void; // 6.4
    public function consolida(FormatempCredentials $c, FasciaRef $f): void;       // 6.6
    public function cercaDiscente(FormatempCredentials $c, string $codFiscale): ?Discente;   // 2.30
    public function creaDiscente(FormatempCredentials $c, Discente $d): Discente;            // 2.29
    public function progetto(FormatempCredentials $c, string $path, string $idProgetto): array; // 3.9
}
```

Principi: controller sottili → `FormatempSyncService`/job → client; nessuna chiamata di rete dentro
transazioni DB; ogni chiamata registrata in `formatemp_syncs` (request/response/errore, con password mascherata);
job idempotenti (`ShouldBeUnique` per lezione) con backoff; consolidamento solo su azione esplicita dell'utente.

## 4. Modifiche al modello dati

| Tabella         | Campo                                                                                                                                       | Uso                                                                                                             |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| companies       | `formatemp_domain` (nullable), `formatemp_environment` (`collaudo`/`produzione`, default collaudo)                                          | header `SM_SDOMAIN`, base URL                                                                                   |
| classrooms      | `formatemp_project_id` (`ProXxx_`), `formatemp_project_path`, `formatemp_project_type_id`                                                   | esiste già `formatemp_project_code`                                                                             |
| meetings        | `formatemp_fascia_id` (`FasOraGioCal_`), `formatemp_registro_id` (`RegPreFas_`), `formatemp_sent_at`, `formatemp_consolidated_at`           | collegamento e stato                                                                                            |
| participants    | `formatemp_participant_id` (`ParPro_`), `birth_date`, `birth_place_code` (codice catastale comune), `gender`, `education_id` (titoloStudio) | esiste già `formatemp_id` = `AnaDis_`; dati necessari a 2.29                                                    |
| formatemp_syncs | `endpoint`, `http_status`, `duration_ms`                                                                                                    | diagnostica; `type` amplia `SyncType` (`connection`, `fasce`, `presenza`, `personale`, `consolida`, `discente`) |

Modello import partecipanti: aggiungere al template CSV le colonne `data_nascita`, `comune_nascita` (codice o nome),
`sesso`, `titolo_studio` (facoltative; obbligatorie solo per creare il discente su FTWEB).

## 5. Fasi, consegne e criteri di accettazione

### Fase 0 — Prerequisiti (cliente / Forma.Temp, senza sviluppo)

- Credenziali di collaudo (`ftweb-col`) per un'APL di test, con l'eventuale dominio.
- Un progetto di test in stato avviato con una giornata `flagFAD = true`, almeno due fasce e tre partecipanti.
- Conferma della strada A (utenza referente rilevazione presenze) e di cosa inviare come firma in FAD.

### Fase 1 — Client HTTP e verifica connessione ✅ fatta

Consegne: `FtwebHttpClient`, `FtwebResponse`, `FormatempCredentials` estese, migrazione companies, pagina
impostazioni con ambiente/dominio e bottone "Verifica connessione", `FakeFormatempClient` allineato.

Criteri: (1) `POST /settings/company/formatemp/test` con driver fake restituisce toast di successo e crea una riga
`formatemp_syncs` type `connection`; (2) test unitario con `Http::fake` verifica header `SM_USER`/`SM_PASSWORD`,
estrazione token e `JSESSIONID`, riuso della sessione dalla cache, re-auth e singolo retry dopo una risposta 401;
(3) `returnCode: KO` diventa `FormatempException` con i `koMessages` nel messaggio.

### Fase 2 — Collegamento aula/progetto e lezione/fascia ✅ fatta

Consegne: campi progetto sull'aula (form + validazione), `FasciaMatcher`, comando "Collega fasce" sull'aula,
lettura del registro sulla pagina lezione (partecipanti FTWEB con `ParPro_`, stato consolidamento).

Criteri: (4) con driver fake, `POST /classrooms/{id}/formatemp/link` associa ogni lezione alla fascia con stessa
data e orari e salva `formatemp_fascia_id`; lezioni senza fascia restano non collegate e sono elencate nel toast;
(5) `GET /meetings/{id}` mostra per ogni partecipante se è presente nel registro FTWEB (match per codice fiscale)
e segnala quelli mancanti.

### Fase 3 — Invio presenze e consolidamento ✅ fatta

Consegne: `AttendanceToPresenzaMapper` (unione gap ≤ 15 min, ore `HH:mm`), job `PushMeetingAttendance`
(attiva fascia se necessario, personale coinvolto per il docente, una `PUT` per partecipante, idempotente),
azione "Invia presenze" e "Consolida" (con conferma, irreversibile), badge di stato, riesecuzione su errore.

Criteri: (6) test con `Http::fake` che, dato un meeting con presenze note (es. 09:03–10:47 con una
disconnessione di 10 min), invia `orePresenza = "01:44"`, `oraFirmaInizioLezione = "09:03"`,
`oraFirmaFineLezione = "10:47"`; (7) inviare due volte non duplica personale coinvolto né presenze;
(8) "Consolida" chiama la 6.6 solo dopo conferma e poi blocca ulteriori invii; (9) ogni chiamata è in
`formatemp_syncs` con password non in chiaro.

### Fase 4 — Anagrafiche discenti ✅ fatta (ricerca e creazione; l'iscrizione al progetto resta all'ente)

Consegne: campi anagrafici sui partecipanti + import, `cercaDiscente`/`creaDiscente`, aggiunta al progetto (3.35)
con `tipoDestinatario` scelto sull'aula, tipologiche geografiche cachate (2.1–2.4).

Criteri: (10) un partecipante con codice fiscale già noto a FTWEB riceve `formatemp_id` senza creazione;
(11) uno nuovo viene creato e aggiunto al progetto, ottenendo `ParPro_`; (12) dati anagrafici mancanti producono
un errore di validazione leggibile prima di qualsiasi chiamata.

### Fase 5 — Facoltativa: creazione giornate/fasce da EasyForma (≈ 2 giorni)

Solo se il cliente non vuole creare il calendario in FTWEB: 3.38/3.39/3.40 dalla pianificazione delle lezioni,
con `flagFAD = true` e sede di formazione scelta sull'aula. Dipende dalla conferma del formato (D4).

Fase 6 — Collaudo con Forma.Temp: giro completo su `ftweb-col` con un progetto reale, poi switch a produzione
per company (`formatemp_environment`).

Stima complessiva fasi 1–4: 9 giorni di sviluppo più il collaudo.

### Cosa è stato consegnato (16/09/2026)

| Consegna                       | Dove                                                                                                                                                                       |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Interfaccia tipizzata + driver | `app/Services/Formatemp/FormatempClient.php`, `FakeFormatempClient.php`, `Ftweb/ApiFormatempClient.php`                                                                    |
| Trasporto, sessione, retry     | `Ftweb/FtwebHttpClient.php`, `Ftweb/FtwebSession.php`, `Ftweb/FtwebResponse.php`                                                                                           |
| DTO                            | `Ftweb/Data/{Fascia,FasciaRef,Registro,Presenza,PersonaleCoinvolto,Discente,ConnectionInfo}.php`                                                                           |
| Formati, mapper, matcher       | `FtwebFormat.php`, `AttendanceToPresenzaMapper.php`, `PresenzaTimes.php`, `FasciaMatcher.php`                                                                              |
| Orchestrazione e log           | `FormatempSyncService.php`, `FormatempCallRecorder.php`, `FormatempLinker.php`, `MeetingAttendanceSender.php`                                                              |
| Coda                           | `app/Jobs/SendMeetingAttendanceToFormatemp.php` (`ShouldBeUnique` per lezione)                                                                                             |
| Endpoint                       | `POST settings/company/formatemp/test`, `POST classrooms/{classroom}/formatemp/link`, `POST meetings/{meeting}/formatemp`, `POST meetings/{meeting}/formatemp/consolidate` |
| Configurazione                 | `config/formatemp.php` (ambienti, tolleranze, default, `create_missing_learners`)                                                                                          |
| Test                           | `tests/Unit/Formatemp/*`, `tests/Feature/Formatemp/*`, fixture in `tests/Fixtures/ftweb/*.json`                                                                            |

## 5-bis. Cosa deve confermare il collaudo

Ogni riga è un'assunzione presa per poter scrivere il codice: al collaudo si verifica e, se sbagliata, si cambia un
valore di configurazione o un campo azienda, non il codice.

| #   | Assunzione                            | Come è implementata                                                                                                                                                                                                                         | Cosa verificare su `ftweb-col`                                                                                                                          |
| --- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A1  | Ambienti e credenziali                | `config/formatemp.php` → `environments.collaudo/produzione` (entrambe `https`), scelta per company in `companies.formatemp_environment`; `SM_SDOMAIN` inviato solo se `companies.formatemp_domain` è valorizzato; utente inviato così com'è | Che le base URL siano quelle giuste **e raggiungibili in `https`**, che l'utenza fornita autentichi, e se serve il dominio e/o il formato `RETE\utente` |
| A2  | Utenza referente rilevazione presenze | Si usano solo i servizi 6.1-6.6                                                                                                                                                                                                             | Che `GET /services/rs/fasce` risponda con le fasce del progetto di test: se torna vuoto, l'utenza non è referente (3.43)                                |
| A3  | Registro FAD = registro per fascia    | `AssPreFasPar_` con `orePresenza`/`oraFirmaInizioLezione`/`oraFirmaFineLezione`                                                                                                                                                             | Che su una giornata con `flagFAD = true` la 6.3 risponda con lo stesso registro e che la 6.5 lo accetti                                                 |
| A4  | Giornate e fasce le crea l'ente       | `FasciaMatcher`, tolleranza `formatemp.match_tolerance_minutes` (15 min)                                                                                                                                                                    | Che le fasce reali combacino con gli orari delle lezioni entro la tolleranza; altrimenti alzare il valore                                               |
| A5  | Nessuna firma grafica                 | `firmaIngressoLezione`/`firmaUscitaLezione`/`firma` sempre assenti dal payload                                                                                                                                                              | Che la 6.5 e soprattutto la **6.6 (consolida)** non pretendano le immagini di firma                                                                     |
| A6  | Piattaforma FAD non censita           | `companies.formatemp_fad_platform`, default "EasyForma (BigBlueButton)"                                                                                                                                                                     | Se il registro o il progetto richiedano davvero un riferimento alla piattaforma (2.33) e in quale campo                                                 |
| A7  | `tipoDestinatario` e crediti          | `config formatemp.defaults.tipo_destinatario` (1 = CANDIDATI_MISSIONE) + `companies.formatemp_tipo_destinatario`; crediti `null`; `create_missing_learners` = false                                                                         | Quale `tipoDestinatario` usa l'ente e se i crediti vanno valorizzati alla 2.29                                                                          |

Oltre alle assunzioni, il collaudo deve stabilire:

1. **Path del personale coinvolto.** L'allegato `Servizi_AppMobile.xlsx` chiude la 6.4 con `/personalecoinvolto/add`,
   il testo del PDF no. Il codice usa `/add` (`ApiFormatempClient::aggiungiPersonale`): se il server risponde 404,
   è una riga da togliere.
2. **Semantica della 6.2.** È documentata come "attiva/disattiva": il codice la chiama solo quando `statoFascia`
   non è `true`, perché una seconda chiamata richiuderebbe la fascia. Va confermato che `statoFascia: true` significhi
   "aperta" e che la 6.2 sia davvero un toggle.
3. **Sessione scaduta.** Il client ri-autentica su 401/403 o su `koMessages` che parlano di sessione: va visto cosa
   risponde davvero FTWEB dopo 45 minuti, per allineare `FtwebResponse::sessionExpired()`.
4. **Consolidamento.** Che la 6.6 chiuda davvero la fascia, cosa risponde su una fascia già consolidata, e se esiste
   un modo di riaprirla (oggi il codice la considera irreversibile).
5. **Partecipanti non a registro.** Che un discente presente su BBB ma non iscritto al progetto sia effettivamente
   assente da `registro.presenze[]`, e quale sia il percorso corretto per aggiungerlo (3.35) — oggi EasyForma lo
   segnala e si ferma.
6. **`idTipoProgetto`.** Il default è `2`; va letto quello del progetto reale (`idTipologiaProgetto` della 6.1 lo
   riempie in automatico alla prima Collega).
7. **Arrotondamento delle ore.** `orePresenza` è la somma dei minuti in `HH:mm` con le disconnessioni ≤ 15 minuti
   unite: va confrontata con quanto il Vademecum FAD e il revisore si aspettano.
8. **`https` su collaudo.** `ftweb-col` è raggiunto in `https` per default: le credenziali `SM_USER`/`SM_PASSWORD`
   viaggiano in header a ogni chiamata, quindi il cleartext non è accettabile. Se il collaudo risponde solo in
   `http`, va acceso `FORMATEMP_ALLOW_INSECURE_TEST_URL=true` (mai in produzione, dove la scelta è ignorata) e
   segnalato a Forma.Temp. Finché l'azienda resta su `collaudo` l'app mostra un banner nelle impostazioni azienda e
   un toast di avviso alla verifica connessione: quanto inviato non ha valore di rendicontazione.
9. **Orari vuoti alla 6.5.** Un discente che non si è collegato riceve `oraFirmaInizioLezione` e
   `oraFirmaFineLezione` come stringhe vuote, non come chiavi assenti, perché un reinvio dopo una correzione deve
   cancellare gli orari precedenti. Va verificato che FTWEB accetti `""` e che azzeri davvero la riga; in caso
   contrario serve sapere quale valore usare (chiave assente, `null`, o niente chiamata).
10. **Campi per company mai inviati.** `formatemp_fad_platform` (A6) e `formatemp_tipo_destinatario` (A7) sono
    impostazioni presenti nella UI: il `tipoDestinatario` finisce solo nella 2.29 quando
    `formatemp.create_missing_learners` è attivo, la piattaforma FAD in nessun payload. Va stabilito quali registri
    li richiedano davvero; finché non lo sappiamo la UI lo dichiara e i campi restano inerti.
11. **Fasce a cavallo della mezzanotte.** `oraFirmaInizioLezione`/`oraFirmaFineLezione` sono `HH:mm` senza data, e
    `FasciaMatcher` richiede che fascia e lezione cadano nello stesso giorno: una lezione 23:00-00:30 oggi non
    trova fascia e si ferma con un errore leggibile. Va verificato se FTWEB ammetta una fascia che attraversa la
    mezzanotte prima di investirci.

## 6. Test e qualità

- Unit: mapper (casi: nessuna presenza, presenza oltre fine fascia, disconnessioni multiple, ingresso prima
  dell'inizio fascia), matcher, parsing involucro.
- Feature: flussi con `Http::fake()` e con `FakeFormatempClient`; nessun test chiama la rete.
- Fixture: risposte reali di collaudo salvate in `tests/Fixtures/ftweb/*.json` (anonimizzate).
- Gate: `composer test`, larastan 7, `/innovatic:gate` a fine fase; validator sui criteri numerati sopra.

## 7. Rischi

| Rischio                                                                      | Mitigazione                                                                                                                      |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Registro FAD diverso dal registro per fascia (`registroFAD` mai documentato) | confermare in Fase 0; il mapper è isolato e sostituibile                                                                         |
| Firme PNG obbligatorie per consolidare                                       | generare un'immagine PNG con testo "Presenza rilevata da EasyForma via BBB, <data/ora>" solo se richiesto                        |
| Sessione condivisa per company (45 min, un solo JSESSIONID)                  | cache per company + lock sulle chiamate concorrenti; job in coda `formatemp` con concorrenza 1                                   |
| HTTP in chiaro su collaudo                                                   | default `https` ovunque; il cleartext richiede `FORMATEMP_ALLOW_INSECURE_TEST_URL=true`, è escluso in produzione e viene loggato |
| Driver `fake` attivo per sbaglio                                             | driver sconosciuto = errore esplicito; `fake` rifiutato in produzione senza `FORMATEMP_ALLOW_FAKE_IN_PRODUCTION=true`            |
| Anagrafiche di discenti estranei nel log delle chiamate                      | in `formatemp_syncs` va solo un riepilogo (conteggi e id); i corpi grezzi richiedono `FORMATEMP_LOG_RAW_RESPONSES=true`          |
| Due lezioni sulla stessa fascia                                              | controllo a scrittura (errore che nomina l'altra lezione) + indice unico su `meetings.formatemp_fascia_id`                       |
| Credenziali del cliente in nostre mani                                       | già cifrate a riposo; mai loggate; mascherate in `formatemp_syncs`                                                               |
| Refusi nel documento (3.38, 3.42)                                            | verificare in collaudo prima di implementare la Fase 5                                                                           |
| Consolidamento irreversibile                                                 | doppia conferma + solo dopo che tutte le presenze risultano inviate                                                              |

## 8. Domande aperte per il cliente / Forma.Temp

- D1. Quale ambiente e quali credenziali abbiamo? Serve `SM_SDOMAIN`? Formato `RETE\\utente`?
- D2. L'utenza di EasyForma sarà un referente rilevazione presenze (servizi 6.x) o un'utenza APL (servizi 3.x)?
- D3. In FAD sincrona il registro è quello per fascia (`AssPreFasPar_`) o esiste una struttura `registroFAD`?
- D4. Le giornate/fasce le crea l'ente in FTWEB (consigliato) o devono nascere da EasyForma (Fase 5)?
- D5. Le firme di ingresso/uscita in FAD possono essere nulle o serve un'immagine? Cosa mostra l'audit?
- D6. La piattaforma BigBlueButton di EasyForma è già censita tra le piattaforme FAD dell'APL (2.33)?
- D7. Quale `tipoDestinatario` e quali crediti (sicurezza generale, diritti e doveri) vanno impostati sui discenti?
