Skip to main content

Cosa espone data-service al verticale

Spostando il dominio nel verticale, data-service smette di essere un motore di calcolo e resta il layer di sicurezza e persistenza: risolve l'organizzazione dal token e filtra ogni riga. Il verticale non parla mai con datahub.

Questi sono i sette contratti su cui poggia il rimpatrio. Nessuno di loro contiene regole di dominio: dicono cosa si può leggere e scrivere, non cosa significa.

Una tabella nuova va registrata, o non esiste

Prima degli endpoint, la cosa che costa più tempo a riscoprire.

Una tabella aggiunta allo schema rendicontazione non è raggiungibile finché non compare in due punti di services/data-service/routes/rendicontazione.js:

  • RENDICONTAZIONE_TENANT_TABLES, che è il perimetro — fuori da lì /query e il CRUD generico rispondono 404;
  • CAPABILITY_PER_TABELLA, che dice quale permesso serve per scriverla. Chi non c'è ricade su admin, quindi la tabella risulta illeggibile a chi dovrebbe scriverla e nessun messaggio spiega perché.

Il sintomo è un 404 che somiglia a una migrazione mancante o a un datahub non aggiornato. Prima di cercare lì, controllare queste due liste.

La capability si sceglie da chi possiede il dato, non da chi capita di scriverlo. Quando due proprietari condividono una tabella, la risposta è un endpoint dedicato per la scrittura più stretta — non allargare la capability della tabella. /forecast/rows e /leave/consumed esistono entrambi per questo.

GET /rendicontazione/context

Organizzazione, ruoli e capability, risolti dal token. Il verticale non deduce mai il tenant: lo legge da qui.

L'header x-organization-id inviato dal client non è una fonte: viene confrontato con la membership e, se non combacia, la richiesta è respinta.

POST /rendicontazione/query

Letture multiple in una sola richiesta. È il contratto che rende sostenibile il confine di rete: il dettaglio di un progetto fa otto letture, e oltre il confine sarebbero diventate otto round trip.

  • massimo 25 letture per richiesta
  • tetto di 10.000 righe per lettura, più alto dei 1.000 del CRUD generico

La differenza fra i due tetti è voluta. Il CRUD serve a sfogliare, e mille righe sono già oltre ciò che una pagina mostra. La lettura multipla serve al dominio, che aggrega e non pagina: l'elenco progetti chiede 10.000 righe di ore mensili per calcolare i totali. Sotto quella soglia i totali sarebbero troncati senza alcun errore, che è peggio di una richiesta pesante.

Solo le tabelle del verticale sono raggiungibili. Non è una via per altri schemi.

GET/POST/PUT/DELETE /rendicontazione/tables/:table[/:id]

CRUD generico tenant-scoped. Il permesso dipende dalla tabella, non dal verbo. audit_log non è scrivibile da qui: il trail lo scrive il middleware con l'attore preso dal token, e consentirne la scrittura permetterebbe di fabbricarlo.

POST /rendicontazione/tables/:table/bulk

Scrittura in blocco, stesso permesso della scrittura singola sulla stessa tabella. Non concede niente di più: cambia quanti giri di rete costa.

Serve perché il codice ancora da spostare scrive riga per riga dentro cicli. Un mese di presenze è una riga per persona e per giorno: cinquanta persone per ventidue giorni sono 1.100 richieste HTTP per un solo import.

Alla prima riga che fallisce si interrompe, e la risposta dice quante ne erano passate — senza quel numero chi riceve l'errore non sa se ritentare tutto o solo il resto.

POST/DELETE /rendicontazione/forecast/rows

Le uniche scritture su project_employee_monthly_hours che il permesso forecast autorizza.

Quella tabella contiene sia i consuntivi sia le previsioni, e scriverla dal CRUD generico richiede actuals — un permesso che chi fa forecast non ha e non deve avere. Senza questo endpoint il ricalcolo, spostato nel verticale, sarebbe fallito su ogni riga; concedendo actuals al ruolo forecast si sarebbe aperta la scrittura anche dei consuntivi.

Da qui non può uscire una riga che si spacci per consuntivo: origine e stato sono imposti dal servizio, non accettati dal chiamante. L'invariante «il consuntivo non viene sovrascritto dalla previsione» diventa vero per il cancello, non solo per il codice che lo attraversa.

