Una busta paga è senza dubbio uno dei documenti più letti e meno compresi nella vita di un dipendente. La si riceve ogni mese, la si conserva fino alla pensione, eppure quasi nessuno è in grado di spiegare come si passa dal salario lordo alla somma effettivamente versata. Questa opacità non è un caso: dietro ogni riga si nasconde un accumulo di regole, eccezioni e casi particolari accumulati da decenni.
Per un ingegnere, questa osservazione ne nasconde un'altra, più interessante. Una busta paga non è altro che l'esecuzione deterministica di un albero di regole : una funzione che prende in input un centinaio di variabili (salario, stato, anzianità, mutua, assenze, cumuli da gennaio…) e produce un insieme di importi. Il vero argomento non è quindi il calcolo, è perfettamente deterministico, ma il modo di codificare migliaia di regole interconnesse senza che la base di codice diventi, a sua volta, una magia nera.
Questa è la scommessa fatta da Puncto, un
software di gestione paghe SaaS francese: piuttosto che tradurre il Codice del lavoro in TypeScript imperativo, descrivere la regolamentazione in un DSL dichiarativo,
Publicodes, per poi eseguirlo da un back-end tipizzato. Ecco come, e i problemi di ingegneria che questo solleva realmente.
Il Codice del lavoro è una codebase legacy
La regolamentazione delle paghe ha esattamente le proprietà che si temono in un vecchio monolite:
Regole stratificate : ogni contributo (salute, pensione, disoccupazione, CSG-CRDS, AT/MP…) ha il suo tasso, la sua base imponibile, i suoi massimali, le sue detrazioni.
Eccezioni ovunque : apprendista, dirigente, stagista, forfait giornaliero… lo stesso contributo si calcola diversamente a seconda dello status.
Un modello sociale in evoluzione : tassi, SMIC e massimale della Previdenza sociale cambiano almeno una volta all'anno, a volte nel corso dell'anno.
Dipendenze circolari : certi valori si definiscono in funzione di sé stessi.
Codificare questo in modo imperativo è fattibile, ma si ottengono rapidamente migliaia di linee di condizioni annidate: ordine di valutazione da gestire manualmente, dipendenze implicite, e una base illeggibile per le sole persone in grado di validarla : gli esperti paghe, che non sono sviluppatori. Il problema non è scrivere il calcolo, è di mantenerlo verificabile.
Publicodes: rendere una regolamentazione eseguibile
Publicodes è un linguaggio dichiarativo open source, sviluppato originariamente dal team di
mon-entreprise.urssaf.fr per modellare il diritto sociale francese. Il principio: si descrivono
regole e le loro dipendenze, non un algoritmo. Il motore costruisce il grafo e lo valuta autonomamente, come un foglio di calcolo ricalcola le sue celle.
Una regola «foglia» si riduce a una domanda:
salaire de base:
question: Salaire de base ce mois ?
unité: €
Una regola calcolata compone altre regole per nome:
rémunération brute:
unité: €
somme:
- salaire de base
- total primes
- indemnisations d'absences
Tre punti ne derivano direttamente. Innanzitutto, il nome della regola è il suo identificatore : si fa riferimento a salario base tramite la sua denominazione francese, il modello è leggibile così com'è. Poi, non si codifica mai l'ordine di calcolo : il motore ne deduce il grafo delle dipendenze e lo valuta su richiesta. Infine, questo file è la specifica: un esperto paghe lo rilegge e lo valida riga per riga, il che sarebbe impossibile con l'equivalente imperativo. In Puncto, questo modello rappresenta oggi qualche centinaio di regole, distribuite su diversi file e organizzate in grandi moduli.
Le vere sfide
Un «hello world» Publicodes è ingannevolmente semplice. La difficoltà emerge quando le regole si intrecciano. Alcuni esempi, tratti dalla vera codebase, lo illustrano.
1. Le dipendenze circolari
In caso di assenza per malattia, il datore di lavoro «mantiene» spesso il salario: integra le indennità giornaliere (IJSS) versate dalla Previdenza sociale. Queste IJSS devono essere sottratte dal lordo per evitare un doppio conteggio… ma il lordo serve proprio a calcolare una parte di ciò che ne dipende. Riferimento circolare. In modo imperativo, la si tratterebbe con un passaggio di calcolo in due fasi. Publicodes la espone come una proprietà dichiarativa:
rémunération brute:
résoudre la référence circulaire: oui
unité: €
somme:
- salaire de base
- total primes
- indemnisations d'absences
- IJSS brutes à soustraire
avec:
IJSS brutes à soustraire:
applicable si: maintien du salaire = oui
valeur: total IJSS ce mois * -1
Con risolvere il riferimento circolare, il motore rileva il ciclo e itera finché il valore si stabilizza (punto fisso). La complessità è dichiarata, non arrangiata in un'orchestrazione interna che bisognerebbe ritestare ad ogni evoluzione.
2. L'inversione numerica: trovare il lordo partendo dal netto
In Francia, un salario viene quasi sempre annunciato al lordo. Ma può capitare che si debba garantire un netto preciso: un netto garantito negoziato all'assunzione, o una regolarizzazione del netto a fine anno. Invece tutta la meccanica delle paghe funziona nell'altro senso: si parte dal lordo, si sottraggono i contributi e poi le imposte, e si ottiene il netto. Come trovare il lordo che produrrà esattamente un netto dato?
Il riflesso sarebbe di scrivere la formula inversa. Salvo che non ce n'è una: il netto in funzione del lordo è una funzione a gradino, non lineare (fasce, massimali, soglie di esonero). Non la si inverte algebricamente. L'unica via affidabile è numerica: provare un lordo, calcolare il netto corrispondente, confrontare con l'obiettivo, regolare, ricominciare fino a convergenza. Anche qui, Publicodes fornisce il meccanismo, l'inversione numerica : si dichiara che una regola solitamente calcolata «in avanti» può essere ritrovata a partire da una delle sue uscite.
rémunération brute calculée depuis le net:
unité: €
applicable si: option de régularisation du net
inversion numérique:
- net à payer avant impôt sur le revenu
remplace:
références à:
rémunération brute
Il motore fa quindi girare tutto il calcolo lordo → netto in loop, regolando il lordo ad ogni iterazione finché il netto calcolato non combacia con l'obiettivo (una ricerca dello zero). Due dettagli eleganti: sostituisce fa sì che questo lordo «invertito» si sostituisca automaticamente al lordo ovunque nel modello non appena si fornisce un netto, e applicabile se mantiene il meccanismo dormiente per il resto del tempo. Soprattutto, non si riscrive un secondo modello «all'indietro»: è lo stesso albero di regole, eseguito nell'altro senso. Cugina del riferimento circolare visto più in alto, l'inversione numerica dice bene cosa sia questo motore: un risolutore iterativo, non una semplice cascata di formule.
Il ponte tra Publicodes e TypeScript
È qui che si gioca la parte più interessante lato ingegneristico: far dialogare un modello scritto in francese con un back-end tipizzato, senza perdere le garanzie né dell'uno né dell'altro.
Alla compilazione, i file .publicodes vengono trasformati in un grafo JSON e in tipi TypeScript generati: l'unione di tutti i nomi delle regole esiste ora come tipo. Concretamente, la chiamata di una regola è autocompletata, e chiedere una regola che non esiste diventa un' errore di compilazione, non un'eccezione al runtime in piena generazione di bollettino. Si istanzia un motore per bollettino, poi si valuta su richiesta:
const { payslipCalculator } = getPayslipCalculator(payslipVariables)
const net = payslipCalculator('net payé')
const superBrut = payslipCalculator('super brut')
Resta l'impedenza: Publicodes ha le sue convenzioni, ereditate dal diritto (un booleano si scrive sì/no, una data JJ/MM/AAAA, un'enumerazione si cita: 'CDI'). Un wrapper tipizzato fa da ponte in entrambi i sensi, e soprattutto, si rifiuta di restituire un risultato se manca una variabile richiesta:
calculateValue<T extends RuleNames>(value: T) {
const res = super.evaluate(value)
const missingVariables = Object.keys(res.missingVariables)
.filter((key) => key.includes('régularisation') === false)
if (missingVariables.length > 0) {
throw new Error(`Missing publicodes variables ${JSON.stringify(missingVariables)}`)
}
return res.nodeValue as RuleValue[T]
}
Questa scelta di fail-fast è una decisione di progettazione a sé stante. In un ambito dove una variabile dimenticata non causa un crash ma produce un importo falso, meglio un'eccezione esplicita che un bollettino silenziosamente errato.
Testare un calcolo dove l'errore costa caro
Una volta che questa base è in atto, si verifica un cambio di mentalità, ed è contro-intuitivo: il calcolo non si sbaglia quasi mai. Un albero di regole validato dà sempre il risultato corretto per input validi. In pratica, gli errori di busta paga provengono quasi esclusivamente da un parametrizzazione o di una inserimento : mutua mal affiliata, anzianità falsa, premio dimenticato, assenza mal registrata.
La strategia di test segue questa logica: si bloccano situazioni complete e si verifica ogni riga al centesimo. Un bollettino per un quadro a 64 k€/anno di aprile 2025 produce così un centinaio di asserzioni del tipo:
test('CSG déductible', () => {
assert.strictEqual(
payslipCalculator("CSG-CRDS . CSG déductible de l'impôt sur le revenu"),
360.7,
)
})
Questi test hanno un doppio ruolo: rete anti-regressione, e specifica eseguibile. Quando un tasso cambia il 1° gennaio, si aggiunge una situazione datata con i nuovi importi attesi; la non-regressione sui vecchi millesimi è garantita per costruzione grazie a un versionamento per data a livello publicodes. Risultato: modificare una regola di contribuzione, di solito temuto in un software paghe, diventa un'operazione controllata: se un centesimo si sposta da qualche parte, un test lo segnala immediatamente.
Ciò che il dichiarativo porta, e ciò che costa
Il bilancio è chiaro. A credito di Publicodes: un modello auditabile dagli esperti di settore stessi, dei diff Git leggibili quando una regola cambia, una risoluzione automatica delle dipendenze (nessun ordinamento topologico a mano), un versionamento normativo quasi gratuito tramite variazioni sulla data - il tutto avvolto in uno strato di tipi che riporta la sicurezza statica di TypeScript.
Il costo esiste: imparare le convenzioni del linguaggio, scrivere e mantenere lo strato di impedenza, e accettare che un motore che risolve cicli per iterazione sia un po' meno trasparente di una funzione svolta a mano. Rapportato a un diritto sociale vivo, retroattivo e pieno di eccezioni, lo scambio è molto favorevole.
La lezione va oltre la busta paga: quando un dominio assomiglia a una codebase legacy che nessuno padroneggia completamente, la risposta giusta non è sempre scrivere più codice: a volte è trovare il linguaggio giusto per descriverlo, e lasciare che una macchina lo esegua. È esattamente ciò che fa funzionare il motore paghe di
Puncto quotidianamente.