Passa al contenuto principale

Logica applicativa — dominio reg

🎯 Cosa fa

Il dominio reg non ha un service dedicato (ICompanyService o equivalente). La logica applicativa vive in due punti:

  1. CompaniesQueryModifier — hook pre/post su CRUD companies
  2. 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: setta imported = false sull'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 UpdateSingle e UpdateWhere: ricalcola il livello di rischio dei workers associati (o di tutti quelli fuori sync, se UpdateWhere senza entity) e rinfresca la variante formativa efficace con EXEC 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 (dominio job)
  • ITrainingExpirationService (dominio edu)
  • ISimpleCRUDService (CRUD generico)

Forms/CompanyForm.razor.cs — validazione VIES e cascade ATECO

Logiche chiave:

  • SetRiskLevelId — invocato dopo cambio ATECO o toggle riskLevelOverride. Carica il riskLevel associato all'ATECO dal dominio job e lo imposta sul dataItem se override non attivo.
  • ValidateVatCodeAsync (:18-44) — chiama VIES e, in caso di esito positivo, sovrascrive companyName, formattedAddress e vatCode con i dati ufficiali, poi rigeocodifica l'indirizzo. Memorizza vatCodeValidated, cioè la partita IVA su cui la validazione è stata fatta. Il flag viesValidated viene invece scritto più tardi, in CheckDataItemBeforeSave (: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:

DipendenzaProvenienzaUso
DataLayer.job.worker.UpdateRiskLevelDominio jobCascade rischi da QueryModifier
IRiskInheritanceServiceDominio jobDettaglio rischi aziendali in UI
ITrainingExpirationServiceDominio eduStats compliance in UI
ISimpleCRUDServiceBrighela.SimpleCRUDCRUD 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.cs
  • TrainingHub.BackOffice/Components/CRUD/reg/Company.razor.cs
  • TrainingHub.BackOffice/Components/CRUD/reg/Forms/CompanyForm.razor.cs
  • Services/QueryModifiers/HostExtensions.cs:28 — registrazione DI del modifier (Program.cs:77 si limita a .AddQueryModifiers())
  • Services/QueryModifiers/EntityDeletionGuard.cs — veto e cascata sulle cancellazioni

⚠️ Debito tecnico

  • Nessun ICompanyService. Pattern esistente in inv (IInvoiceService) non replicato in reg. Se la logica cresce, l'assenza di un layer di service diventa problema per testabilità e separazione di responsabilità.
  • PostExecutionQuery senza entity ricalcola tutti i workers. Path UpdateWhere senza entity specifica invoca worker.UpdateRiskLevel(simpleCRUD) globale. Su dataset grandi è costoso. Mitigazione: evitare UpdateWhere su companies o accettare il costo come raro.
  • Cross-dominio nel code-behind UI. Company.razor.cs inietta services di job ed edu. 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.cs o in un servizio centralizzato.
  • imported reset 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