Vai al contenuto

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

  1. safeops Control plane, backoffice, identita, tenant governance.
  2. training-engine Servizio dedicato alla formazione.
  3. storage Bucket o prefissi dedicati per attestati, materiali, export.
  4. celery / scheduler Job async per sync, notifiche, scadenze e generazione documenti.

Identificatori condivisi

I riferimenti minimi tra i due sistemi devono essere:

  • tenant_key
  • safeops_user_id
  • cliente_id se il corso e collegato al cliente
  • dipendente_id se 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_key
  • user_id
  • username
  • role_names
  • role_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:

  1. token signed handoff
  2. 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.completed
  • safeops.training.certificate.issued
  • safeops.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_portal
  • feature.formazione.sso_v1
  • feature.formazione.certificate_sync_v2

Strategia rollout

  1. attivare tenant pilota
  2. validare sync utenti e anagrafiche
  3. validare corso -> attestato -> scadenza
  4. estendere ad altri tenant
  5. attivare default quando stabile

Modello dati minimo del training-engine

Tabelle minime consigliate:

  • training_tenant_config
  • training_course
  • training_course_session
  • training_enrollment
  • training_attendance
  • training_certificate
  • training_survey
  • training_sync_audit

Campi chiave ricorrenti:

  • tenant_key
  • safeops_user_id
  • cliente_id
  • dipendente_id
  • source_updated_at
  • synced_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 = formazione in 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:

  1. contratti API
  2. schema token auth
  3. sincronizzazione eventi e job
  4. 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-Key sui POST sensibili
  • firma HMAC o JWT service-to-service
  • X-Tenant-Key ammesso 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
  • iss
  • aud
  • exp
  • jti non riusato
  • tenant_key presente
  • modulo formazione abilitato

Session bootstrap

Flusso minimo:

  1. utente autenticato su SafeOps
  2. click su voce portale formazione
  3. SafeOps genera token signed breve
  4. browser apre training-engine/session/consume
  5. training-engine valida token
  6. 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.created
  • safeops.training.course.completed
  • safeops.training.enrollment.updated
  • safeops.training.certificate.issued
  • safeops.training.certificate.expiry_updated
  • safeops.training.survey.closed

Regole di robustezza

  • ogni evento ha event_id univoco
  • 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_key
  • direction (safeops_to_training, training_to_safeops)
  • entity_type
  • entity_id
  • event_id
  • status
  • attempt_count
  • error_message
  • payload_hash
  • created_at
  • processed_at

4. Modello tabelle minimo

Tabelle training-engine

training_tenant_config

Campi minimi:

  • id
  • tenant_key unique
  • display_name
  • branding_json
  • module_status
  • feature_flags_json
  • updated_at

training_user_access

Campi minimi:

  • id
  • tenant_key
  • safeops_user_id
  • username
  • email
  • role_profiles_json
  • readonly
  • is_active
  • last_synced_at

Vincolo:

  • unique (tenant_key, safeops_user_id)

training_employee_ref

Campi minimi:

  • id
  • tenant_key
  • dipendente_id
  • cliente_id
  • full_name
  • tax_code
  • email
  • is_active
  • source_updated_at
  • last_synced_at

Vincolo:

  • unique (tenant_key, dipendente_id)

training_course

Campi minimi:

  • id
  • tenant_key
  • title
  • course_code
  • cliente_id nullable
  • category_code
  • capacity
  • status
  • created_by_safeops_user_id
  • created_at
  • updated_at

training_course_session

Campi minimi:

  • id
  • tenant_key
  • course_id
  • session_date
  • time_from
  • time_to
  • room
  • teacher_name
  • status

training_enrollment

Campi minimi:

  • id
  • tenant_key
  • course_id
  • dipendente_id
  • enrollment_status
  • completed_at
  • notes

Vincolo:

  • unique (tenant_key, course_id, dipendente_id)

training_attendance

Campi minimi:

  • id
  • tenant_key
  • course_id
  • session_id
  • dipendente_id
  • attendance_type (morning, afternoon, full)
  • signed_by_user_id
  • signed_at

training_certificate

Campi minimi:

  • id
  • tenant_key
  • course_id
  • dipendente_id
  • certificate_number
  • issued_at
  • expiry_date
  • document_uri
  • sync_status
  • safeops_document_id nullable

training_survey

Campi minimi:

  • id
  • tenant_key
  • course_id
  • dipendente_id nullable
  • survey_status
  • score
  • payload_json
  • submitted_at

training_sync_audit

Campi minimi:

  • id
  • tenant_key
  • direction
  • entity_type
  • entity_id
  • event_id
  • status
  • attempt_count
  • error_message
  • payload_hash
  • created_at
  • processed_at

Tabelle minime lato SafeOps

Non serve duplicare il dominio formazione completo.

Bastano:

  • tenant_module_entitlement con module_code = formazione
  • tenant_feature_flag per rollout fine
  • eventuale training_portal_registry per URL, versione, stato sync tenant
  • eventuale training_sync_audit se vuoi audit centralizzato anche lato SafeOps

Sequenza minima implementativa

  1. abilitare formalmente module_code = formazione
  2. introdurre feature.formazione.external_portal
  3. creare endpoint handoff signed
  4. creare endpoint sync tenant, users, dipendenti
  5. creare audit sync su entrambi i lati
  6. spostare prima corsi/calendario/iscrizioni
  7. spostare poi attestati e survey