Logica applicativa — dominio reg
🎯 Cosa fa
Il dominio reg non ha un service dedicato (ICompanyService o
equivalente). La logica applicativa vive in due punti:
CompaniesQueryModifier— hook pre/post su CRUDcompanies- Code-behind custom
.razor.cs— logica UI per caricamento dettagli, validazioni form, integrazioni cross-dominio
Questa pagina documenta dove sta cosa e i pattern in uso.
🧩 CompaniesQueryModifier
Implementa Brighela.SimpleCRUD.Service.IQueryModifier<company>.
Registrato come hook sul CRUD standard di reg.companies.
Responsabilità
Pre-execution:
- Su
UpdateSingle: settaimported = falsesull'entity in arrivo. Effetto: alla prima modifica manuale dell'utente, il flag "azienda importata da Excel" viene resettato. - Su
DeleteSingle: applica la guardia di cancellazione — veto sui fatti bloccanti e cascata sui figli propri (:95-107, vedi sotto).
Post-execution:
- Su
Insert: crea la sede predefinita dall'indirizzo dell'azienda (companyLocation.EnsureDefaultForCompanyAsync,:68-69). - Su
UpdateSingleeUpdateWhere: ricalcola il livello di rischio dei workers associati (o di tutti quelli fuori sync, seUpdateWheresenza entity) e rinfresca la variante formativa efficace conEXEC edu.sp_refreshEffectiveVariantForScope @companyId(:79).
public async ValueTask PostExecutionQuery(
QueryModifierArgs<company> args,
object? executionResult)
{
// Il servizio è quello del chiamante: stessa connessione e transazione.
var db = CallerService(args);
if (operationsToHandle.Contains(args.Operation))
{
if (args.Entity != null)
{
await worker.UpdateRiskLevel(db, companyId: args.Entity.id);
var cid = args.Entity.id;
await db.ExecuteAsync(
"EXEC edu.sp_refreshEffectiveVariantForScope @companyId=@cid", new { cid });
}
else
{
await worker.UpdateRiskLevel(db); // tutti quelli fuori sync
}
}
}
Perché è un QueryModifier e non un service
- Si innesta su ogni percorso che aggiorna
companies, incluso:- UI CRUD standard (form azienda)
- Batch update da altri moduli
- Direct call al servizio generico
ISimpleCRUDService
- Non richiede che i chiamanti conoscano la logica di cascade rischio: funziona trasparentemente.
- Svantaggio: effetto "magico" — chi legge il codice chiamante non vede la cascade, scopre il comportamento solo leggendo il modifier.
🧩 Code-behind custom
Company.razor.cs — dettaglio aggregato
Carica in parallelo rischi aziendali, dipartimenti e compliance stats
quando l'utente seleziona un'azienda. Usa Task.WhenAll per
parallelismo e traccia lastLoadedCompanyId per idempotency.
Servizi iniettati:
IRiskInheritanceService(dominiojob)ITrainingExpirationService(dominioedu)ISimpleCRUDService(CRUD generico)
Forms/CompanyForm.razor.cs — validazione VIES e cascade ATECO
Logiche chiave:
SetRiskLevelId— invocato dopo cambio ATECO o toggleriskLevelOverride. Carica il riskLevel associato all'ATECO dal dominiojobe lo imposta suldataItemse override non attivo.ValidateVatCodeAsync(:18-44) — chiama VIES e, in caso di esito positivo, sovrascrivecompanyName,formattedAddressevatCodecon i dati ufficiali, poi rigeocodifica l'indirizzo. MemorizzavatCodeValidated, cioè la partita IVA su cui la validazione è stata fatta. Il flagviesValidatedviene invece scritto più tardi, inCheckDataItemBeforeSave(:46-61), e solo se la P.IVA non è cambiata dopo la validazione: modificarla dopo il controllo lo azzera.
SetRiskLevelId è sincrono e senza accesso al DB: legge dalla lista
FK già caricata nel form (CompanyForm.razor.cs:64-71).
private void SetRiskLevelId()
{
var selectedAteco = fk_atecoCode.Find(a => a.code == dataItem.atecoCode);
// … imposta dataItem.riskLevelId dal codice selezionato
}
Altri .razor.cs reg
CompanyContact.razor.cs,CompanyLocation.razor.cs,Headquarter.razor.cs— code-behind minimi, override standard senza logica estesa.
📦 Dipendenze cross-dominio
Il dominio reg dipende da:
| Dipendenza | Provenienza | Uso |
|---|---|---|
DataLayer.job.worker.UpdateRiskLevel | Dominio job | Cascade rischi da QueryModifier |
IRiskInheritanceService | Dominio job | Dettaglio rischi aziendali in UI |
ITrainingExpirationService | Dominio edu | Stats compliance in UI |
ISimpleCRUDService | Brighela.SimpleCRUD | CRUD generico |
I domini inv, edu, iso, bi dipendono a loro volta da reg (via
FK companyId nelle rispettive tabelle), creando relazione
bidirezionale di fatto: reg è base dati ma importa logica
cross-dominio tramite service injection.
🧩 Pattern e convenzioni
Idempotency per carichi UI
lastLoadedCompanyId in Company.razor.cs evita ricarichi duplicati
al reclick sulla stessa riga:
private Guid? lastLoadedCompanyId;
protected async Task OnSelectedCompanyChanged(Guid companyId)
{
if (lastLoadedCompanyId == companyId) return;
lastLoadedCompanyId = companyId;
// ... load ...
}
Pattern da replicare quando una selezione di riga triggera caricamenti di dataset correlati costosi.
Nessun layer di validazione business
Il form non blocca partite IVA mal formate, codici fiscali invalidi o SDI errati: la validazione strutturale non è implementata. Emerso come open question in pagine utente e schema DB.
Cancellazione di un'azienda — veto esplicito e cascata
Non è una cascata implicita del DB e non è un errore FK.
CompaniesQueryModifier.PreExecutionQuery (righe 95-107) intercetta il
DeleteSingle e delega a
Services/QueryModifiers/EntityDeletionGuard.cs, che applica due
liste dichiarate nel modifier:
blocking(righe 17-27) — la presenza di uno di questi vieta l'eliminazione:job.workers,inv.invoices,fin.projectCompanies,fin.companyMemberships,fin.implementingBodies,iso.companyCertifications,edu.locations,edu.teachers. L'utente vede un messaggio localizzato (company_delete_blocked) che nomina la categoria di dato, non la tabella.ownChildren(righe 31-44) — pezzi propri dell'anagrafica, cancellati in cascata:reg.companyDocuments,reg.companiesPreferences,reg.companyTags,reg.companiesExternalRspps,reg.companiesTerritorialReferents,job.companiesRisks,job.departments,inv.companyConventions,inv.reminders,reg.companyContacts,reg.companyLocations. L'ordine conta: le sedi vanno per ultime perché i lavoratori le referenziano con FK NOT NULL — e se ce ne fossero, il veto avrebbe già fermato tutto.
Quindi un'azienda con solo contatti e sedi si cancella, portandosi via i figli; una con anche un solo lavoratore o una fattura no, con un messaggio che dice quale.
Lo stesso meccanismo regge la cancellazione del lavoratore
(job/WorkersQueryModifier.cs:95-104) — vedi
dominio job.
📁 File chiave
TrainingHub.BackOffice/Services/QueryModifiers/reg/CompaniesQueryModifier.csTrainingHub.BackOffice/Components/CRUD/reg/Company.razor.csTrainingHub.BackOffice/Components/CRUD/reg/Forms/CompanyForm.razor.csServices/QueryModifiers/HostExtensions.cs:28— registrazione DI del modifier (Program.cs:77si limita a.AddQueryModifiers())Services/QueryModifiers/EntityDeletionGuard.cs— veto e cascata sulle cancellazioni
⚠️ Debito tecnico
- Nessun
ICompanyService. Pattern esistente ininv(IInvoiceService) non replicato inreg. Se la logica cresce, l'assenza di un layer di service diventa problema per testabilità e separazione di responsabilità. -
PostExecutionQuerysenza entity ricalcola tutti i workers. PathUpdateWheresenza entity specifica invocaworker.UpdateRiskLevel(simpleCRUD)globale. Su dataset grandi è costoso. Mitigazione: evitareUpdateWheresucompanieso accettare il costo come raro. - Cross-dominio nel code-behind UI.
Company.razor.csinietta services dijobededu. Concentra responsabilità di aggregazione nel componente UI. Se crescono i pannelli, valutare ViewModel/Query object dedicato. - Validazione P.IVA / CF / SDI non strutturata. Manca
validazione formato (Luhn, regex). Errori silenziosi al salvataggio.
Feature da aggiungere in
CompanyForm.razor.cso in un servizio centralizzato. -
importedreset implicito. Utente non è avvisato del comportamento. Documentare o rendere esplicito (es. toast "azienda marcata come manualmente verificata"). - Magic string
"imported"e"riskLevel"in flussi cascade. Refactoring in costanti o enum semplifica audit del comportamento.
🔗 Vedi anche
- Panoramica dominio
- Schema DB —
IX_companies_activeottimizza le query su aziende attive - Componenti UI — form con cascade ATECO e VIES
- Dominio
inv: servizi — pattern service-oriented per confronto