Haelias
Solo CTO / Principal Engineer (con contributor support su UI) · 2022 – present
Clienti: Montefarmaco, Logista SPA
Visita il sitoProblema
Le farmacie italiane gestiscono il procurement con telefonate, email e fax verso più grossisti, contrattando i prezzi a voce e riconciliando manualmente bolle e fatture. Servono catalog real-time multi-fornitore, ODA digitali, audit trail compliance-grade e UX safety per ordini high-value.
Architettura
Stack production end-to-end su 5 repo git indipendenti (core / ui / sync-worker / landingpage / haelias_docs): backend Express + TypeScript con 143 endpoint montati da un singolo `main.ts`, SPA React 19 + Tailwind 3 + MUI 7 con 136 componenti in Atomic Design e 5 React Context (no Redux), landing in Vite, worker Rust + Tokio per l'ingestion dal catalogo SOAP di Farmadati su MongoDB Managed condiviso. Il data layer è composto da 16 schemi Mongoose in modello multi-tenant (5 ruoli RBAC: admin / manager / buyer / supplier / referral_partner), un search engine custom su indici parziali pesati e una strategia indici a due livelli che confina gli indici di Discover/ricerca ai ~14k prodotti in stock invece dei 5,5M totali. Il dominio ODA è una state machine a 6 stati con sub-ordini cascading (`createSubOda`) e una state machine counter-offer per linea a 6 stati (`requested → admin_absorbed | sent_to_supplier → supplier_responded → resolved`), più un layer `stripSupplierDataForBuyer` invocato su ogni read lato buyer. Il customer support è un bridge Discord ↔ SSE bidirezionale: messaggi pollati via REST da Discord vengono pushati nella UI in tempo reale. 493 test case backend (Jest + supertest), 68% statement coverage. La UI principale è CRA + craco (non Next.js); Vite alimenta solo la landingpage.
Decisioni chiave
Cluster upgrade vs ottimizzazione indici
Diagnosi via WiredTiger cache-pressure analysis: dei 5,5M documenti prodotti, solo ~14k portano supply attiva. Indici sostituiti con indici parziali filtrati su `{hasSupply: true, status: "active"}` (name, producer, category) più un indice testuale partial pesato multi-campo (name:10, sku:8, ean:8, producer:5, category:3, description:1). Risultato: ~99% di riduzione del footprint in cache sui search path, 70% di riduzione on-disk live (1.69 GB → 369 MB), cluster $15/mese mantenuto, upgrade da €1.260/anno evitato.
Worker Farmadati in Rust + Tokio
Sync continuo SOAP-to-Mongo con concorrenza per tabella e per pagina (`FARMADATI_PAGE_CONCURRENCY`) e bulk write. Lock + run-history collections (`farmadati_sync_states`, `farmadati_sync_runs`) e comando `resume` heartbeat-based per crash recovery. Modalità CLI: `delta` / `full` / `auto` / `resume` / `snapshot` / `reset-lock`. Rust con `$set`-only è intenzionale: il flag `hasSupply` denormalizzato dal backend sopravvive a ogni upsert Farmadati perché il worker tocca solo i propri campi.
Privacy-by-design tra supplier e buyer
`stripSupplierDataForBuyer` (`core/routes/orders.ts:182`) è il sanitiser unico chiamato su ogni read lato buyer: rimuove `warehouseAssignments`, droppa `warehouse`/`entityId` dai breakdown di linea, e gating della visibilità `priceNegotiation` in base allo status dell'ordine. La state machine counter-offer ha 6 stati + 3 risoluzioni (`buyer_price_accepted` / `supplier_price_accepted` / `negotiation_failed`) con `originalNetPrice` e `absorbedNetPrice` rigorosamente separati.
Modal warning per ordini high-value
Hard block scartato in favore di un warning configurabile per ordini €250k–€1M+ — sicurezza senza frustrare i buyer enterprise.
Sub-ODA cascading per fulfilment parziale
Quando un magazzino fulfilla solo parzialmente una linea, l'admin emette un sub-ODA (`createSubOda`, `routes/orders.ts:6350`) collegato via `parent.subOrderIds[]` e `subOrderId: "ODA-SUB-..."`. Il buyer vede un singolo ODA; il flusso warehouse riceve un nuovo ODA per ogni round di fulfilment. Coperto dai test in `tests/orders.test.ts:3570-3645`.
Bridge customer support Discord ↔ SSE
Customer support real-time senza Intercom: REST-polling di un canale Discord ogni 10s (`libs/support-discord.ts`), persistenza in `support-conversations` + `support-messages`, push verso UI via Server-Sent Events (`libs/support-stream.ts`). Gestisce 429-retry, filtering del bot user e subscription per conversazione.
bulkWrite singolo per CSV da 50k righe
Gli upload CSV (`csv-upload.ts`) e la maintenance del flag `hasSupply` (`hasSupplyMaintenance.ts`) emettono un singolo MongoDB bulkWrite per upload indipendentemente dalla size — anche un CSV da 50k righe diventa una sola operazione, con progress streamato in UI tramite l'utility upload-progress.