La DELETE applica lo stesso principio al contrario: il filtro su source=AUTOMATIC vive nel servizio, perché una cancellazione che si fida di cosa le viene detto di cancellare potrebbe portarsi via i consuntivi. Senza project_id rifiuta, invece di riguardare l'intero tenant.

POST /rendicontazione/leave/consumed

Le uniche scritture su employee_leave_balances che il permesso actuals autorizza. È la stessa forma di /forecast/rows, e per la stessa ragione.

Quella tabella tiene due cose con due proprietari diversi. I giorni previsti li inserisce Admin o HR, e scriverli richiede hr. I giorni goduti non li inserisce nessuno: sono il conteggio delle giornate 8FE nelle presenze, e chi importa le presenze ha actuals. Dargli hr per aggiornare un contatore gli aprirebbe l'anagrafica intera.

Da qui si scrive solo consumed_days. entitled_days non viene nemmeno letto dal corpo della richiesta: l'import non decide quanti giorni spettano a una persona. Se il saldo dell'anno non esiste viene creato con entitled_days a NULL — «ha preso ferie, quanti gliene spettino non l'ha ancora detto nessuno» — che è diverso da zero e deve restare distinguibile.

Le righe sono validate tutte prima di scriverne una: una riga sbagliata a metà elenco lascerebbe le precedenti applicate e le successive no, e il chiamante non saprebbe quali.

GET /rendicontazione/me/leave e PUT /rendicontazione/me/leave/plan

Il self-service del piano ferie, e le prime scritture concesse a self.

Stanno qui e non nel verticale per la stessa ragione di /me/employee: un dipendente ha la sola capability self, quindi non può leggere employee_leave_balances dal CRUD generico — servirebbe read — né scrivere il piano, che richiederebbe hr. Il verticale chiama con il token di chi ha fatto la richiesta, quindi non può farlo per lui: fa da tramite e basta.

Il dipendente si risolve dal token, mai da un parametro. Un employee_id nel corpo della richiesta non ha alcun effetto: non esiste una forma di queste chiamate che tocchi le ferie di qualcun altro.

La lettura restituisce sempre dodici mesi, con i totali dell'anno: previsti, già goduti, pianificati e ancora disponibili. previsti e disponibili sono null quando nessuno ha ancora detto quanti giorni spettino — un 0 direbbe «le hai finite».

La scrittura impone due regole che il browser non deve poter aggirare:

  • si pianificano solo i mesi futuri. Il mese in corso è in parte già passato e le sue giornate stanno già arrivando dal foglio presenze: pianificarci sopra le conterebbe due volte;
  • non si pianificano più giorni di quanti se ne abbiano, e il messaggio dice quanti se ne possono mettere. Il tetto guarda l'anno del mese pianificato, non l'anno in corso: i giorni non si ereditano da un anno all'altro, quindi pianificare il 2027 senza un saldo 2027 non ha tetto.

Il tetto vale solo dove entitled_days è noto. Bloccare chi non ce l'ha renderebbe la pagina inutile proprio il giorno in cui viene aperta, visto che oggi quel valore è NULL per tutti.

Mettere zero giorni in un mese toglie la riga invece di scriverci uno zero: un mese non pianificato è l'assenza di un piano, non un piano di niente.

GET /rendicontazione/users/:id

Chi è l'utente dietro un id. Serve a scrivere «approvato da X» accanto a una data.

Non può arrivare da /query perché le tabelle di identità non appartengono al verticale, ed è giusto così: risolvere un'identità è mestiere del layer di sicurezza.

Risponde solo per utenti che appartengono all'organizzazione di chi chiede, e restituisce tre campi — id, email, nome. Un id di un'altra organizzazione dà 404 identico a quello di un id inesistente: rispondere in modo diverso confermerebbe che quell'utente esiste da qualche parte. L'appartenenza è verificata prima di leggere l'utente, non dopo: filtrare a valle significa aver già caricato in memoria il dato di un altro tenant.

Cosa questi contratti non fanno

Non validano un progetto, non decidono quale run vince per un mese, non sanno cosa sia una previsione. Quelle sono regole di dominio e vivono nel verticale.

Conseguenza da conoscere: chi ha la capability di scrivere una tabella può chiamare data-service direttamente e scrivere una riga che il verticale avrebbe rifiutato. È l'attuazione di ADR-002 — «le capability si dividono» — non una dimenticanza. Il tenant scope e il permesso grossolano restano non aggirabili; il significato fine dei permessi è del verticale.