Skip to main content

Difetti trovati durante il rimpatrio

Trovati mentre si spostava il codice, e lasciati com'erano fino alla fine. Il motivo era sempre lo stesso: un'onda sposta codice senza cambiarne il comportamento, e correggere durante lo spostamento rende impossibile distinguere una differenza dovuta al trasloco da una dovuta alla correzione.

A rimpatrio finito quella ragione è scaduta, e i due difetti attivi sono stati chiusi. Restano elencati qui sotto, sotto Corretti, perché la spiegazione di perché i numeri sono cambiati vale più della loro sparizione.

Corretti

La cancellazione di ore importate puntava all'id sbagliato

Dove: modules/data-exchange/importDiff.js, caricaOreMensiliPerImport

La riga delle ore veniva arricchita con i campi del dipendente e del progetto tramite spread, e quegli oggetti hanno un proprio id che sovrascriveva quello della riga. La proposta di cancellazione puntava all'id del progetto: in modalità SYNC una riga non più presente nel foglio non veniva cancellata, e nel caso peggiore veniva cancellata la riga sbagliata.

Era presente anche nell'implementazione originale — verificato eseguendola sulla stessa fixture. Non introdotto dal rimpatrio.

Corretto ripristinando id, employee_id e project_id dopo gli spread. Lo snapshot del diff è cambiato in un solo valore: target_record_id da 20 (l'id del progetto) a 900 (l'id della riga).

Elenco e dettaglio progetti non concordavano

Dove: modules/projects/projectQueries.js

L'elenco aggregava solo ore mensili e run giornaliere; il dettaglio includeva anche allocation_consolidated_results. Sulla fixture l'elenco dichiarava 181 ore dove il dettaglio ne diceva 314.

Non era ancora attivo — nessun codice scriveva quella tabella — ma lo sarebbe diventato con la consolidazione.

Corretto estraendo la regola di prevalenza fra le sorgenti in oreEffettive, usata ora da entrambi: due copie della stessa regola avrebbero riprodotto la divergenza al primo cambiamento. Lo snapshot dell'elenco è cambiato in sei valori sulla riga del progetto 10 e quattro nel riepilogo, tutti verificati uno per uno prima di aggiornarlo a mano.

Ancora aperti

Il dettaglio dipendente non riclassifica le ore

Dove: rendicontazione-service/modules/workforce/employeeQueries.js

Il dettaglio progetto applica withDynamicDataStatus alle ore — quindi le righe coperte da baseline diventano BASELINE_COVERED e non entrano nei totali. Il dettaglio dipendente non lo fa.

Conseguenza: le stesse ore contano nel totale del dipendente e non in quello del progetto. Due numeri diversi per lo stesso fatto.

Non ha un test dedicato perché è comportamento registrato negli snapshot: si vedrà come differenza quando qualcuno lo correggerà, che è il momento giusto per accorgersene.

Il forecast non è riproducibile

Dove: rendicontazione-service/modules/forecasting/projectForecast.js Test: data-service/test/golden/forecast.golden.test.js, marcato todo

Il ricalcolo legge new Date() per stabilire da quale mese partire. Lo stesso input a marzo genera 9 previsioni, a maggio 6.

Non è un difetto di calcolo: è che il calcolo dipende da quando lo esegui. Finché è così, un forecast non può essere riprodotto per verificarlo, e la domanda «perché questo numero era diverso il mese scorso» non ha risposta.

Si chiude iniettando l'orologio invece di leggerlo. È previsto nella riscrittura del forecasting.

Il forecast non ha vincolo di capacità — chiuso il 18 agosto 2026

Dove: come sopra Test: stesso file, marcato todo

Il ricalcolo per progetto divide il budget residuo in parti uguali fra gli slot futuri, senza chiedersi quante ore una persona possa realmente lavorare in un mese. Sulla fixture prevede 355 ore in un mese per una persona sola.

Il ricalcolo globale invece un tetto ce l'ha — il monte ore annuo diviso dodici — quindi i due percorsi non solo danno risultati diversi, danno risultati diversi in modo strutturale.

