Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Portale italiano di verifica Partita IVA e consultazione dati camerali. Ricerca una P.IVA, un codice fiscale o una ragione sociale e ottieni una scheda azienda chiara e veloce. Servizio indipendente. Non affiliato a Camere di Commercio, InfoCamere o Unioncamere. I dati provengono da fonti pubbliche. • Next.js 16 (App Router) + TypeScript strict • Tailwind CSS v4 + shadcn/ui (preset radix-nova) + lucide-react • next-themes per il dark mode • zod per validare ogni input e ogni risposta esterna • In arrivo: Drizzle + Neon Postgres, Upstash Redis, @anthropic-ai/sdk檢視完整 README ↗
Portale italiano di verifica Partita IVA e consultazione dati camerali. Ricerca una P.IVA, un codice fiscale o una ragione sociale e ottieni una scheda azienda chiara e veloce.
Servizio indipendente. Non affiliato a Camere di Commercio, InfoCamere o Unioncamere. I dati provengono da fonti pubbliche.
| Step | Contenuto | Stato |
|---|---|---|
| 1 | Scaffold, design token, layout | ✅ fatto |
| 2 | Validazione P.IVA / CF + UI ricerca | ✅ fatto |
| 3 | Provider VIES + /verifica-partita-iva | ✅ fatto |
| 4 | Schema DB, cache, provider camerale | ✅ dati mock; provider reale da collegare |
| 5 | Scheda azienda + SEO | ✅ fatto (manca la mappa statica) |
| 6 | Descrizioni AI asincrone | ⬜ |
| 7 | Rate limiting, pagine legali, test | ✅ fatto (deploy da fare) |
@anthropic-ai/sdkNode.js 22 (vedi .nvmrc). Con nvm:
nvm use
npm install
cp .env.example .env.local
npm run dev
Il sito è su http://localhost:3000, l'health check su /api/health.
npm run dev # server di sviluppo
npm run build # build di produzione
npm run start # avvia la build
npm run lint # ESLint
npm run typecheck # TypeScript senza emettere output
npm run test # unit test (vitest)
npm run test:e2e # test end-to-end (playwright, su build di produzione)
npm run format # Prettier su tutto il progetto
npm run db:generate # genera una migrazione dallo schema Drizzle
npm run db:migrate # applica le migrazioni (richiede DATABASE_URL)
npm run db:studio # esplora il database
npm run buildenpm run devsi contendono la cartella.next: ferma il server di sviluppo prima di lanciare una build di produzione.
Tutti i colori, i raggi e le ombre sono definiti una sola volta in
src/app/globals.css, sia per il tema chiaro (:root)
che per quello scuro (.dark). I componenti non devono mai contenere valori
cromatici hardcodati: usa le utility Tailwind generate dai token
(bg-primary, text-accent, bg-success-subtle, …).
Convenzioni:
--primary — blu profondo istituzionale, azioni principali--accent — teal/salvia, semantica di "verificato"--success / --warning / --danger — solo stati reali (attiva, in
liquidazione, cessata, errori). Mai decorativi..num — attiva i numeri tabulari per P.IVA, REA, capitale socialedata/ contiene tre file JSON committati nel repository, generati a mano
dagli script in scripts/. Il build su Vercel non deve mai dipendere dalla
raggiungibilità di istat.it o di GitHub.
npm run build:ateco # struttura ATECO 2025 + raccordo bidirezionale con la 2022
npm run build:comuni # comuni, province, regioni e CAP
Istat rinomina i file a ogni aggiornamento: se un URL non risponde più, lo script si ferma dicendo quale indirizzo cercare e dove aggiornarlo.
I fornitori di dati camerali restituiscono ancora in larga parte codici ATECO
2022, mentre la classificazione in vigore è la 2025. Cercare un codice
2022 nella tabella 2025 non dà nulla: 62.01.00 non esiste in ATECO 2025,
dove la programmazione informatica è 62.10.00.
La conversione non è uno a uno: 1.048 codici 2022 su 3.157 corrispondono a
più codici 2025. Quando la corrispondenza è ambigua, descriviAteco() risale
al livello gerarchico condiviso invece di scegliere arbitrariamente il primo
risultato, e segnala l'approssimazione con esatta: false.
La cascata completa: struttura 2025 → conversione dal raccordo → troncamento a
un livello superiore → null. Mai una descrizione inventata.
Nel database restano tre campi distinti — codice grezzo, versione della classificazione, descrizione risolta — e il codice del fornitore non viene mai sovrascritto con quello convertito: quando Istat pubblicherà il raccordo successivo, tutto si ricalcola da capo.
data/imprese-sviluppo.json contiene 1.838 imprese con partita IVA
formalmente valida e 127 unità locali, estratte da elenchi pubblici con
scripts/estrai-imprese.py. Servono a provare la resa su dati veri invece
che sui tre esempi inventati.
python3 scripts/estrai-imprese.py <formato> <file> [origine]
Lo script conosce due disposizioni di colonne nei PDF (elenco-imprese,
rete-vendita) e un formato tabellare (tabella, un TSV con intestazione, per
gli elenchi che non arrivano dentro un PDF; le sorgenti stanno in
data/sorgenti/). Unisce i record a quelli già presenti, con la partita IVA
come chiave: righe che condividono la stessa partita diventano unità locali
della stessa impresa. Legge la tabella dalle coordinate del testo nella pagina,
non dall'ordine dei frammenti, perché le celle del PDF vanno a capo e
l'accostamento per vicinanza mescola i campi.
Sono di due nature diverse, e la distinzione è importante.
Delle 838 imprese reali si conoscono solo denominazione, sede e partita
IVA, e solo quelli vengono esposti: attribuire a un'impresa esistente un
codice ATECO, un capitale o un numero REA inventati significherebbe pubblicare
informazioni false su un soggetto reale. I campi mancanti restano null e la
scheda non mostra quelle sezioni — che è anche un buon banco di prova per il
caso «dati scarsi».
Le 1.000 aziende dimostrative non esistono, e proprio per questo hanno
tutti i campi valorizzati: servono a provare la resa completa. Ogni record
deve dichiarare dati_fittizi="SI", altrimenti lo script lo scarta, e la
scheda mostra un avviso in cima, non viene indicizzata e non entra nella
sitemap. Una scheda inventata indistinguibile da una vera sarebbe
un'informazione falsa.
Lo script scarta di proposito ogni colonna che legava quelle imprese al procedimento amministrativo di origine. L'elenco conteneva ditte individuali, cioè persone fisiche: non riaggiungere quelle colonne senza una valutazione legale.
normalizzaComune() in src/lib/geo.ts riconosce un comune scritto in
qualunque modo: maiuscolo, senza accenti, con o senza apostrofi, o nella forma
bilingue con la barra (Bolzano/Bozen). Serve soprattutto agli indirizzi VIES,
che arrivano tutti in maiuscolo: "LARGO FRANCESCO RICHINI 6 \n20122 MILANO MI"
diventa via, CAP, comune e provincia riconosciuti.
La sorgente dei comuni ha due difetti sistematici, entrambi corretti in fase
di build e segnalati a schermo. Il CAP è passato per un tipo numerico e ha
perso lo zero iniziale in 848 comuni su 7904 (Spigno Saturnia risulta 4020
invece di 04020): un CAP italiano ha sempre cinque cifre, quindi si
ricostruisce con certezza.
Le coordinate del centro di ogni comune servono a centrare la mappa statica
nelle schede. Anche qui la sorgente ne sbaglia alcune: undici righe scrivono la
coordinata senza punto decimale (45581 invece di 45.581) e vengono
ricostruite in automatico, perché nessuna coordinata italiana supera 47,2 di
latitudine; per Brescia, collocata 31 km a nord-est del centro città, c'è una
correzione manuale motivata nello script. Al termine nessuna coordinata cade
fuori dai confini d'Italia.
Due limiti dichiarati: il dataset contiene i soli nomi italiani dei comuni,
quindi Bozen da solo non risolve (Bolzano/Bozen sì); e sei nomi sono usati
da più comuni (Samone, Calliano, Livo, Peglio, Castro, Castello), per i quali
senza provincia si restituisce null invece di scegliere a caso.
Oltre alla ricerca c'è un archivio percorribile, che è ciò che rende le schede raggiungibili senza conoscerne la partita IVA:
/aziende regioni
/aziende/lombardia province
/aziende/lombardia/bergamo comuni + anteprima
/aziende/lombardia/bergamo/treviglio aziende del comune
C'è anche l'indice alfabetico, /aziende/lettera/a: le lettere senza aziende
restano visibili ma spente, perché nasconderle farebbe saltare l'alfabeto e
costringerebbe a cercare la lettera invece di puntarla.
Lo stesso vale per i settori:
/attivita divisioni ATECO 2025
/attivita/62-attivita-di-programmazione aziende della divisione
Le due direzioni si incrociano: la pagina di un settore elenca le province in cui quelle aziende hanno sede, e il codice ATECO di una scheda è un collegamento al proprio settore.
Ogni livello ha briciole di navigazione con dati strutturati BreadcrumbList,
e ogni scheda azienda chiude con «Altre aziende a …»: senza collegamenti
interni una scheda è un vicolo cieco.
elenco() e aggrega() sono metodi facoltativi di CompanyProvider. Un
fornitore a pagamento non lascia enumerare il proprio archivio, quindi in
produzione queste pagine andranno costruite sui dati già salvati in Postgres,
non interrogando il fornitore.
src/lib/seo.ts espone una regola sola: finché COMPANY_PROVIDER è mock,
nessuna pagina costruita sui dati aziendali viene indicizzata. Sono dati
dimostrativi, e non devono finire nei motori di ricerca. Collegando un
fornitore reale l'indicizzazione si attiva da sé.
Circa quattro imprese reali su dieci arrivano dagli elenchi pubblici con la sola denominazione e l'indirizzo. La loro pagina è corretta, ma non ha nulla da offrire a chi ci arriva da un motore di ricerca — e molte pagine sottili su un dominio nuovo ne abbassano la reputazione complessiva.
src/lib/scheda.ts conta i campi che distinguono una scheda da un elenco
telefonico (la sede non conta: ce l'hanno tutte). Sotto i tre dati la pagina
si marca noindex, follow: fuori dai risultati, ma i suoi collegamenti verso
comune e settore restano percorribili. La scheda lo dichiara anche al lettore,
perché altrimenti sembrerebbe affermare che quei dati non esistono, mentre
semplicemente non li abbiamo.
La stessa soglia esiste due volte, e devono restare allineate: in JavaScript
per decidere il robots della pagina, e in SQL (datiSostanzialiSql) per
decidere cosa entra nella sitemap. Un test su PGlite verifica che le due
diano lo stesso verdetto — se divergessero, si chiederebbe ai motori di
visitare indirizzi a cui si è appena detto di non indicizzare.
Sull'archivio attuale: 503 schede indicizzabili su 840 reali.
/ricerca è insieme pagina dei risultati ed elenco navigabile: senza query
mostra tutte le aziende in ordine alfabetico, impaginate, con i filtri per
provincia. Ogni scheda dell'elenco è un collegamento all'indirizzo canonico
dell'azienda.
La ricerca per ragione sociale è un metodo facoltativo di
CompanyProvider: chi non la offre semplicemente non lo implementa, e la
pagina lo dice apertamente invece di mostrare zero risultati come se non
esistesse nulla.
Il confronto passa da src/lib/ricerca.ts, che appiattisce accenti e
punteggiatura — le ragioni sociali sono piene di «S.R.L.» e «SOCIETA'
COOPERATIVA» — e richiede che tutte le parole digitate compaiano: chi
cerca due parole non vuole i risultati che ne contengono una sola.
Nota: robots.txt esclude /ricerca dall'indicizzazione. Le pagine di
risultato non hanno contenuto proprio, e tenerle fuori riduce la superficie di
ripubblicazione. Le schede azienda restano indicizzabili.
src/lib/ricerca.ts contiene la normalizzazione e il punteggio, e li usano
entrambe le strade — l'archivio Postgres e il provider in memoria: due
implementazioni con regole diverse darebbero all'utente due risposte diverse
alla stessa domanda.
Due regole meritano di essere spiegate.
Le sigle puntate si ricompongono. "S.P.A." diventa spa, "A.D.R." diventa
adr. Senza questo passaggio la denominazione si riduce a eni s p a, dove
la sequenza spa non compare: chi scrive "eni spa" — cioè la maggioranza, i
punti non li mette quasi nessuno — non troverebbe ENI.
Una parola che apre un nome vale più di una che sta in mezzo a un'altra. Chi cerca "eni" vuole ENI, non THALES ALENIA SPACE. La corrispondenza interna resta valida, perché serve a chi cerca "paolo" dentro "Sanpaolo", ma finisce in fondo. A parità di merito vince il nome più corto: fra "ENI S.P.A." e "ENI GLOBAL ENERGY MARKETS S.P.A." chi ha scritto "eni" cercava la prima.
Sull'archivio il confronto avviene sulla colonna denominazione_ricerca, che
conserva la forma normalizzata. È una colonna e non un calcolo al volo perché
la normalizzazione è codice JavaScript: rifarla in SQL darebbe risultati
diversi. Le righe scritte prima della migrazione 0004 hanno la colonna
vuota e la ricerca ripiega su lower(denominazione) — trova comunque, ma non
riconosce le sigle. npm run db:backfill-ricerca chiude la differenza ed è
idempotente.
src/lib/validation/ contiene i controlli formali, condivisi da client e
server e privi di dipendenze di rete:
partita-iva.ts — normalizzazione (spazi, punteggiatura, prefisso IT) e
cifra di controllo secondo l'algoritmo di Luhn.codice-fiscale.ts — carattere di controllo del CF di persona fisica,
gestione dell'omocodia, e CF di persona giuridica (11 cifre come la P.IVA).query.ts — riconosce da solo se l'utente ha digitato una P.IVA, un codice
fiscale o una ragione sociale, e raccoglie i testi corrispondenti in
QUERY_KIND_TEXT (in italiano l'accordo cambia con il genere: Partita IVA
valida ma codice fiscale valido).La stessa analisi viene rieseguita lato server sulla pagina /ricerca: il
parametro q arriva dall'URL e non ci si può fidare del client.
src/lib/providers/vies.ts interroga il servizio VIES della Commissione
europea, che conferma se una Partita IVA è valida per gli scambi
intracomunitari. È gratuito e non richiede chiavi, ma è lento e spesso
indisponibile: interroga in tempo reale l'anagrafe tributaria dello Stato
membro, che può non rispondere.
Per questo checkVies() non lancia mai. Qualunque problema diventa uno
stato unavailable con un motivo esplicito (TIMEOUT, MS_UNAVAILABLE,
SERVICE_UNAVAILABLE, MS_MAX_CONCURRENT_REQ, GLOBAL_MAX_CONCURRENT_REQ,
NETWORK, UNEXPECTED), e la UI degrada dicendo chiaramente all'utente che
non sa, invece di far passare un'assenza di risposta per un esito negativo.
L'attesa è interrotta dopo 5 secondi (VIES_TIMEOUT_MS).
La chiamata parte dalla Route Handler /api/vies?piva=…: il browser non
contatta mai direttamente la Commissione. Le risposte sono messe in cache per
un'ora, per non gravare su un servizio già fragile.
Nota: VIES riempie denominazione e indirizzo con --- quando lo Stato membro
non li divulga; il provider li normalizza a null e la UI lo spiega.
Ogni impresa ha un solo indirizzo canonico:
/azienda/<denominazione-slug>-<partita-iva>. La Partita IVA in coda è
l'identificatore stabile — la denominazione può cambiare, il numero no — e
uno slug non canonico viene rediretto con un 308 sul canonico, così i motori
di ricerca vedono un URL solo.
Cercare una Partita IVA valida porta direttamente alla scheda: una P.IVA identifica una sola impresa, e mostrare una lista di un elemento sarebbe un passaggio inutile.
Le schede si rigenerano al massimo una volta all'ora (ISR). Ciascuna espone
dati strutturati Organization e un'immagine Open Graph generata al volo.
Il pulsante «Stampa o PDF» usa window.print() con un foglio di stile
dedicato in globals.css: niente dipendenze e niente generazione lato server.
MappaStatica compone le tile di OpenStreetMap in un riquadro fisso: nessuna
libreria, nessun iframe, nessun JavaScript, solo immagini posizionate. Le
coordinate sono quelle del centro del comune, non del numero civico — per
puntare l'indirizzo esatto servirebbe una geocodifica — e la didascalia lo
dichiara invece di lasciarlo intendere.
Le tile pubbliche di openstreetmap.org hanno una politica d'uso che
scoraggia il traffico elevato: prima di mettere il sito sotto carico vero
conviene passare a un fornitore di tile proprio.
src/lib/descrizioni/ scrive la sintesi in prosa che compare come «In
sintesi» sopra i dati. Tre scelte la governano:
Solo su richiesta. Con quasi duemila imprese in archivio, generarle tutte
sarebbe una spesa certa per un beneficio ipotetico: la maggior parte delle
schede non viene mai aperta. La prima visita programma la generazione dentro
after() — la pagina non attende il modello — e dalla seconda in poi il testo
è già in cache (30 giorni) e nella colonna descrizione.
Il modello riceve solo la scheda. fattiDi() costruisce l'elenco dei
fatti, e quell'elenco è l'unico ingresso. Il modello «sa» già chi è Eni, e
quella conoscenza è il rischio: una descrizione che integra a memoria produce
affermazioni su un'impresa reale che nessuno ha verificato. Il prompt lo vieta
come prima regola.
Ogni cifra viene ricontrollata. verificaDescrizione() estrae le sequenze
di cifre del testo generato e le confronta con quelle dei fatti: un importo,
un anno o un conteggio che nei dati non c'è fa scartare l'intero testo, che
non viene né mostrato né salvato. Non si può accertare automaticamente che una
frase sia vera, ma si può accertare che i numeri vengano dai dati — ed è la
classe di invenzione più dannosa.
Senza ANTHROPIC_API_KEY non parte nulla e le schede restano come sono. Le
aziende del dataset dimostrativo sono escluse: hanno dati inventati, e una
descrizione in prosa li renderebbe ancora più simili a informazioni vere.
src/lib/documenti.ts elenca i documenti camerali con i relativi prezzi
indicativi. Il pulsante «Ordina» non avvia un pagamento: apre una finestra
in cui l'utente lascia nome, email e (facoltativi) telefono e note, con il
consenso al trattamento. La richiesta finisce nella tabella
richieste_documenti (POST /api/richieste, validata con zod e soggetta al
limite di richieste) con il nome e il prezzo del documento copiati al momento:
se il listino cambia dopo, la richiesta ricorda cosa aveva visto l'utente.
Chi gestisce il portale legge le richieste, verifica la disponibilità e
risponde per email con il collegamento per procedere all'acquisto; la colonna
stato (ricevuta → risposta → chiusa) si aggiorna a mano. Senza
DATABASE_URL la richiesta non ha dove andare e la finestra lo dice, invece di
fingere di averla salvata.
playwright.config.ts impone COMPANY_PROVIDER=mock e svuota DATABASE_URL
al server dei test. Senza, ogni esecuzione della suite farebbe decine di
chiamate a pagamento e dipenderebbe da dati che cambiano.
e2e/accessibilita.spec.ts passa axe su sette pagine, in tema chiaro e
scuro, su desktop e su mobile: 22 controlli, nessuna violazione WCAG 2.1
A/AA. Il tema scuro si verifica a parte perché ridefinisce tutti i token di
colore — altrimenti si scopre tardi che una metà è a norma e l'altra no.
axe non certifica la conformità: molti criteri richiedono un giudizio umano, e uno strumento automatico ne copre circa un terzo. Coglie però in modo affidabile la classe di errori che si introduce senza accorgersene — contrasti insufficienti, campi senza etichetta, gerarchie di titoli saltate.
Due prove sono di tastiera, dove axe non arriva: il primo Tab deve offrire
«Vai al contenuto» visibile, e dopo il salto la tabulazione deve ripartire dal
contenuto e non dalla testata. <main> ha tabIndex={-1} perché il salto
funzioni anche con un lettore di schermo: Chrome e Firefox spostano comunque
il punto di partenza della tabulazione, ma il cursore di lettura si muove solo
se l'elemento può ricevere il fuoco.
next.config.ts le applica a ogni risposta: CSP, X-Content-Type-Options,
X-Frame-Options, Referrer-Policy, Permissions-Policy e HSTS. Un test E2E
le verifica sulla build di produzione, perché una regressione qui non si vede
guardando il sito.
La CSP ammette 'unsafe-inline' negli script, e la scelta va dichiarata: Next
inserisce script inline per l'idratazione e la scheda azienda ne aggiunge uno
con i dati strutturati JSON-LD. L'alternativa pulita è una CSP con nonce, che
richiede un middleware che lo generi a ogni richiesta — rendendo dinamica ogni
pagina e buttando via l'ISR su cui il sito si regge. Meglio un compromesso
dichiarato che una CSP severa disattivata al primo errore.
Il resto è stretto: nessun frame, nessun plugin, niente form verso l'esterno,
e come sola origine esterna per le immagini le tile di OpenStreetMap.
'unsafe-eval' esiste solo in sviluppo, dove il compilatore ne ha bisogno.
Dieci ricerche al minuto e cento al giorno per indirizzo IP, applicate alle
Route Handler in src/lib/rate-limit/. Con Upstash configurato il conteggio è
condiviso fra le istanze; senza, si ripiega su un contatore in memoria che
vale solo per il singolo processo — in sviluppo va bene, in produzione non
protegge davvero.
Se Redis è irraggiungibile il limite lascia passare: un contatore rotto non
deve trasformarsi in un blocco totale del servizio. Le risposte portano le
intestazioni RateLimit-* e, quando bloccano, un Retry-After e un messaggio
che dice fra quanto riprovare.
Informativa privacy, termini, cookie e chi siamo sono scritte, non più
segnaposto. Non sono però definitive: i dati identificativi del titolare
del trattamento vivono in src/lib/site-config.ts e sono ancora da compilare.
Finché restano tali, ogni pagina legale mostra un avviso ben visibile che si
spegne da solo appena i campi vengono riempiti.
I testi non sono stati esaminati da un legale. Vanno fatti rivedere prima di pubblicare, in particolare la base giuridica dichiarata (legittimo interesse) e la procedura di rettifica per le imprese individuali.
Le interrogazioni ai provider camerali si pagano a chiamata, quindi la regola
è: non ricomprare un dato che si ha già. getCompany() in
src/lib/companies/repository.ts segue
sempre lo stesso ordine:
REFRESH_AFTER_DAYS (default 30);?refresh=1).Se il provider non risponde ma in archivio c'è un record vecchio, viene
mostrato quello con source: "database-stale" e la data di scarico: un dato
datato e dichiarato tale è più utile di una pagina d'errore. Al contrario, se
il provider risponde "non esiste", quella risposta è autorevole e non si
ripiega sull'archivio.
Ogni chiamata al fornitore finisce nella tabella api_calls con durata, esito
e costo. Il costo vale 0 finché non si dichiara il listino reale in
OPENAPI_COSTO_PER_CHIAMATA: il numero di chiamate è un dato certo, l'importo
no, e una cifra inventata nella colonna dei costi produce un totale che sembra
una spesa senza esserlo.
Il portale funziona anche senza database e senza Redis: degrada a cache in memoria e paga ogni interrogazione. Utile in sviluppo, da non fare in produzione.
search_logIl piano iniziale prevedeva anche una tabella con le ricerche degli utenti. Non è stata creata: registrare le query è un trattamento di dati personali che va dichiarato nell'informativa e difeso, e al momento non serve a nessuna funzionalità. Se in futuro servirà per le statistiche, meglio introdurla consapevolmente e in forma aggregata.
I provider vivranno in src/lib/providers/ e implementeranno tutti la stessa
interfaccia CompanyProvider, così la UI non cambia mai al cambiare del
fornitore:
src/lib/providers/<nome>.ts con una classe che implementa
CompanyProvider (vedi cerved.ts come scheletro);CompanyData;COMPANY_PROVIDER in src/lib/env.ts e un ramo
alla factory in src/lib/providers/index.ts;.env.example e nello schema zod;COMPANY_PROVIDER.Regole a cui ogni provider deve attenersi:
{ status: "unavailable", reason }, e l'indisponibilità va tenuta distinta da not-found;UNEXPECTED rumoroso che una scheda plausibile ma sbagliata;costPerLookupEur: è ciò che finisce in api_calls.Verificato su risposte reali dei livelli IT-start e IT-advanced: le
fixture in src/lib/providers/__fixtures__/ sono risposte vere, catturate
dall'API, e i test girano su quelle.
L'autenticazione è a due passaggi, cosa che la documentazione non rende evidente: la API key non si usa direttamente sull'endpoint dei dati, va scambiata con un token con gli scope richiesti.
curl -u 'EMAIL:APIKEY' -H 'Content-Type: application/json' \
-X POST https://oauth.openapi.it/token \
-d '{"scopes":["GET:company.openapi.com/IT-advanced"],"ttl":31536000}'
Il token che torna va in OPENAPI_IT_TOKEN. Serve anche che la Company API
sia attiva sul conto: finché non lo è, la richiesta di token risponde
invalid scopes specified or API not enabled qualunque scope si chieda.
Due cose che si vedono solo guardando una risposta vera, e che avevo sbagliato ipotizzandole dalla documentazione:
streetName contiene già l'indirizzo completo, non il solo nome della
via: ricomporlo con toponimo e civico lo duplica;balanceSheets.last.In più l'API fornisce cose che il modello non prevedeva: coordinate GPS della sede (che rendono la mappa precisa al civico invece che al comune), il codice destinatario SDI, e la serie storica dei bilanci.
Il costo per chiamata non è scritto nel codice: si dichiara in
OPENAPI_COSTO_PER_CHIAMATA e vale 0 finché non si conosce il listino.
Un'API a pagamento non lascia enumerare il proprio contenuto: si può chiedere una partita IVA alla volta, non «tutte le imprese di Bergamo».
Per questo ricerca ed elenchi si costruiscono sull'archivio
(src/lib/companies/archivio.ts), non sul fornitore: crescono con le aziende
che qualcuno ha già cercato, e ogni scheda aperta le allarga. Senza
DATABASE_URL si ripiega sul provider, che sa farlo solo in quello
dimostrativo — e le pagine lo dichiarano invece di mostrare zero risultati
come se non esistesse nulla.
.env.example per Production e Preview.npm run db:migrate).I termini di quasi tutti i provider di dati camerali limitano la ripubblicazione dei dati su un sito pubblico indicizzabile. Verifica il contratto del provider prima di attivare la cache permanente, la sitemap dinamica e il JSON-LD sulle schede azienda. VIES non ha questa restrizione.