Skip to content

Experience · Backend · Active

cs-portal backend

NestJS BFF for CS Portal, owning HR, administration, document and productivity workflows while delegating shared auth and Salesforce access to Core services.

  • NestJS
  • TypeScript
  • Google APIs
  • Microsoft Graph
  • pdf-lib
  • pdf-parse

Spec sheet

Repository path

cs-portal/backend

Runtime

NestJS BFF

Default port

3003

Public surface

Same-origin /api on portal host

Salesforce access

Through platform-connectors-service

CSRF

Global Origin/Referer guard on mutating /api requests

Responsibilities

  • Expose product APIs for HR, payroll, consumptives, documents, entities, processes and administration.
  • Expose /api/administration/prebilling/* for prebilling forecast list, detail, economics enrichment, project-resource resolution, create, clone, delete and line replacement.
  • Bridge platform-auth-service for shared session and local password login.
  • Use PlatformConnectorsClient and request context propagation for Salesforce-backed flows.
  • Provide structured Salesforce lookup endpoints and DTOs for frontend consumers.

Interfaces and contract surface

  • GET /api/health
  • Same-origin /api/auth/*
  • Same-origin /api/salesforce/lookups/*
  • Same-origin /api/administration/prebilling/*
  • GET /api/administration/prebilling/project-resources
  • Same-origin /api/mail/*
  • Same-origin HR and administration APIs

Consumers

Dependencies and external touchpoints

Notes

  • Raw Salesforce query remains administrative tooling and is disabled by default.
  • Prebilling writes through SalesforceService and platform-connectors-service; the browser never composes SOQL or calls Salesforce directly.
  • Monthly personnel costs and margins are calculated read-only from Montly Cost, Montly Cost Line, Montly Cost Detail and Montly Project Cost records.
  • Recent commits extracted large backend services into runtime services to keep public service files thin.

Source references

  • cs-portal/backend/package.json
  • cs-portal/backend/src/platform/platform-connectors.client.ts
  • cs-portal/backend/src/salesforce/salesforce-lookup.service.ts
  • cs-portal/backend/src/administration-prebilling
  • cs-portal/backend/src/monthly-costs
  • cs-portal/docs/backend-api-map.md

Richieste dipendente di assenza e smart working ​

POST /consumptives/me/request-absence e POST /consumptives/me/request-smart-working accettano le richieste self-service dal browser autenticato. Il BFF richiede almeno un'intera giornata lavorativa tra invio e inizio, escludendo entrambi i giorni e contando lunedì-venerdì; per iniziare martedì la richiesta va inviata entro venerdì. Il giorno corrente è calcolato in Europe/Rome. La malattia identificata dalla commessa MA è esente. Il controllo precede ogni scrittura Salesforce e la creazione dell'evento calendario. Un rifiuto usa HTTP 400 con code INSUFFICIENT_NOTICE e un messaggio localizzato.

API integration ​

Il BFF mantiene la registry delle capability abilitate al canale Bearer. Ogni entry dichiara capability code, versione, classificazione, risorsa ACL e binding a un endpoint canonico /api/...; non esistono handlerRef dinamici o grant ACL locali.

PortalAuthGuard conserva il percorso sessione per il browser e delega il Bearer a platform-auth-service tramite IntegrationBearerGuard. Il resolver ricostruisce per ogni richiesta membership, ACL e data scope correnti e popola PortalPrincipalContext. Un Bearer su una route assente dalla registry viene rifiutato.

Route pubbliche same-origin:

  • POST /api/auth/token delega il token exchange;
  • /api/me/api-credentials/* gestisce PAT human con sessione e CSRF;
  • /api/me/service-accounts/* gestisce service account con owner derivato dalla sessione; GET include il riepilogo della credenziale corrente senza secret;
  • GET /api/integrations/v1/me e GET /api/integrations/v1/capabilities espongono contesto e discovery.

La GUI autenticata /#/settings/integrations compone le sezioni PAT e service account, con conferma per rotazione/revoca e reveal una tantum di secret e client ID. Il browser usa solo le route /api/*: il token exchange resta responsabilità del bridge o del consumer.

La registry abilita gli endpoint HR month-close, il contesto di GET /api/hr/monthly-check?month=YYYY-MM, Commesse e i sei GET di /api/hr/consumptive-management. Il contesto mensile include le risorse attive e i consuntivi del mese (query Salesforce limitate a 5.000 risorse e 10.000 consuntivi); ogni consuntivo espone Customer__c e la relazione Customer__r con Name e VAT__c sotto Project_Resource__r.Project__r. Usa l'ACL rest:hr-monthly-check (Portal HR, Portal Admin). I sei GET di consumptive-management usano capability distinte con path parameter espliciti e ACL rest:hr-consumptive-management (Portal HR, Portal Admin); le quattro POST di share, clear-share, move e status restano session-only. Le capability di scrittura già abilitate mantengono il protocollo preview/execute. CS_PORTAL_INTEGRATION_TOKENS_ENABLED resta disabilitato per default.

Sessione e login-as ​

Il BFF verifica che l'attore sia un PORTAL_ADMIN e che entrambi i Contact siano attivi. Per il login-as chiede a platform-auth-service un token firmato con un contesto cs-portal limitato a quella sessione, mantenendo l'identity e la membership persistente dell'amministratore. I claim del Contact impersonato viaggiano nel contesto del token e non vengono scritti sulla membership di un altro utente. Alla risoluzione il BFF rilegge Contact, permessi e revoca; il revert emette la sessione normale dell'amministratore.

Tassonomia di navigazione ​

GET /navigation espone una tassonomia a tre livelli: app, categorie (groups) e schede (items). La configurazione sotto cs-portal/backend/config/navigation/categories separa label, descrizione, sinonimi di ricerca e collocazione della scheda; una scheda e' grouped con groupId oppure direct con presentation: "direct". Il resolver rifiuta la voce che non dichiara una collocazione valida e il filtro ACL rimuove i gruppi rimasti senza schede accessibili, senza modificare route, ID o permission code esistenti.

Feature amministrative Salesforce ​

Il backend espone superfici BFF dedicate per le feature recuperate da cs-salesforce, senza far transitare LWC, Apex o SOQL nel frontend.

  • GET /hr/qonto-payments filtra opzionalmente i pagamenti Qonto su Montly_Cost_Detail__c per stato pending|paid e mese YYYY/MM; senza stato restituisce entrambi per la vista raggruppata. Riguarda Cedolino, Cedolino - Tredicesima e Acquisto, con ACL rest:qonto-payments (Portal HR, Portal Admin). PUT /hr/qonto-payments/mark-paid rilegge gli ID ancora pending e imposta Payed__c/Date__c; PUT /hr/qonto-payments/rollback-paid rilegge gli ID paid, riporta Payed__c a false e svuota la data. Entrambe le mutazioni usano update bulk Salesforce allOrNone: true; il rollback non annulla un bonifico già eseguito.
  • GET /administration/commesse richiede customerId e espone la vista contabile per anno e, opzionalmente, per mese con costi progetto, fatture e fee gia normalizzati in DTO. L'assenza di month mantiene tutti i mesi dell'anno; i filtri mese e Account sono applicati lato BFF a tutte le fonti e al recap. I dettagli costo vengono interrogati solo per le coppie mese/risorsa ricavate dai costi della commessa del cliente, evitando una lettura annuale globale. L'ACL e rest:administration-commesse (Portal Amministrazione, Portal HR, Portal Admin) e le righe costo escludono i dettagli Cedolino e Cedolino - Tredicesima per non ricontare i netti gia inclusi. La risposta include anche la proiezione canonica clipboard che serializza il formato SFDC legacy senza logica data nel browser.
  • GET /administration/commesse/sheet-tabs?customerId= e POST /administration/commesse/sheet-comparison usano la ACL di lettura. Il primo restituisce i tab GRID del workbook Google Sheets configurato; il secondo riceve solo ID delle righe selezionate, ricarica il dataset Salesforce customer-scoped, legge A:L del tab selezionato e confronta tutte le righe con colonna A=SFDC tramite SHA-256 della tupla canonica. La risposta include syncAction (none|update|create|blocked_ambiguous) e mantiene matched, possible_match, portal_only e sheet_only separati. POST /administration/commesse/sheet-sync/preview e POST /administration/commesse/sheet-sync/execute sono invece governati da rest:administration-commesse-sheet-sync, con gli stessi tre ruoli: ricostruiscono il piano su dati customer-scoped, richiedono fingerprint e conferma testuale, quindi applicano update di A, D, G:L, create A:L o un pair manuale Solo portale → Solo foglio. Il BFF rilegge e verifica sempre le celle dopo il batch; non esistono chiamate Google dal browser, score fuzzy, fallback CSV o retry automatici. Il reader usa spreadsheets.readonly, il writer spreadsheets, e la service account Editor richiede GOOGLE_COMMESSE_SPREADSHEET_ID, GOOGLE_COMMESSE_SHEET_MAX_ROWS e Google Sheets API abilitata.
  • GET /administration/prebilling/forecasts e il dettaglio forecast economico calcolano i costi personale escludendo Cedolino e Cedolino - Tredicesima, per evitare doppio conteggio dei netti gia inclusi nei costi, e mantenendo Acquisto fuori dal costo personale. Il billing usa il mese opzionale Invoice__c.Economic_Competence_Month__c e, quando manca, il mese della data di emissione; una competenza diversa attribuisce il ricavo al mese economico, mantiene un marker nel mese di emissione e non modifica la data contabile. Una riga è bloccata solo se risultano sia una fattura collegata sia un costo personale actual; la sola presenza di una fonte non basta. Le righe bloccate usano economics.actualCostBaseAmount, il costo consuntivo prima del peso riga. Se fattura e mese/commessa identificano una sola riga forecast, il gap residuo è zero e i parametri economici consentono la conversione, il dettaglio restituisce effectiveDays calcolato dall'importo fatturato; la UI mostra quel valore al posto dei giorni pre-fatturazione senza riscrivere il record Salesforce. Se resta un gap o il match è ambiguo, effectiveDays resta nullo. L'editor generico invoice espone entrambi i campi di competenza e invia l'aggiornamento tramite il BFF; FLS e validation rule Salesforce restano autorevoli. Il dettaglio restituisce inoltre una risoluzione canonica dell'allocazione Project_Resource__c per riga: il lock non nasconde un link salvato o un candidato univoco; gli esiti ambigui e assenti restano espliciti.
  • /hr/consumptive-management è un BFF HR resource-centrico: ricerca risorsa, mese di riferimento obbligatorio e riepilogo delle commesse con ore totali, ore Internal non condivise, ore Internal già condivise e ore attribuite. Le ore attribuite sono ricevute dalla commessa destinazione e sono quindi distinte dalle ore della commessa sorgente già condivise. GET /shareable-resources permette, esclusivamente per un mese completo, una selezione preventiva della risorsa aggregando le ore Internal senza Project_Resource_Shared__c; la risposta non espone i consuntivi individuali. I filtri applicati sono ripristinabili con resourceId e month (YYYY-MM) in query string e il BFF risolve nuovamente l'Id risorsa. Le commesse Internal con ore senza Project_Resource_Shared__c sono evidenziate; la scelta della commessa restituisce soltanto le assegnazioni della risorsa nel perimetro e richiede una selezione esplicita quando sono multiple. Per Share e Move, la lista destinazioni esclude sempre l'assegnazione sorgente e le mutazioni rifiutano anche richieste manomesse con lo stesso Id; le assegnazioni diverse espongono riferimento PR-… e tipo per restare distinguibili. Il workbench conserva attribuzione condivisa, rimozione, Move e stato Approved/Denied; tutte le mutazioni validano record e assegnazione sorgente e usano update Salesforce atomico. L'ACL rest:hr-consumptive-management è limitata a Portal HR e Portal Admin; non è pubblicata alcuna route Operations alternativa.
  • POST /hr/costs/import/commit risolve i Contact attivi del CSV e invoca la generazione completa del mese tramite platform-connectors-service prima di cercare le Montly_Cost_Line__c. In questo modo i dettagli costo non dipendono dalla creazione manuale del solo header; le risorse senza una linea generata restano errori per riga e non vengono importate.
  • GET /hr/month-close/preview?period=YYYY-MM e POST /hr/month-close/execute espongono la chiusura mensile a Portal HR e Portal Admin con ACL rest:hr-month-close. La preview legge esclusivamente i consuntivi e le Fee usati dal calcolo storico, ritorna soli conteggi e uno SHA-256 deterministico; execute richiede che l'impronta e la conferma testuale del periodo coincidano con una nuova lettura. Solo dopo tale controllo il BFF invoca l'Apex REST esistente attraverso platform-connectors-service: linee e Fee sono upsert, i costi progetto vengono sostituiti, mentre i dettagli non Fee restano invariati. Il flusso non usa il servizio di import CSV/PDF.

Curriculum HR ​

Il modulo hr-curricula espone upload, lista, detail, lookup Contact/Job, retry, approvazione, rifiuto e retry storage su /hr/curricula. Le route browser richiedono sessione Portal HR o Portal Admin; il callback Agent e una route interna separata, protetta da x-platform-internal-token e con bypass CSRF limitato tramite metadata.

L'intake valida MIME, magic bytes, dimensione e SHA-256 prima di creare il workflow. L'estrazione PDF conserva le pagine e rifiuta file senza testo; il BFF invia soltanto profileKey all'Agent, mai prompt/provider inline. SharePoint e l'unico provider storage ammesso e Salesforce conserva il valore canonico sharepoint piu item id stabile. Il client Documentale mantiene uploadEmployeeDocument, ma instrada typeCode=CV sulla route multipart limitata /documents/hr/contacts/:contactId/curricula; gli altri tipi continuano a usare /documents senza passare dall'orchestratore Curriculum.

La prima execution usa una idempotencyKey deterministica basata sulla chiave esterna del Curriculum e viene creata con dispatch:false. Il BFF persiste execution e snapshot prima di resume; un replay recupera la stessa execution e riconcilia in modo esplicito gli stati QUEUED, RUNNING e terminali senza creare job o callback orfani.

La riconciliazione usa codice fiscale valido come sola chiave forte; l'email univoca e solo un suggerimento. L'approvazione ricalcola le decisioni sullo stato corrente e usa expectedRevision con precondizioni atomiche. Per Contact creati dal workflow, una UUIDv5 deterministica rende idempotenti sia Contact sia Job Member nella stessa Composite Graph.

Il detail supporta la preview non mutante reviewMode=AUTO|CREATE|EXISTING: per un Contact selezionato ricalcola diff e expectedRevision, mentre CREATE produce il confronto contro un Contact vuoto. La precondizione approvata coincide cosi con i valori mostrati all'utente.

La configurazione runtime richiede provider e modello Agent espliciti e, per la creazione di un nuovo Applicant, CS_PORTAL_HR_CV_INTERNAL_ACCOUNT_ID. Il BFF valida che l'Id identifichi davvero un Account di tipo Internal; non seleziona Account tramite query ambigue e non applica fallback.

start:local-stack attende in modo bounded la disponibilita dell'Agent, sincronizza i profili e avvia il BFF soltanto dopo una sync valida. Upload e callback hanno limiti parser espliciti; il callback usa CS_PORTAL_HR_CV_CALLBACK_MAX_JSON_BYTES. L'health generale resta ok, ma espone la capability hrCurricula come degraded quando lo snapshot Agent richiesto non e operativo oppure lo storage SharePoint non e configurato.

Recruiting Job-first ​

Il dominio hr-recruiting riusa i repository Curriculum e il runtime Connector esistente. /api/hr/jobs offre lista, create, metadata e lookup; /:id/workspace compone requisiti, CV primari/secondari e candidature con paginazione indipendente. PATCH Job e Job Member validano campi e picklist Salesforce consentiti, richiedono revisione e usano una precondizione confrontata anche dentro il DML. I conflitti diventano 409.

POST /api/hr/jobs/:id/job-members accetta soltanto un CV approvato e riusa la coppia Contact–Job, senza azzerarne stato o feedback. La nuova candidatura e il suo legame con l'assessment sono atomici. Non sono esposti delete o scollegamenti generici. La vista unassigned richiede assenza di Job primario e conteggio assegnazioni zero, indipendentemente dai filtri di stato. Il bulk rimane una sequenza client-orchestrated di POST /api/hr/curricula; GET /api/hr/curricula/processing-status legge al massimo 50 ID per poll e non espone il contenuto dei CV.

POST/GET /api/hr/curricula/:id/job-fit crea/legge un assessment univoco CV–Job. Il profilo versionato cs-portal.hr-curriculum-job-fit riceve solo i quattro requisiti professionali consentiti e fatti estratti dal CV, non confidenza o dati anagrafici strutturati. Callback interna autenticata, schema rigoroso, fonti verificate, risultato e righe di evidenza atomici: nessun parsing o provider alternativo. L'assessment conserva snapshot, versione, timestamp e segnala requisiti/estrazione cambiati; non modifica il Job Member. NOT_COMPUTABLE evita uno score inventato. Entrambe le callback riusano CS_PORTAL_HR_CV_CALLBACK_MAX_JSON_BYTES. La health espone hrCurricula.jobFit separatamente dalla readiness dell'estrattore.

Il contratto HTTP appartiene a cs-portal/shared/hr-recruiting.d.ts: non e un contratto CORE e non introduce tool MCP o chiamate Salesforce nel browser. Runbook: cs-portal/docs/hr-recruiting.md.

Modulo hr-lifecycle ​

/api/hr/lifecycle espone contesto CV/Contact, ricerca dei referenti BU attivi, preview/execute onboarding e offboarding, consultazione operazioni e retry della sola email fallita per entrambe le azioni. I ruoli ammessi sono Portal HR e Portal Admin. L’onboarding verifica il referente, impedisce autorelazioni e cicli, quindi salva BU__c nella stessa transazione di attivazione. Il permission set tecnico deve avere lettura/modifica su Contact.BU__c. Record Type disponibili e attivi vengono risolti per oggetto e DeveloperName; il client non puo assegnare ruoli o Record Type arbitrari.

Policy, metadata, contesto, piani, audit e notifiche sono moduli separati. Il connector CORE esegue allOrNone con Contact condizionale per primo. I guard Salesforce confrontano la revisione attesa dentro il DML. Idempotenza persistita e revisione della preview proteggono da replay e conflitti; la coda email e l’audit sono nella stessa transazione. Template e destinatari sono gestiti dal servizio email cs-portal: il benvenuto va alla risorsa e l’offboarding va alla risorsa e a HR. Il Flow Salesforce Welcome_on_board viene disattivato. Lo stato email distingue accodata, accettata dal trasporto ed errore, non prova la consegna.

Il manifest Salesforce di preparazione include Administration e HR_Lifecycle_Integration: accesso ai campi HR, ai record di audit e alla coda email, revisioni di mutazione read-only e precondizioni scrivibili. I grant Administration preesistenti sono preservati e confrontati con produzione; il deploy metadata non assegna permission set, non cambia ruoli Contact e non attiva il processo HR.

La feature richiede HR_LIFECYCLE_ENABLED e formula esplicita; la riconciliazione e separata, solo Admin, con finestra HR_RECONCILIATION_UNTIL e hash HR_RECONCILIATION_MANIFEST_SHA256. Il CLI in backend/scripts/hr-lifecycle riusa parser, hash e API BFF: rileva, simula, applica un elemento approvato per Contact e confronta gli esiti. Artefatti personali fuori workspace, directory 0700 e file 0600; nessun export di passcode.

Le API del vecchio onboarding candidato rispondono 410. Pubblicare questo BFF richiede il gate di ritiro dei writer legacy descritto nel runbook, non un deploy ordinario. Ogni richiesta autenticata rilegge stato e permessi del Contact e verifica verifiedIssuedAt contro Portal_Access_Revoked_At__c, anche per integrazioni e impersonazione.

Task tracking ​

Il modulo Nest task-tracking espone API session-only /api/task-boards/* e deriva l'attore dal Contact della sessione Portal; l'input browser non può scegliere l'identità. Ogni accesso legge la membership attiva, mentre il ruolo board decide CRUD task/colonne e condivisione. Il modulo non aggiunge un ACL globale per Tasks. PORTAL_ADMIN può invocare soltanto le due API amministrative di recovery, che restituiscono metadati limitati e non espongono colonne o task.

DatabaseModule inizializza Prisma 7 con @prisma/adapter-pg e richiede DATABASE_URL. Il database CS Portal è dedicato; le migrazioni forward-only sono in backend/prisma/migrations. Lo schema conserva Contact ID Salesforce canonici a 18 caratteri senza FK verso Salesforce e tiene board, membership, colonne, task e audit locali. Board lock e versioni rendono atomiche proprietà, condivisione, progressivo task e riordino. I task hanno isUrgent persistito con default false; l'assegnatario omesso in creazione diventa il Contact attore, mentre null esplicito lascia il task non assegnato. Audit di share registra attore effettivo e impersonator senza loggare payload task.

La directory Contact riusa platform-connectors-service, con query fissa per Contact Portal attivi e soli campi Id/Name. Le route non accettano SOQL, email o cursor Salesforce. Liveness /api/health/live, readiness PostgreSQL /api/health/ready e diagnostica storica /api/health restano segnali distinti; readiness non equivale a monitoraggio continuo. Deploy, private DATABASE_URL, FLS Salesforce e backup/restore sono trattati in cs-portal/docs/task-tracking-operations.md.

Il TaskDto restituisce isUrgent; create e update lo accettano con optimistic concurrency tramite expectedVersion. La lista ammette sort=kanbanPosition, stabile su is_urgent DESC, position ASC, id ASC; il cursore include il gruppo di urgenza. I riordini manuali usano la posizione entro il proprio gruppo e rifiutano anchor di priorità diversa. La scadenza resta una data YYYY-MM-DD; lo stato derivato dal giorno non viene persistito.

Le notifiche task sono catturate nella stessa transazione Prisma delle concessioni e dei cambi ruolo di membership e delle nuove assegnazioni. TaskNotificationWorker reclama l'outbox con FOR UPDATE SKIP LOCKED; la consegna usa i template MailService e la coda Salesforce Email_Queue__c, con Request_Key__c stabile per riconciliare timeout e retry. Il destinatario viene risolto al momento dell'invio; Contact inattivi, email mancanti e HasOptedOutOfEmail sono soppressi. TASK_NOTIFICATIONS_ENABLED è false per default e richiede la configurazione di PORTAL_PUBLIC_URL e dei permessi Salesforce indicati nel runbook operativo.

Workspace reference: /Users/jeanpaul/projects/cs-repository