Motore Formazione Separato¶
Obiettivo¶
Rendere il dominio formazione un motore separato senza perdere:
- tenancy SafeOps;
- governance centralizzata;
- ruoli e permessi;
- anagrafiche condivise;
- rollout controllato per tenant.
La soluzione consigliata non e un portale scollegato. E un servizio o applicazione dedicata, ma governata da SafeOps come capability tenant-aware.
Stato attuale¶
Nel codice esiste gia un modulo formazione dentro SafeOps, con:
- corsi;
- date e orari;
- iscrizioni;
- presenze;
- attestati;
- survey qualita;
- dashboard e calendario.
Questo rende fattibile una estrazione graduale, perche il dominio esiste gia e il perimetro funzionale e abbastanza chiaro.
Decisione architetturale consigliata¶
SafeOps resta il control plane¶
SafeOps deve restare la sorgente di verita per:
tenant_key;- registry tenant;
- utenti e ruoli;
- profili tenant;
- feature flag;
- module entitlements;
- anagrafiche clienti e dipendenti;
- policy menu e governance.
Formazione diventa motore separato¶
Il motore formazione deve gestire in autonomia:
- catalogo corsi e sessioni;
- iscrizioni;
- presenze;
- attestati;
- survey;
- KPI e dashboard formazione;
- eventuali portali docente/cliente.
Modello target¶
Componenti¶
safeopsControl plane, backoffice, identita, tenant governance.training-engineServizio dedicato alla formazione.storageBucket o prefissi dedicati per attestati, materiali, export.celery / schedulerJob async per sync, notifiche, scadenze e generazione documenti.
Identificatori condivisi¶
I riferimenti minimi tra i due sistemi devono essere:
tenant_keysafeops_user_idcliente_idse il corso e collegato al clientedipendente_idse il corso e collegato al dipendente
Isolamento tenant¶
Il motore separato deve restare tenant-aware con la stessa chiave logica di SafeOps:
- ogni record business del motore usa
tenant_key - nessun record cross-tenant senza chiave esplicita
- i job di sync lavorano sempre per
tenant_key
La tenancy non deve essere reinventata nel motore formazione. Deve essere ereditata da SafeOps.
Autenticazione e sessione¶
Modello consigliato¶
SafeOps autentica l'utente e rilascia un token firmato verso il motore formazione.
Il token deve contenere almeno:
tenant_keyuser_idusernamerole_namesrole_profiles- eventuale
readonly
Perche questa scelta¶
- evita doppia gestione password;
- mantiene SafeOps come identity provider interno;
- consente aperture contestuali dal gestionale al portale formazione;
- riduce disallineamenti tra ruoli e tenant.
Modelli alternativi¶
- SSO OAuth/OIDC interno: migliore a medio termine
- token signed handoff da SafeOps: piu rapido per partire
Per time-to-market sceglierei:
- token signed handoff
- successiva evoluzione a SSO vero
Dati da lasciare in SafeOps¶
- tenant registry
- utenti
- ruoli FAB
- role profiles
- tenant features
- tenant modules
- anagrafiche clienti
- anagrafiche dipendenti
- documenti cross-modulo o documentale master
Dati da spostare nel motore formazione¶
- corsi
- edizioni / date e orari
- iscrizioni
- presenze
- completamenti
- attestati
- survey e KPI formazione
- preferenze calendario formazione
Sincronizzazione dati¶
Da SafeOps verso training-engine¶
Sincronizzare:
- tenant attivi con modulo formazione abilitato
- utenti autorizzati al motore formazione
- clienti collegati
- dipendenti collegati
- eventuali categorie e metadata necessari
Da training-engine verso SafeOps¶
Sincronizzare:
- stato attestati
- scadenze formazione
- KPI sintetici
- eventi utili a notifiche o cruscotti
- eventuali documenti finali registrati nel documentale
Pattern di integrazione consigliato¶
API + eventi asincroni¶
La soluzione piu robusta e:
- API per query e operazioni sincrone
- event bus o job asincroni per stato e allineamenti
Esempi:
safeops.training.course.completedsafeops.training.certificate.issuedsafeops.training.expiry.updated
Da evitare¶
- doppia scrittura manuale su due DB
- copia utenti disallineata
- sincronizzazioni ad hoc senza chiave tenant
Aggiornamenti tra portali¶
Qui vanno separati due piani diversi.
1. Aggiornamenti software¶
Riguardano:
- nuova versione del motore formazione
- nuove API
- nuove tabelle
- bugfix runtime
Come governarli:
- una sola codebase del training-engine
- versioni deployabili per ambiente
- migrazioni DB versionate
- rollout graduale con feature flag tenant
2. Aggiornamenti configurativi tenant¶
Riguardano:
- branding portale
- catalogo corsi
- template attestati
- regole survey
- policy o menu di accesso
Come governarli:
- configurazioni per
tenant_key - pannello admin in SafeOps
- sync verso training-engine solo dei delta necessari
Come fare i rollout¶
Module entitlement¶
Aggiungere o usare il modulo:
module_code = formazione
Se il modulo non e abilitato:
- il tenant non vede ingressi portale;
- il training-engine non deve accettare accessi tenant.
Feature flags¶
Usare feature flag per rollout fine, per esempio:
feature.formazione.external_portalfeature.formazione.sso_v1feature.formazione.certificate_sync_v2
Strategia rollout¶
- attivare tenant pilota
- validare sync utenti e anagrafiche
- validare corso -> attestato -> scadenza
- estendere ad altri tenant
- attivare default quando stabile
Modello dati minimo del training-engine¶
Tabelle minime consigliate:
training_tenant_configtraining_coursetraining_course_sessiontraining_enrollmenttraining_attendancetraining_certificatetraining_surveytraining_sync_audit
Campi chiave ricorrenti:
tenant_keysafeops_user_idcliente_iddipendente_idsource_updated_atsynced_at
Documenti e storage¶
Per attestati e materiali hai due strade corrette.
Opzione A: storage dedicato al training-engine¶
Pro:
- isolamento chiaro
- ciclo documentale separato
Contro:
- serve sync o registrazione in SafeOps per visibilita unificata
Opzione B: storage condiviso logico¶
Pro:
- piu facile mostrare documenti nel documentale SafeOps
Contro:
- richiede naming, prefissi e permessi molto chiari
Scelta pratica:
- storage dedicato, ma registrazione metadata in SafeOps per i documenti che devono emergere nel gestionale
UI e domini¶
Possibili modelli:
- sottodominio dedicato:
academy.<tenant-domain> - route dedicata dietro SafeOps
- dominio unico con redirect firmato
Per chiarezza architetturale sceglierei:
- app separata
- sottodominio dedicato
- handoff firmato da SafeOps
Roadmap consigliata¶
Fase 1¶
- lasciare il dominio formazione dentro SafeOps
- introdurre
module_code = formazionein modo esplicito - introdurre feature flag per portale esterno
- definire API e contratti dati
Fase 2¶
- creare
training-engine - attivare autenticazione signed handoff
- spostare dashboard, calendario, corsi, iscrizioni
Fase 3¶
- spostare attestati e survey
- sincronizzare scadenze e KPI verso SafeOps
- lasciare in SafeOps solo governance e viste sintetiche
Fase 4¶
- passare da handoff signed a SSO pieno
- consolidare audit e monitoraggio tenant-by-tenant
Rischi principali¶
- duplicazione utenti tra sistemi
- inconsistenza tra tenant_key SafeOps e tenant_key del motore
- rollout senza feature flag
- dipendenza diretta del training-engine dal DB interno SafeOps
- documenti duplicati senza ownership chiara
Scelta raccomandata¶
Si, e fattibile.
La scelta raccomandata e:
- motore formazione separato
- governance centralizzata in SafeOps
- tenancy basata su
tenant_key - autenticazione federata da SafeOps
- modulo
formazione+ feature flag per rollout - API + sync asincrono per allineamento tra portali
Design esecutivo¶
Questa sezione traduce la proposta in 4 blocchi implementabili:
- contratti API
- schema token auth
- sincronizzazione eventi e job
- modello tabelle minimo
1. Contratti API¶
Principi¶
- tutte le API sono tenant-aware
- ogni richiesta applicativa deve includere contesto tenant verificabile
- SafeOps resta master per utenti, tenant e anagrafiche
- training-engine resta master per corsi, presenze, attestati e survey
API SafeOps -> training-engine¶
Queste API servono ad aprire il portale e a sincronizzare dati di base.
POST /api/v1/session/handoff¶
Scopo:
- creare una sessione trusted nel training-engine partendo da un utente gia autenticato in SafeOps
Request esempio:
{
"tenant_key": "tenant_besant",
"safeops_user_id": 145,
"username": "m.rossi",
"role_names": ["Gamma", "USR_m.rossi"],
"role_profiles": ["tecnico"],
"readonly": false,
"return_to": "/dashboard"
}
Response esempio:
{
"ok": true,
"redirect_url": "https://academy.besant.example/session/consume?token=...",
"expires_in": 120
}
PUT /api/v1/sync/tenants/{tenant_key}¶
Scopo:
- allineare configurazione tenant, branding e moduli attivi
Payload minimo:
{
"tenant_key": "tenant_besant",
"display_name": "Besant",
"modules": {
"formazione": {"enabled": true, "readonly": false, "status": "active"}
},
"features": {
"feature.formazione.external_portal": {"enabled": true, "status": "active"}
}
}
PUT /api/v1/sync/users/{tenant_key}¶
Scopo:
- allineare utenti abilitati al portale formazione
Payload minimo:
{
"items": [
{
"safeops_user_id": 145,
"username": "m.rossi",
"email": "m.rossi@example.test",
"role_profiles": ["tecnico"],
"readonly": false,
"is_active": true
}
]
}
PUT /api/v1/sync/dipendenti/{tenant_key}¶
Scopo:
- allineare i dipendenti che partecipano ai corsi
Payload minimo:
{
"items": [
{
"dipendente_id": 991,
"cliente_id": 202,
"nome": "Mario Rossi",
"codice_fiscale": "RSSMRA80A01H501X",
"email": "mario.rossi@example.test",
"is_active": true,
"updated_at": "2026-03-18T20:10:00Z"
}
]
}
API training-engine -> SafeOps¶
Queste API servono a riportare stato e output di formazione dentro SafeOps.
POST /api/v1/training/events¶
Scopo:
- inviare eventi applicativi firmati verso SafeOps
Payload esempio:
{
"event_type": "safeops.training.certificate.issued",
"tenant_key": "tenant_besant",
"event_id": "evt_01JXYZ...",
"occurred_at": "2026-03-18T20:15:00Z",
"payload": {
"course_id": 55,
"certificate_id": 144,
"dipendente_id": 991,
"expiry_date": "2027-03-18",
"document_url": "s3://training/tenant_besant/certificates/144.pdf"
}
}
PUT /api/v1/training/status/{tenant_key}¶
Scopo:
- aggiornare snapshot sintetici per dashboard, alert e scadenze
Payload esempio:
{
"tenant_key": "tenant_besant",
"stats": {
"courses_open": 12,
"certificates_expiring_30d": 9,
"certificates_expired": 3
},
"generated_at": "2026-03-18T20:20:00Z"
}
Policy HTTP consigliate¶
Idempotency-KeysuiPOSTsensibili- firma HMAC o JWT service-to-service
X-Tenant-Keyammesso solo se coerente col token- timeout brevi e retry solo su endpoint idempotenti
2. Schema token auth¶
Obiettivo¶
Consentire a SafeOps di autenticare l'utente e al training-engine di fidarsi del contesto senza gestire password locali.
Scelta iniziale¶
Usare un token signed handoff a durata breve.
Claim minimi consigliati¶
{
"iss": "safeops",
"aud": "training-engine",
"sub": "user:145",
"jti": "01JXYZ...",
"tenant_key": "tenant_besant",
"safeops_user_id": 145,
"username": "m.rossi",
"email": "m.rossi@example.test",
"role_names": ["Gamma", "USR_m.rossi"],
"role_profiles": ["tecnico"],
"readonly": false,
"modules": {
"formazione": {"enabled": true, "readonly": false}
},
"features": {
"feature.formazione.external_portal": true
},
"iat": 1773864900,
"exp": 1773865020
}
Regole di validazione¶
Il training-engine deve verificare:
- firma del token
issaudexpjtinon riusatotenant_keypresente- modulo
formazioneabilitato
Session bootstrap¶
Flusso minimo:
- utente autenticato su SafeOps
- click su voce portale formazione
- SafeOps genera token signed breve
- browser apre
training-engine/session/consume - training-engine valida token
- training-engine apre sessione locale breve o rilascia session cookie proprio
Evoluzione target¶
Quando il flusso e stabile:
- migrare a OAuth2/OIDC interno
- mantenere gli stessi claim tenant-aware nel token applicativo
3. Sincronizzazione eventi e job¶
Pattern consigliato¶
Combinare:
- sync bulk pianificati
- eventi near-real-time
Job SafeOps -> training-engine¶
sync_training_tenant_config¶
Frequenza:
- on demand + notturna
Scopo:
- allineare modulo, feature, branding, tenant state
sync_training_users¶
Frequenza:
- on demand dopo variazioni utenti/profili
- ricostruzione completa notturna
Scopo:
- allineare utenti, ruolo profilo, readonly
sync_training_dipendenti¶
Frequenza:
- incrementale ogni pochi minuti
- full rebuild giornaliera
Scopo:
- allineare partecipanti, clienti e stato attivazione
Job training-engine -> SafeOps¶
push_training_certificate_events¶
Scopo:
- notificare emissione o rinnovo attestati
push_training_expiry_snapshot¶
Scopo:
- aggiornare indicatori sintetici su scadenze formazione
register_training_documents¶
Scopo:
- registrare nel documentale SafeOps i documenti che devono emergere nel gestionale
Eventi consigliati¶
safeops.training.course.createdsafeops.training.course.completedsafeops.training.enrollment.updatedsafeops.training.certificate.issuedsafeops.training.certificate.expiry_updatedsafeops.training.survey.closed
Regole di robustezza¶
- ogni evento ha
event_idunivoco - ogni consumer deve essere idempotente
- errori di sync devono finire in audit table
- no stop globale: errore tenant A non blocca tenant B
Audit minimo consigliato¶
Campi:
tenant_keydirection(safeops_to_training,training_to_safeops)entity_typeentity_idevent_idstatusattempt_counterror_messagepayload_hashcreated_atprocessed_at
4. Modello tabelle minimo¶
Tabelle training-engine¶
training_tenant_config¶
Campi minimi:
idtenant_keyuniquedisplay_namebranding_jsonmodule_statusfeature_flags_jsonupdated_at
training_user_access¶
Campi minimi:
idtenant_keysafeops_user_idusernameemailrole_profiles_jsonreadonlyis_activelast_synced_at
Vincolo:
- unique (
tenant_key,safeops_user_id)
training_employee_ref¶
Campi minimi:
idtenant_keydipendente_idcliente_idfull_nametax_codeemailis_activesource_updated_atlast_synced_at
Vincolo:
- unique (
tenant_key,dipendente_id)
training_course¶
Campi minimi:
idtenant_keytitlecourse_codecliente_idnullablecategory_codecapacitystatuscreated_by_safeops_user_idcreated_atupdated_at
training_course_session¶
Campi minimi:
idtenant_keycourse_idsession_datetime_fromtime_toroomteacher_namestatus
training_enrollment¶
Campi minimi:
idtenant_keycourse_iddipendente_idenrollment_statuscompleted_atnotes
Vincolo:
- unique (
tenant_key,course_id,dipendente_id)
training_attendance¶
Campi minimi:
idtenant_keycourse_idsession_iddipendente_idattendance_type(morning,afternoon,full)signed_by_user_idsigned_at
training_certificate¶
Campi minimi:
idtenant_keycourse_iddipendente_idcertificate_numberissued_atexpiry_datedocument_urisync_statussafeops_document_idnullable
training_survey¶
Campi minimi:
idtenant_keycourse_iddipendente_idnullablesurvey_statusscorepayload_jsonsubmitted_at
training_sync_audit¶
Campi minimi:
idtenant_keydirectionentity_typeentity_idevent_idstatusattempt_counterror_messagepayload_hashcreated_atprocessed_at
Tabelle minime lato SafeOps¶
Non serve duplicare il dominio formazione completo.
Bastano:
tenant_module_entitlementconmodule_code = formazionetenant_feature_flagper rollout fine- eventuale
training_portal_registryper URL, versione, stato sync tenant - eventuale
training_sync_auditse vuoi audit centralizzato anche lato SafeOps
Sequenza minima implementativa¶
- abilitare formalmente
module_code = formazione - introdurre
feature.formazione.external_portal - creare endpoint handoff signed
- creare endpoint sync tenant, users, dipendenti
- creare audit sync su entrambi i lati
- spostare prima corsi/calendario/iscrizioni
- spostare poi attestati e survey