In produzione · B2B SaaS · Procurement Pharma
Haelias
- Ruolo
- Solo CTO / Principal Engineer (con contributor support su UI)
- Periodo
- 2022 – present
- Clienti
- Montefarmaco · Logista SPA

Numeri chiave
- 5.5M+Documenti MongoDB live
- ~14kProdotti in stock cross-supplier
- 143Endpoint Express (singolo `main.ts`)
- 493Test case backend (Jest + supertest)
01 · Haelias
Problema
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.
02 · Haelias
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.
03 · Haelias
Decisioni chiave
- 01
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.
- 02
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.
- 03
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.
- 04
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.
- 05
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`.
- 06
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.
- 07
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.
04 · Haelias
Risultati
- 5.5M+
Documenti MongoDB live
- ~14k
Prodotti in stock cross-supplier
- 143
Endpoint Express (singolo `main.ts`)
- 493
Test case backend (Jest + supertest)
- 68%
Statement coverage backend
- 16
Schemi Mongoose
- 136
Componenti React (Atomic Design)
- 5
Repo git indipendenti
- 70% (1.69 GB → 369 MB)
Riduzione size indici on-disk (live)
- ~99%
Riduzione indici in cache (search path)
- €1,260/yr
Risparmio annuale vs upgrade cluster
- 50k rows = 1 bulkWrite
Benchmark CSV ingest
05 · Haelias
Tech e pilastri
Tech stack
- Node.js
- Express 4
- TypeScript
- MongoDB
- Mongoose
- MongoDB driver (native)
- Helmet
- Web Push
- Resend
- @react-email/components
- PDFKit
- ExcelJS
- Turf.js
- React 19
- React Router v7
- Tailwind 3
- MUI 7
- MUI X DataGrid
- framer-motion
- Chart.js
- Recharts
- react-window
- CRA + craco
- Vite (landingpage only)
- Rust 2021
- Tokio
- reqwest
- mongodb (Rust driver)
- rustls
- Jest
- supertest
- DigitalOcean App Platform
- Managed MongoDB
- Vercel
- Discord Bot API
- Server-Sent Events
- Farmadati SOAP
Pillar toccati
- Traditional Software Engineering
- Cloud & DevOps
- IT Management & Digitalizzazione
Contatto
Hai un sistema simile da costruire?
Raccontami il contesto in una call di 30 minuti.
hello@andrea0x.me · @andrea0x_eth