Chiuso il 18 agosto 2026: i due percorsi sono diventati lo stesso motore. Il per-progetto non divide più il budget sugli slot, parte dalla capacità come il globale, e in più tiene conto delle ore che ciascuno ha già su altri progetti. Sulla fixture le ore massime in un mese passano da 355,56 a 153,33, e sede, tetto per risorsa e ferie pianificate valgono per entrambi.

Il costo standard di progetto è uno stub — chiuso il 18 agosto 2026

Dove: forecastRules.js (profiliCostoStandardDi) e allocationEngine.js (resolveHourlyCostEur, tariffaDaProfilo) Trovato: 18 agosto 2026, preparando la Fase 3

Entrambi i motori leggono projects.standard_cost_profiles, un JSON che mappa il job level sulla tariffa. Quella colonna non esiste in nessuna parte dello schema: non in init.sql, non in db/postgres/init/, non in nessuna migrazione. Esiste solo nelle fixture golden, che quindi sono più generose del database — è esattamente ciò che era già successo con job_level, e la ragione per cui quella lezione era stata scritta.

Chiuso il 18 agosto 2026 con le migrazioni 112114 e la sostituzione della lettura nei due motori: la tariffa ora viene da funding_body_rates, risolta per fascia, ente e data. Quanto segue resta perché spiega perché i numeri sono cambiati.

Non era una funzione rotta: era uno stub di una funzione mai costruita, ed è stato buttato invece che riparato. Il costo standard, come è scritto in To-be funzionale, non è una tariffa del progetto per job level: è una tariffa dell'ente finanziatore per macro-livello, storicizzata. Il progetto sceglie soltanto se usarla.

Ne discende che anche la pagina Progetti descrive una griglia di tariffe per job level che non può salvare nulla: è documentazione di qualcosa che non esiste.

Le conseguenze sono diverse nei due motori, e nessuna delle due si vede:

  • nel forecast un progetto a costo standard ottiene tariffa 0, quindi non produce nessuna previsione. Il conteggio finisce in skippedNoRate, che nessuna schermata mostra;
  • nel consuntivo si ricade su current_hourly_cost_eur, cioè il costo reale della persona. Un progetto dichiarato a costo standard viene quindi valorizzato al costo reale, in silenzio e con numeri plausibili.

I golden sono verdi perché le fixture contengono la colonna. Sono verdi su codice che in produzione non lo è.

Si chiude in Fase 3, ed è il motivo per cui i punti 1, 2 e 6 vanno affrontati insieme: la tariffa standard va spostata dal JSON di progetto a una tabella storicizzata per ente e macro-livello, e risolta alla data.

Le fixture sono state allineate il 18 agosto 2026, prima di toccare il codice, ed è quella modifica ad aver reso visibile il difetto. Sulla fixture del forecast globale il conto è questo:

primadopo
previsioni scritte2621
assegnazioni scartate per tariffa mancante23
progetti che ricevono previsioni10, 11, 15, 1610, 11, 16

Il progetto 15 — ZETA, l'unico a costo standard, sparisce dal forecast: le sue cinque previsioni non esistono, e le ore delle due persone che vi erano assegnate si ridistribuiscono sugli altri progetti, cambiando anche numeri che sembravano non c'entrare. Gli snapshot sono stati riscritti a mano su questi valori: non sono un miglioramento, sono la fotografia di cosa succede oggi in produzione, ed è da lì che la Fase 3 riparte.

Anche freeze_config è una colonna che non esiste — chiusa il 18 agosto 2026

Dove: projectQueries.js, freezeSchedule.js, projectPayload.js Trovato: 18 agosto 2026, allineando le fixture per la Fase 3

Stessa classe di standard_cost_profiles, trovata con lo stesso metodo: projects.freeze_config non compare in nessuno dei 118 file di migrazione né in init.sql. Il codice la legge, il payload la scrive, e datahub la scarta in silenzio.

Ne discende che il freeze warning — «questo progetto è in ritardo con il consolidamento» — in produzione non si accende mai: freeze_config è sempre null, quindi nessuna scadenza è configurata per nessun progetto. I test che lo verificano sono verdi perché la fixture contiene la colonna.

