Fase 2 - Specifica tecnica catalogo moduli¶
Data audit: 2026-06-30
Documenti collegati:
docs/processo/inventario_fase1_catalogo_moduli_tenant.mddocs/processo/fase1b_catalogo_moduli_business.md
Scopo¶
Questa fase definisce il modello tecnico target per rendere i moduli SafeOps attivabili per tenant con menu, sottomenu, feature, profili, regole, quantita, scadenze e costi.
Il documento e' una specifica. Non contiene codice applicativo e non modifica il runtime.
Obiettivi tecnici¶
- Introdurre un catalogo moduli canonico.
- Collegare ogni modulo a menu, feature, profili, dipendenze, piani e listini.
- Mantenere compatibilita con
TenantModuleEntitlement,TenantFeatureFlag,TenantRoleProfile,ServiceCatalogeCommercialeListino*. - Evitare una migrazione big-bang del menu.
- Separare modulo tenant, servizio commerciale e servizio venduto a cliente finale.
- Rendere espliciti limiti e stati.
- Preparare un enforcement uniforme su menu, route, API e job.
Principio di rollout¶
Il nuovo catalogo deve partire come fonte descrittiva e di orchestrazione, non come sostituzione immediata.
Sequenza consigliata:
- creare catalogo e mapping;
- popolare seed iniziali;
- leggere catalogo in parallelo agli entitlement esistenti;
- generare entitlement legacy dalle subscription;
- spostare gradualmente i menu da hardcoded a dichiarativi;
- attivare enforcement unificato solo modulo per modulo.
Modello dati target¶
module_catalog¶
Anagrafica canonica dei moduli.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Unico, es. safe_ras, core_documentale |
legacy_module_code |
string nullable | Es. ras, documentale, commerciale |
name |
string | Nome business |
description |
text nullable | Descrizione |
module_type |
enum/string | core, sellable, addon, integration, internal |
category |
string | Es. platform, operations, safecondo, training |
owner |
string nullable | Owner funzionale |
is_active |
boolean | Visibile/gestibile nel catalogo |
is_tenant_visible |
boolean | Visibile ai tenant admin |
is_sellable |
boolean | Usabile in offerte/piani |
requires_subscription |
boolean | Richiede subscription tenant |
default_status |
string | active, disabled, trial |
sort_order |
integer | Ordinamento |
metadata_json |
json/text | Estensioni |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
module_code; - indice su
legacy_module_code; - indice su
module_type,is_active,is_sellable.
module_dependency¶
Dipendenze tra moduli.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo dipendente |
depends_on_module_code |
string | Modulo richiesto |
dependency_type |
string | required, optional, recommended, addon_parent |
enforcement |
string | warn, block_activation, block_runtime |
notes |
text nullable | Note |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(module_code, depends_on_module_code, dependency_type); - indici su
module_code,depends_on_module_code.
module_menu_item¶
Menu dichiarativi per modulo.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo owner |
parent_key |
string nullable | Chiave menu padre |
menu_key |
string | Chiave stabile, non label |
label |
string | Testo menu |
category_label |
string nullable | Compatibilita FAB/menu attuale |
href |
string nullable | URL |
endpoint |
string nullable | Endpoint Flask/FAB |
icon |
string nullable | Icona |
sort_order |
integer | Ordinamento |
required_feature_code |
string nullable | Feature richiesta |
required_profile_code |
string nullable | Profilo richiesto |
required_permission |
string nullable | Permesso/FAB action |
min_status |
string | Es. active, readonly |
readonly_behavior |
string | show, hide, disable_writes |
is_active |
boolean | Menu attivo |
metadata_json |
json/text | Estensioni |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
menu_key; - indice su
module_code; - indice su
parent_key; - indice su
required_feature_code.
Nota: menu_key deve diventare la fonte stabile. Le label devono essere solo presentazione.
module_feature¶
Associazione tra modulo e feature flag.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo |
feature_code |
string | Es. feature.ras.extrabit_zip_export |
name |
string | Nome leggibile |
description |
text nullable | Descrizione |
default_enabled |
boolean | Default catalogo |
included_in_plan_code |
string nullable | Piano minimo |
is_billable_addon |
boolean | Feature fatturabile separatamente |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(module_code, feature_code); - indice su
feature_code.
Runtime:
- la tabella non sostituisce subito
TenantFeatureFlag; - definisce a quale modulo appartiene una feature;
TenantFeatureFlagresta la decisione tenant-specific.
module_profile¶
Profili suggeriti o richiesti da un modulo.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo |
profile_code |
string | Profilo |
profile_role |
string | admin, operator, readonly, external, technical |
default_enabled |
boolean | Abilitato di default quando il modulo e' attivo |
default_readonly |
boolean | Readonly di default |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(module_code, profile_code); - indice su
profile_code.
Adapter:
- legge/scrive
TenantRoleProfile; - genera default per
TenantRoleProfileEntitlementquando si attiva un modulo.
module_plan¶
Piani commerciali associati a un modulo.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo |
plan_code |
string | trial, base, pro, enterprise, addon_* |
name |
string | Nome piano |
description |
text nullable | Descrizione |
billing_cycle |
string | monthly, yearly, one_time, usage |
is_active |
boolean | Piano attivo |
is_trial |
boolean | Trial |
trial_days |
integer nullable | Durata trial |
default_currency |
string | Es. EUR |
sort_order |
integer | Ordinamento |
metadata_json |
json/text | Estensioni |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(module_code, plan_code); - indice su
module_code,is_active.
module_price¶
Prezzi base per modulo/piano. Non sostituisce subito i listini verticali RAS.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo |
plan_code |
string | Piano |
price_code |
string | Codice prezzo |
amount |
decimal | Importo |
currency |
string | EUR |
billing_cycle |
string | monthly, yearly, one_time, usage |
valid_from |
date nullable | Validita |
valid_to |
date nullable | Validita |
is_active |
boolean | Stato |
external_price_ref |
string nullable | Ponte verso listino esterno |
metadata_json |
json/text | Estensioni |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(module_code, plan_code, price_code); - indice su validita e stato.
module_service_mapping¶
Mapping tra catalogo moduli e service code legacy/canonici.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
module_code |
string | Modulo |
plan_code |
string nullable | Piano |
canonical_service_code |
string | Es. SAFE_RAS_REDAZIONE |
legacy_service_code |
string opzionale | Es. RAS_CONDOMINIO_RED; DB normalizzato a stringa vuota per unicita MySQL |
source_system |
string | service_catalog, commerciale_listino, manual, legacy |
source_ref |
string nullable | ID o codice sorgente |
workflow_type |
string nullable | Es. ras_quote, dvr |
is_active |
boolean | Stato |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(canonical_service_code, legacy_service_code, source_system); - indice su
module_code; - indice su
legacy_service_code.
Nota MySQL: legacy_service_code e' opzionale a livello logico, ma nello schema fisico non deve essere NULL quando partecipa al vincolo unique. Usare stringa vuota come valore normalizzato.
tenant_module_subscription¶
Subscription tenant al modulo/piano. Questa e' la fonte commerciale-operativa futura.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
tenant_key |
string | Tenant |
module_code |
string | Modulo canonico |
plan_code |
string nullable | Piano |
status |
string | trial, active, readonly, suspended, expired, disabled |
enabled |
boolean | Flag rapido |
valid_from |
datetime nullable | Decorrenza |
valid_to |
datetime nullable | Scadenza |
grace_until |
datetime nullable | Periodo di grazia |
billing_cycle |
string nullable | Ciclo |
agreed_price |
decimal nullable | Prezzo concordato |
currency |
string | EUR |
auto_renew |
boolean | Rinnovo automatico |
source |
string | manual, service_catalog, commerciale, migration, trial |
source_ref |
string nullable | ID legacy o contratto |
notes |
text nullable | Note |
created_by |
string nullable | Audit |
updated_by |
string nullable | Audit |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique consigliato su
(tenant_key, module_code)per la prima versione; - se in futuro servono piu piani contemporanei, passare a unique su
(tenant_key, module_code, plan_code, source_ref); - indice su
tenant_key,module_code,status,valid_to.
tenant_module_limit¶
Limiti quantitativi per tenant/modulo.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
tenant_key |
string | Tenant |
module_code |
string | Modulo |
limit_key |
string | Es. users.max, storage.bytes |
limit_value |
decimal/string | Valore |
unit |
string nullable | count, bytes, monthly, days |
scope |
string | tenant, user, cliente, module |
scope_ref |
string opzionale | ID utente/cliente se scoped; DB normalizzato a stringa vuota per unicita MySQL |
valid_from |
datetime nullable | Validita |
valid_to |
datetime nullable | Validita |
source |
string | plan, override, manual, legacy |
is_active |
boolean | Stato |
notes |
text nullable | Note |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(tenant_key, module_code, limit_key, scope, scope_ref); - indice su
tenant_key,module_code,limit_key.
Nota MySQL: scope_ref e' opzionale a livello logico, ma nello schema fisico non deve essere NULL quando partecipa al vincolo unique. Usare stringa vuota come valore normalizzato.
tenant_module_rule¶
Regole configurabili per tenant/modulo.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
tenant_key |
string | Tenant |
module_code |
string | Modulo |
rule_key |
string | Codice regola |
rule_type |
string | boolean, threshold, workflow, pricing, visibility, integration |
config_json |
json/text | Configurazione validata |
status |
string | active, disabled, readonly |
priority |
integer | Precedenza |
valid_from |
datetime nullable | Validita |
valid_to |
datetime nullable | Validita |
notes |
text nullable | Note |
created_at |
datetime | Audit |
updated_at |
datetime | Audit |
Vincoli:
- unique su
(tenant_key, module_code, rule_key); - indice su
tenant_key,module_code,status.
tenant_module_activation_event¶
Registro eventi attivazione/modifica.
| Campo | Tipo logico | Note |
|---|---|---|
id |
integer | PK |
tenant_key |
string | Tenant |
module_code |
string | Modulo |
event_type |
string | created, activated, suspended, expired, renewed, plan_changed, limit_changed |
old_status |
string nullable | Stato precedente |
new_status |
string nullable | Stato nuovo |
payload_json |
json/text | Dettaglio evento |
created_by |
string nullable | Utente/processo |
created_at |
datetime | Audit |
Indici:
tenant_key,module_code,event_type,created_at.
Adapter verso tabelle esistenti¶
Adapter TenantModuleEntitlement¶
Responsabilita:
- mantenere il runtime compatibile con
is_module_enabled; - generare o aggiornare entitlement legacy quando cambia
tenant_module_subscription; - leggere gli entitlement esistenti durante la transizione.
Regola proposta:
| Subscription | Entitlement legacy |
|---|---|
active |
enabled=True, status=active |
trial |
enabled=True, status=trial |
readonly |
enabled=True, status=readonly |
suspended |
enabled=False, status=suspended |
expired oltre grace |
enabled=False, status=expired |
disabled |
enabled=False, status=disabled |
Nota critica: eliminare progressivamente i commit impliciti nelle letture di stato. La lettura non deve creare righe in produzione; i seed devono essere job espliciti.
Adapter TenantFeatureFlag¶
Responsabilita:
- associare feature al modulo tramite
module_feature; - applicare default di piano;
- mantenere override tenant-specific.
Precedenza proposta:
- override esplicito in
TenantFeatureFlag; - piano attivo su
tenant_module_subscription; - default
module_feature; - default globale disabilitato.
Adapter profili¶
Responsabilita:
- associare profili a moduli tramite
module_profile; - attivare profili tenant consigliati quando si attiva un modulo;
- non rimuovere automaticamente profili utente gia assegnati senza preview.
Precedenza proposta:
- assegnazione utente esplicita;
- entitlement profilo tenant;
- default modulo/piano;
- readonly globale se subscription readonly.
Adapter ServiceCatalog¶
Responsabilita:
- ponte verso catalogo prezzi semplice;
- supportare attivazioni tenant gia esistenti;
- non diventare subito il modello completo dei moduli.
Mapping consigliato:
ServiceCatalog.code->module_service_mapping.canonical_service_codeolegacy_service_code;TenantServiceSubscription->tenant_module_subscriptionquando il servizio rappresenta un modulo/piano tenant;- mantenere
TenantServiceSubscriptionper servizi non ancora migrati.
Adapter CommercialeListino*¶
Responsabilita:
- lasciare i listini RAS verticali dove sono;
- collegare i service code RAS al catalogo tramite
module_service_mapping; - non generalizzare subito il motore listini RAS a tutti i moduli.
Mapping iniziale:
| Legacy | Canonico | Modulo |
|---|---|---|
AMM_PORTALE_RAS |
SAFE_RAS_ADMIN_PORTAL |
safe_admin_portal / safe_ras |
AMM_RAS_SCAD_DOC |
SAFE_RAS_ADMIN_DOC_EXPIRY |
safe_admin_portal / safe_ras |
RAS_CONDOMINIO |
SAFE_RAS_CONDOMINIO |
safe_ras |
RAS_CONDOMINIO_RED |
SAFE_RAS_REDAZIONE |
safe_ras |
RAS_CONDOMINIO_RIAL |
SAFE_RAS_RIALLINEAMENTO |
safe_ras |
Adapter menu runtime¶
Responsabilita:
- generare menu da
module_menu_itemper i moduli migrati; - lasciare menu hardcoded per i moduli non migrati;
- usare
menu_keystabile, non label testuali, come fonte di policy; - mantenere compatibilita temporanea con policy label-based.
Fasi:
- shadow mode: calcolare menu dichiarativo e confrontarlo con menu reale;
- hybrid mode: alcuni moduli leggono da catalogo;
- catalog mode: modulo completamente dichiarativo;
- cleanup: rimuovere condizioni hardcoded per il modulo migrato.
Enforcement target¶
Ogni accesso a modulo/route dovrebbe valutare:
- tenant valido;
- subscription modulo valida;
- entitlement legacy coerente;
- feature richiesta attiva;
- profilo utente compatibile;
- permission FAB/route compatibile;
- stato non scaduto;
- readonly rispettato;
- limiti quantitativi rispettati;
- dipendenze runtime soddisfatte.
Il menu puo nascondere voci, ma non deve essere l'unico controllo.
Stati canonici¶
| Stato | Significato runtime | Note |
|---|---|---|
trial |
Abilitato con scadenza | Limiti bassi e data obbligatoria |
active |
Abilitato pieno | Stato normale |
readonly |
Lettura consentita, scritture bloccate | Deve valere anche per API/import/job |
suspended |
Accesso bloccato temporaneamente | Recuperabile |
expired |
Scaduto | Puo avere grace in readonly |
disabled |
Disabilitato | Stato amministrativo |
Limiti canonici iniziali¶
| Limit key | Tipo | Moduli candidati |
|---|---|---|
users.max |
count | tutti |
storage.bytes |
bytes | documentale, tecnici, mailops |
documents.max |
count | documentale, RAS, DVR |
exports.monthly |
count/month | documentale, RAS |
practices.monthly |
count/month | RAS, DVR, tecnici |
tickets.monthly |
count/month | ticketing |
courses.active |
count | formazione |
students.active |
count | formazione |
integrations.enabled |
count/list | OSM, WooCommerce, SSO |
Seed iniziale consigliato¶
module_catalog¶
| module_code | legacy | type |
|---|---|---|
core_anagrafiche |
anagrafiche |
core |
core_documentale |
documentale |
sellable o core |
core_catalogo_servizi |
service_catalog |
internal |
safe_ras |
ras |
sellable |
safe_commerciale |
commerciale |
sellable |
safe_formazione |
formazione |
sellable |
safe_tecnici |
tecnici |
sellable |
safe_interventi |
interventi |
internal o alias di safe_tecnici |
safe_ticketing |
tickets |
sellable |
safe_calendar |
calendar |
addon |
safe_file_space |
tecnico_file_space |
addon |
safe_mailops |
mailops |
addon |
safe_dvr |
dvr |
addon |
safe_compila |
safecondo_compila |
addon |
safe_admin_portal |
amministratore_condominio |
addon |
integration_osm |
osm |
integration |
Nota: safe_interventi puo essere evitato come modulo pubblico e trattato come legacy alias di safe_tecnici.
Piano migrazione concettuale¶
Step 1 - Tabelle e seed¶
- creare tabelle catalogo;
- seed moduli;
- seed dipendenze;
- seed mapping service code;
- nessun cambio runtime.
Step 2 - Vista controllo¶
- nuova vista control-plane "Catalogo Moduli";
- vista "Attivazioni Moduli";
- lettura combinata catalogo + entitlement + service subscription;
- solo diagnostica o edit limitato.
Step 3 - Sync verso entitlement¶
- se si attiva una subscription, aggiornare
TenantModuleEntitlement; - non leggere ancora dal nuovo catalogo per autorizzare tutto;
- logging eventi in
tenant_module_activation_event.
Step 4 - Feature e profili¶
- collegare feature a moduli;
- applicare default feature per piano;
- suggerire profili da abilitare;
- preview prima di modificare assegnazioni utente.
Step 5 - Menu shadow¶
- calcolare menu dichiarativo in parallelo;
- confrontare con menu effettivo;
- produrre report differenze per tenant/profilo.
Step 6 - Primo modulo migrato¶
Modulo candidato: safe_file_space o safe_mailops, per blast radius ridotto.
Motivo:
- menu limitato;
- dipendenze chiare;
- limiti semplici;
- minore rischio rispetto a RAS/commerciale.
Step 7 - Moduli complessi¶
Migrare dopo:
core_documentale;safe_tecnici;safe_formazione;safe_commerciale;safe_ras.
RAS va lasciato dopo commerciale/documentale perche' dipende da entrambi.
Invarianti da rispettare¶
- Nessuna lettura deve creare righe o fare commit.
- I service code legacy devono restare validi finche esistono offerte/contratti/documenti che li usano.
- La disattivazione menu non equivale a blocco autorizzativo.
- Il readonly deve essere valutato anche sulle azioni POST/API/job.
- Le dipendenze devono bloccare nuove attivazioni incoerenti, ma non rompere tenant legacy senza migrazione guidata.
- I moduli core non devono essere disattivabili accidentalmente da pannelli commerciali.
- Ogni sync automatico deve scrivere un evento audit.
Test e verifiche richieste in fase implementativa¶
Quando si passera al codice, servono test su:
- attivazione modulo -> entitlement legacy aggiornato;
- scadenza subscription -> stato
expiredoreadonlycon grace; - feature inclusa nel piano -> feature flag effettiva;
- override feature tenant -> prevale sul default piano;
- profilo abilitato da modulo -> visibilita menu coerente;
- menu nascosto -> route diretta negata;
- readonly -> POST/API/job bloccati;
- limite superato -> operazione negata con messaggio chiaro;
- mapping legacy service code -> canonico;
- shadow menu -> differenze tracciate.
Decisioni ancora aperte¶
core_documentaledeve essere vendibile o core incluso in tutti i piani?safe_interventideve esistere come modulo separato o essere alias interno disafe_tecnici?- Il primo unique su
tenant_module_subscriptiondeve consentire un solo piano per modulo/tenant? ServiceCatalogdeve diventare solo adapter o essere evoluto nel catalogo prezzi generale?- Quale modulo e' il primo candidato per migrazione menu dichiarativa?
- Quanto deve durare la compatibilita label-based delle policy menu?
Conclusione fase 2¶
La direzione consigliata e' introdurre un catalogo moduli come livello canonico sopra le tabelle attuali. Le tabelle esistenti non vanno eliminate: vanno collegate tramite adapter e poi migrate gradualmente.
Il primo risultato tecnico utile non e' riscrivere RAS o commerciale, ma creare una fonte unica che dica cosa e' un modulo, quali menu possiede, quali feature/profili richiede, quali piani lo vendono e quali limiti lo governano.