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.

Post-execution:

  • Su UpdateSingle e UpdateWhere: ricalcola il livello di rischio dei workers associati all'azienda modificata (o di tutti i workers, se UpdateWhere senza entity specifica).
public async ValueTask PostExecutionQuery(
QueryModifierArgs<company> args,
object? executionResult)
{
if (operationsToHandle.Contains(args.Operation))
{
if (args.Entity != null)
await worker.UpdateRiskLevel(simpleCRUD, companyId: args.Entity.id);
else
await worker.UpdateRiskLevel(simpleCRUD); // tutti
}
}

private static readonly List<SqlOperation> operationsToHandle = [
SqlOperation.UpdateSingle,
SqlOperation.UpdateWhere
];

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 — chiama l'integrazione VIES (servizio esterno o API), setta viesValidated = true in caso di successo.

Esempio:

private async Task SetRiskLevelId()
{
if (dataItem.riskLevelOverride) return;
var risk = await simpleCRUD.GetAsync<job.atecoCode, string>(dataItem.atecoCode);
if (risk != null) dataItem.riskLevelId = risk.riskLevelId;
}

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.

Cascade implicita via DB FK

Cancellazione di un'azienda è impossibile se ci sono record dipendenti (invoices, workers, ecc.): la cancellazione fallisce con errore FK DB. Il messaggio generico "record in uso" emerge anche in UI (vedi pagina utente Convenzioni (docs-site-user) per caso simile).

📁 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
  • Program.cs — registrazione DI di QueryModifier e services correlati

⚠️ 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