C'era però una terza conseguenza, e non era innocua: il salvataggio di un progetto era rotto. Il payload scriveva freeze_config a ogni PUT, datahub la rifiutava con __strict_columns e l'intera scrittura falliva. Arrivava al browser come «Servizio dati non raggiungibile» — un guasto inesistente — e nessuno collegava le due cose.

Chiusa dalla migrazione 116, che crea la colonna invece di togliere la funzione. Il congelamento è costruito per intero: maschera, payload, query, freezeSchedule, invarianti golden. Mancava un pezzo, non il disegno. Se un giorno non servisse più, si toglie insieme alla sua interfaccia — non lasciando a metà quello che c'è.

La verifica della 116 controlla tutte e tre le colonne che il payload scrive — freeze_config, max_hours_per_resource, work_location_id — e rifiuta il ritorno di standard_cost_profiles: è il modo di non riaprire lo stesso buco da un'altra parte.

Il self-service del dipendente è dichiarato e non implementato

Dove: ADR-003, migrazione 095

Il ruolo rendicontazione_employee esiste, ha la capability self, ed è descritto come «accesso self-service del dipendente». Ma self non è implementata a nessuno dei tre livelli necessari, e soprattutto rendicontazione.employees non ha alcuna colonna che colleghi una riga a un account.

La migrazione 095 ha tolto al ruolo il permesso di gateway: finché il filtro non esiste, un permesso di lettura su tutto il verticale è una promessa che il codice non può mantenere. Il ruolo non va assegnato.

Il prerequisito è un dato, non un permesso: vedi ADR-003.

Contatori diagnostici nel contratto dell'API

Dove: projectForecast.js, campo debug della risposta del ricalcolo globale

Undici contatori di debug fanno parte della risposta, e la pagina Forecast li mostra. Nel rimpatrio sono stati ricostruiti — le regole restituiscono il motivo dell'esclusione e i conteggi si ricavano da lì — invece di reintrodurre le impalcature che li producevano.

Due campi non sono più riproducibili e lo dichiarano nel codice: failedInserts e firstInsertError descrivevano una riga che fallisce mentre le altre proseguono. Con la scrittura in blocco quel caso non esiste: o passa tutto o solleva.

skippedDateRange conta due cose diverse — progetti scartati in blocco e singole assegnazioni fuori periodo. È così anche nell'implementazione precedente, quindi è rimasto così.

Una scrittura che risponde 2xx e non salva — chiuso

Il difetto e costato tre giri: l'email del dipendente, il job level, la citta di una sede. Ogni volta lo stesso sintomo — la richiesta riesce, il dato non c'e — e ogni volta mezz'ora per capirlo.

La causa sta in dynamicRouterGenerator.js: il router di ogni tabella cattura la columnMap in una closure quando viene generato, e le scritture filtrano il corpo su quella mappa. Dopo una migrazione che aggiunge colonne, la mappa in memoria e vecchia — e POST /api/refresh ricarica lo schema ma non rigenera i router gia montati. Lo dice la documentazione di datahub stessa, in fondo alla pagina delle limitazioni. Finche il processo non riparte, la colonna nuova non esiste per chi scrive.

Il rimedio operativo resta lo stesso: dopo una migrazione che tocca le colonne, docker restart del container di datahub, non solo il refresh.

Il rimedio vero e __strict_columns, la controparte in scrittura di __strict_filters. Chi lo dichiara riceve un 400 che nomina le colonne rifiutate, elenca quelle note e dice di riavviare il servizio, invece di un 2xx silenzioso. data-service lo dichiara su ogni create e update.

Chi non lo dichiara non cambia comportamento: i chiamanti esistenti continuano a funzionare come prima.

Costruendolo e emerso un secondo difetto, piu vecchio: il catch di POST e PUT rispondeva 500 a qualunque errore, quindi anche un errore di richiesta sembrava un guasto del server e mandava a cercare nel posto sbagliato. Ora entrambi rispettano lo stato dichiarato dall'errore.