Pe scurt
- Timeout scurt pe apifirme (aici 3 s) și un buget de timp pentru toată căutarea (5 s), din care trăiește și rezerva.
- Treci pe openapi.ro la timeout, conexiune căzută și 5xx, inclusiv 503
source-unavailable; la 202 și 429 doar dacăRetry-Afternu încape în buget. Niciodată la 400 sau 404. - Ambele răspunsuri ajung în aceeași structură, cu
sourcemarcat și cunullpentru ce openapi.ro nu spune: e-Factura, inactivitate, plata defalcată, CAEN.
Operatorul scrie CUI-ul clientului nou, apasă Tab și așteaptă denumirea. Dacă cererea către furnizorul de date stă agățată, el vede un formular care nu se mișcă și după zece secunde completează de mână, cu greșeli. Orice API extern are minute proaste, iar pentru o aplicație care nu poate factura fără datele clientului merită un al doilea drum: openapi.ro, alt furnizor românesc de date despre firme.
Rezerva e pentru „nu știu acum”, nu pentru „nu”
La fiecare cod de răspuns întrebi: ar putea alt furnizor să știe ceva în plus? Dacă apifirme nu a răspuns sau a spus că nu poate acum, da. Dacă a spus că firma nu există, nu: un CUI pe care ANAF nu îl cunoaște nu devine cunoscut pentru că întrebi în altă parte. Pe codurile din documentație:
| Răspuns apifirme | Ce înseamnă | Ce faci |
|---|---|---|
| timeout, conexiune căzută | niciun răspuns | openapi.ro |
500, 502, 504 |
eroare la furnizor sau pe drum | openapi.ro |
503 source-unavailable |
firma nu e încă în bază și ANAF nu poate fi întrebat acum | openapi.ro |
202, 429 |
firma e căutată la ANAF; limita pe minut sau pe lună | aștepți Retry-After o dată, dacă încape în buget; altfel openapi.ro |
404 |
CUI neînregistrat sau persoană fizică | răspuns final |
400 |
CUI invalid | îl prinzi înainte, local |
401, 403 |
cheia sau planul | eroare de configurare, vizibilă |
404 vine și pentru PFA, II și IF (company-not-available): apifirme nu servește date despre persoane fizice, iar aplicația ta are oricum nevoie de un drum pentru ele, de obicei completarea de mână. 401 „rezolvat” cu rezerva înseamnă că afli de cheia greșită peste o lună, din loguri.
202 și 429 vin cu Retry-After și nu se numără ca cereri; diferența e cine așteaptă. Un job de noapte poate dormi 12 secunde, un formular nu. Codul așteaptă deci o singură dată și doar dacă pauza încape în bugetul cererii: cu 5 s, un Retry-After: 12 înseamnă openapi.ro; cu 60 s, o pauză.
Timeout-ul îl alegi după răbdarea omului din fața ecranului, minus ce îi trebuie rezervei. Când ANAF întârzie, apifirme răspunde cu 202 în loc să țină cererea deschisă, deci timeout-ul nu trebuie să acopere și căutarea la ANAF. Aici: 1,5 s pentru conectare, 3 s pentru răspuns, 5 s pentru tot.
Același CUI, două răspunsuri
Pe aceeași firmă de exemplu, cele două API-uri spun aproape aceleași lucruri, cu alte nume și tipuri:
$ curl -s -H "Authorization: Bearer $APIFIRME_KEY" "$APIFIRME_BASE/companies/90000057" \
| jq '{judet, scp_tva, tva_incasare, split_tva, status_inactiv, e_factura}'
{
"judet": "IF",
"scp_tva": true,
"tva_incasare": true,
"split_tva": false,
"status_inactiv": false,
"e_factura": true
}
$ curl -s -H "x-api-key: $OPENAPI_RO_KEY" "$OPENAPI_RO_BASE/companies/90000057" \
| jq '{judet, tva, tva_la_incasare, radiata, stare}'
{
"judet": "Ilfov",
"tva": "2012-06-24",
"tva_la_incasare": [
{
"tip": "I",
"de_la": "2026-09-01",
"pana_la": null,
…
}
],
"radiata": false,
"stare": "INREGISTRAT din data 25.03.2012"
}
- Cheia merge în
x-api-key;cife text, nu număr. judete numele („Ilfov”), nu codul (IF). Adaptorul îl traduce, ca să nu ai două feluri de județe în aceeași coloană.tvae data înregistrării în scopuri de TVA saunull; din el ieseplatitor_tva.tva_la_incasaree o listă de perioade, nu da/nu. Am considerat că firma aplică azi TVA la încasare dacă are o perioadă de tipIcare cuprinde ziua de azi; verifică interpretarea pe câteva firme cunoscute.- 202 are în corp
retry_after, un moment ISO 8601, nu secunde. - 404 vine și pentru un CIF invalid, deci cifra de control se verifică înainte, local; altfel o greșeală de tastare arată ca o firmă inexistentă.
Fără echivalent clar în răspunsul lor rămân e_factura, status_inactiv, split_tva, caen (există caen_code la bilanțuri, nu la firmă), euid, forma_juridica_cod și sources. În structura comună ele devin null, adică „necunoscut”. Nu false: un false spune „am verificat și nu e inactivă”, iar asta nu a verificat nimeni.
Python: trei ieșiri posibile
Cu requests. firma() se termină în trei feluri: o structură cu source, FirmaNegasita (un răspuns, nu o pană) sau FirmaIndisponibila (niciun furnizor nu a răspuns la timp). 400, 401 și 403 ies ca excepții obișnuite, ca să le vadă cineva.
"""Datele unei firme de la apifirme, cu openapi.ro ca rezervă."""
import json, os, re, sys, time, unicodedata
from datetime import date, datetime, timezone
import requests
APIFIRME_BASE = os.environ.get("APIFIRME_BASE", "https://apifirme.dev/rest/v1")
OPENAPI_RO_BASE = os.environ.get("OPENAPI_RO_BASE", "https://api.openapi.ro/api")
class CuiInvalid(Exception): pass
class FirmaNegasita(Exception): pass # 404: un răspuns, nu o pană
class FirmaIndisponibila(Exception): pass # niciun furnizor nu a răspuns la timp
class _Rezerva(Exception): pass # apifirme a eșuat; motivul ajunge în log
def cui_valid(text):
s = str(text).strip().upper().removeprefix("RO").strip()
if not re.fullmatch(r"[0-9]{2,10}", s):
return None
suma = sum(int(c) * int(p) for c, p in zip(s[:-1].rjust(9, "0"), "753217532"))
return int(s) if suma * 10 % 11 % 10 == int(s[-1]) else None
def ramas(limita):
return limita - time.monotonic()
def firma(text, buget=5.0):
cui = cui_valid(text)
if cui is None:
raise CuiInvalid(text) # nu trimitem gunoi la niciunul dintre furnizori
limita = time.monotonic() + buget
try:
return din_apifirme(cui, limita)
except _Rezerva as e:
print(f"apifirme: {e}, după {buget - ramas(limita):.1f} s; întreb openapi.ro",
file=sys.stderr)
return din_openapi_ro(cui, limita)
def din_apifirme(cui, limita):
cheie = {"Authorization": f"Bearer {os.environ['APIFIRME_KEY']}"}
for incercare in (1, 2):
try:
r = requests.get(f"{APIFIRME_BASE}/companies/{cui}", headers=cheie,
timeout=(1.5, max(0.1, min(3.0, ramas(limita) - 1))))
except requests.Timeout:
raise _Rezerva("timeout")
except requests.ConnectionError as e:
raise _Rezerva(f"conexiune căzută: {type(e).__name__}")
if r.status_code == 200:
return normalizeaza_apifirme(r.json())
if r.status_code == 404:
raise FirmaNegasita(tip_problema(r))
if r.status_code in (202, 429):
pauza = float(r.headers.get("Retry-After", 2))
if incercare == 1 and pauza + 1.5 < ramas(limita):
time.sleep(pauza)
continue
raise _Rezerva(f"{r.status_code}, Retry-After {pauza:g} s nu încape în buget")
if r.status_code >= 500:
raise _Rezerva(f"{r.status_code} {tip_problema(r)}".strip())
r.raise_for_status() # 400, 401, 403: greșeala e la noi; rezerva ar ascunde-o
raise _Rezerva("tot 202 după o pauză")
def tip_problema(r):
"""Ultima parte din `type` (RFC 7807), de exemplu company-not-found."""
try:
return r.json()["type"].rsplit("/", 1)[-1]
except (ValueError, KeyError, TypeError, AttributeError):
return ""
def din_openapi_ro(cui, limita):
for incercare in (1, 2):
if ramas(limita) < 0.5:
raise FirmaIndisponibila("s-a terminat bugetul de timp")
try:
r = requests.get(f"{OPENAPI_RO_BASE}/companies/{cui}",
headers={"x-api-key": os.environ["OPENAPI_RO_KEY"],
"accept": "application/json"},
timeout=(1.5, ramas(limita)))
except requests.RequestException as e:
raise FirmaIndisponibila(f"nici openapi.ro: {type(e).__name__}") from e
if r.status_code == 200:
return normalizeaza_openapi_ro(r.json())
if r.status_code == 404:
raise FirmaNegasita("openapi.ro: 404")
if r.status_code == 202:
# retry_after este un moment ISO 8601, nu un număr de secunde; marja
# acoperă ceasurile care nu bat la fel la tine și la ei
text = r.json()["retry_after"]
moment = datetime.fromisoformat(text.replace("Z", "+00:00"))
pauza = (moment - datetime.now(timezone.utc)).total_seconds() + 0.25
pauza = max(0.5, pauza)
if incercare == 1 and pauza + 1 < ramas(limita):
time.sleep(pauza)
continue
raise FirmaIndisponibila("openapi.ro: firma e încă în coada lor")
raise FirmaIndisponibila(f"nici openapi.ro: HTTP {r.status_code}")
raise FirmaIndisponibila("openapi.ro: tot 202 după o pauză")
def normalizeaza_apifirme(d):
return {
"cui": d["cui"], "denumire": d["denumire"], "nr_reg_com": d["nr_reg_com"],
"adresa": d["adresa_completa"], "judet": d["judet"],
"cod_postal": d["cod_postal"],
"platitor_tva": d["scp_tva"], "tva_la_incasare": d["tva_incasare"],
"split_tva": d["split_tva"], "inactiv": d["status_inactiv"],
"radiata": None if d["stare"] is None else d["stare"] == "RADIERE",
"e_factura": d["e_factura"], "caen": d["caen"], "source": "apifirme",
}
def normalizeaza_openapi_ro(d):
return {
"cui": int(d["cif"]), "denumire": d["denumire"], "nr_reg_com": d["numar_reg_com"],
"adresa": d["adresa"], "judet": cod_judet(d["judet"]),
"cod_postal": d["cod_postal"],
"platitor_tva": d["tva"] is not None, # tva = data înregistrării sau null
"tva_la_incasare": la_incasare_azi(d.get("tva_la_incasare")),
# fără echivalent în răspunsul lor: necunoscut, nu „nu”
"split_tva": None, "inactiv": None, "e_factura": None, "caen": None,
"radiata": d["radiata"], "source": "openapi.ro",
}
def la_incasare_azi(perioade):
if perioade is None:
return None
azi = date.today().isoformat()
return any(p.get("tip") == "I" and p["de_la"] <= azi <= (p.get("pana_la") or "9999")
for p in perioade)
JUDETE = dict(x.split(" ", 1)[::-1] for x in (
"AB ALBA|AR ARAD|AG ARGES|BC BACAU|BH BIHOR|BN BISTRITA-NASAUD|BT BOTOSANI|BR BRAILA|"
"BV BRASOV|BZ BUZAU|CL CALARASI|CS CARAS-SEVERIN|CJ CLUJ|CT CONSTANTA|CV COVASNA|"
"DB DAMBOVITA|DJ DOLJ|GL GALATI|GR GIURGIU|GJ GORJ|HR HARGHITA|HD HUNEDOARA|"
"IL IALOMITA|IS IASI|IF ILFOV|MM MARAMURES|MH MEHEDINTI|MS MURES|NT NEAMT|OT OLT|"
"PH PRAHOVA|SJ SALAJ|SM SATU MARE|SB SIBIU|SV SUCEAVA|TR TELEORMAN|TM TIMIS|"
"TL TULCEA|VL VALCEA|VS VASLUI|VN VRANCEA|B BUCURESTI").split("|"))
def cod_judet(nume):
"""openapi.ro dă numele județului („Sibiu”), apifirme codul („SB”)."""
if not nume:
return None
n = unicodedata.normalize("NFKD", nume).encode("ascii", "ignore").decode().upper()
return JUDETE.get(n.removeprefix("MUNICIPIUL ").strip())
if __name__ == "__main__":
try:
buget = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0
print(json.dumps(firma(sys.argv[1], buget), ensure_ascii=False, indent=2))
except (CuiInvalid, FirmaNegasita, FirmaIndisponibila) as e:
sys.exit(f"{type(e).__name__}: {e}")
Timeout-ul de citire din requests se aplică fiecărei citiri de pe socket, nu întregii cereri; pentru un formular ajunge. Validarea locală ține gunoiul departe de ambii furnizori: /validate/cui face același lucru fără cheie, dar e tot o cerere la apifirme, deci nu ajută tocmai când apifirme nu răspunde. Marja de 0,25 s la retry_after vine din test: dormind exact până la momentul primit, prima versiune ajungea uneori cu câteva milisecunde prea devreme și primea încă un 202.
Am rulat codul pe un server local care imită ambele API-uri și se comută din mers ($MOCK; firmele „EXEMPLU … SRL” sunt date de exemplu). Fiecare fel în care cade apifirme duce la openapi.ro:
$ for s in slow down 503 429; do
> curl -s -X POST "$MOCK/_mock/scenario?apifirme=$s&openapi=ok" > /dev/null
> python3 firma.py 90000057 2>&1 | grep -E '^apifirme|"source"'
> done
apifirme: timeout, după 3.0 s; întreb openapi.ro
"source": "openapi.ro"
apifirme: conexiune căzută: ConnectionError, după 0.0 s; întreb openapi.ro
"source": "openapi.ro"
apifirme: 503 source-unavailable, după 0.0 s; întreb openapi.ro
"source": "openapi.ro"
apifirme: 429, Retry-After 12 s nu încape în buget, după 0.0 s; întreb openapi.ro
"source": "openapi.ro"
Cazurile în care rezerva nu are voie să pornească le-am rulat cu openapi.ro pus pe „căzut”: dacă programul l-ar fi întrebat, rezultatul ar fi fost FirmaIndisponibila.
$ curl -s -X POST "$MOCK/_mock/scenario?apifirme=ok&openapi=down" > /dev/null
$ python3 firma.py 90000030
FirmaNegasita: company-not-available
$ python3 firma.py 90000049
FirmaNegasita: company-not-found
$ python3 firma.py 90000058
CuiInvalid: 90000058
La 202 decide bugetul. Firma 90000081 e „căutată la ANAF” la prima cerere, cu Retry-After: 2: cu 5 s de buget programul așteaptă și primește răspunsul de la apifirme, cu 3 s trece pe rezervă:
$ curl -s -X POST "$MOCK/_mock/scenario?apifirme=pending&openapi=ok" > /dev/null
$ time python3 firma.py 90000081 | grep source
"source": "apifirme"
real 0m2,070s
$ curl -s -X POST "$MOCK/_mock/scenario?apifirme=pending&openapi=ok" > /dev/null
$ python3 firma.py 90000081 3 | grep source
apifirme: 202, Retry-After 2 s nu încape în buget, după 0.0 s; întreb openapi.ro
"source": "openapi.ro"
Când nu răspunde niciunul (ambele servere de test puse pe „lent”), FirmaIndisponibila vine după 5,07 s, la capătul bugetului, nu după două timeout-uri lungi puse cap la cap.
TypeScript: același contract, cu fetch
Node 18 sau mai nou, fără dependențe; aceeași structură, aceleași excepții.
// firma.ts — apifirme, cu openapi.ro ca rezervă. Node 18+, fără dependențe.
const APIFIRME_BASE = process.env.APIFIRME_BASE ?? "https://apifirme.dev/rest/v1";
const OPENAPI_RO_BASE = process.env.OPENAPI_RO_BASE ?? "https://api.openapi.ro/api";
type DaNu = boolean | null; // null = necunoscut, nu „nu”
export interface Firma {
cui: number; denumire: string | null; nr_reg_com: string | null;
adresa: string | null; judet: string | null; cod_postal: string | null;
platitor_tva: DaNu; tva_la_incasare: DaNu; split_tva: DaNu;
inactiv: DaNu; radiata: DaNu; e_factura: DaNu; caen: number | null;
source: "apifirme" | "openapi.ro";
}
export class CuiInvalid extends Error {}
export class FirmaNegasita extends Error {}
export class FirmaIndisponibila extends Error {}
class Rezerva extends Error {}
const sleep = (s: number) => new Promise((ok) => setTimeout(ok, s * 1000));
const ramas = (limita: number) => (limita - Date.now()) / 1000;
export function cuiValid(text: string): number | null {
const s = text.trim().toUpperCase().replace(/^RO\s*/, "");
if (!/^[0-9]{2,10}$/.test(s)) return null;
const corp = s.slice(0, -1).padStart(9, "0");
const suma = [...corp].reduce((acc, c, i) => acc + +c * +"753217532"[i], 0);
return ((suma * 10) % 11) % 10 === +s.slice(-1) ? +s : null;
}
async function cerere(url: string, headers: Record<string, string>, sec: number) {
try {
// timeout() vrea milisecunde întregi și taie toată cererea, inclusiv corpul
const ms = Math.max(100, Math.round(sec * 1000));
return await fetch(url, { headers, signal: AbortSignal.timeout(ms) });
} catch (e) {
const err = e as Error & { cause?: { code?: string } };
throw new Error(err.name === "TimeoutError" ? "timeout"
: `conexiune căzută: ${err.cause?.code ?? err.message}`);
}
}
async function tipProblema(r: Response): Promise<string> {
const corp = await r.json().catch(() => ({}));
return String(corp?.type ?? "").split("/").pop() ?? "";
}
export async function firma(text: string, bugetSec = 5): Promise<Firma> {
const cui = cuiValid(text);
if (cui === null) throw new CuiInvalid(text);
const limita = Date.now() + bugetSec * 1000;
try {
return await dinApifirme(cui, limita);
} catch (e) {
if (!(e instanceof Rezerva)) throw e;
const dupa = (bugetSec - ramas(limita)).toFixed(1);
console.error(`apifirme: ${e.message}, după ${dupa} s; întreb openapi.ro`);
}
return dinOpenapiRo(cui, limita);
}
async function dinApifirme(cui: number, limita: number): Promise<Firma> {
const cheie = { Authorization: `Bearer ${process.env.APIFIRME_KEY}` };
for (const incercare of [1, 2]) {
const r = await cerere(`${APIFIRME_BASE}/companies/${cui}`, cheie,
Math.min(3, ramas(limita) - 1)).catch((e) => { throw new Rezerva(e.message); });
if (r.status === 200) return normalizeazaApifirme(await r.json());
if (r.status === 404) throw new FirmaNegasita(await tipProblema(r));
if (r.status === 202 || r.status === 429) {
const pauza = Number(r.headers.get("retry-after") ?? 2);
if (incercare === 1 && pauza + 1.5 < ramas(limita)) {
await sleep(pauza);
continue;
}
throw new Rezerva(`${r.status}, Retry-After ${pauza} s nu încape în buget`);
}
if (r.status >= 500) throw new Rezerva(`${r.status} ${await tipProblema(r)}`);
// 400, 401, 403: greșeala e la noi; rezerva ar ascunde-o
throw new Error(`apifirme: HTTP ${r.status} ${await tipProblema(r)}`);
}
throw new Rezerva("tot 202 după o pauză");
}
async function dinOpenapiRo(cui: number, limita: number): Promise<Firma> {
const cheie = { "x-api-key": process.env.OPENAPI_RO_KEY ?? "", accept: "application/json" };
for (const incercare of [1, 2]) {
if (ramas(limita) < 0.5) throw new FirmaIndisponibila("s-a terminat bugetul de timp");
const r = await cerere(`${OPENAPI_RO_BASE}/companies/${cui}`, cheie, ramas(limita))
.catch((e) => { throw new FirmaIndisponibila(`nici openapi.ro: ${e.message}`); });
if (r.status === 200) return normalizeazaOpenapiRo(await r.json());
if (r.status === 404) throw new FirmaNegasita("openapi.ro: 404");
if (r.status === 202) {
// retry_after e un moment ISO 8601; marja acoperă ceasurile nesincronizate
const { retry_after } = await r.json();
const pauza = Math.max(0.5,
(Date.parse(retry_after) - Date.now()) / 1000 + 0.25);
if (incercare === 1 && pauza + 1 < ramas(limita)) {
await sleep(pauza);
continue;
}
throw new FirmaIndisponibila("openapi.ro: firma e încă în coada lor");
}
throw new FirmaIndisponibila(`nici openapi.ro: HTTP ${r.status}`);
}
throw new FirmaIndisponibila("openapi.ro: tot 202 după o pauză");
}
function normalizeazaApifirme(d: any): Firma {
return {
cui: d.cui, denumire: d.denumire, nr_reg_com: d.nr_reg_com,
adresa: d.adresa_completa, judet: d.judet, cod_postal: d.cod_postal,
platitor_tva: d.scp_tva, tva_la_incasare: d.tva_incasare,
split_tva: d.split_tva, inactiv: d.status_inactiv,
radiata: d.stare == null ? null : d.stare === "RADIERE",
e_factura: d.e_factura, caen: d.caen, source: "apifirme",
};
}
function normalizeazaOpenapiRo(d: any): Firma {
return {
cui: Number(d.cif), denumire: d.denumire, nr_reg_com: d.numar_reg_com,
adresa: d.adresa, judet: codJudet(d.judet), cod_postal: d.cod_postal,
platitor_tva: d.tva != null, // tva = data înregistrării sau null
tva_la_incasare: laIncasareAzi(d.tva_la_incasare),
split_tva: null, inactiv: null, e_factura: null, caen: null, // fără echivalent
radiata: d.radiata ?? null, source: "openapi.ro",
};
}
type Perioada = { tip: string; de_la: string; pana_la: string | null };
function laIncasareAzi(perioade?: Perioada[] | null): DaNu {
if (perioade == null) return null;
const azi = new Date().toISOString().slice(0, 10);
return perioade.some(
(p) => p.tip === "I" && p.de_la <= azi && azi <= (p.pana_la ?? "9999"));
}
const JUDETE = new Map(
("AB ALBA|AR ARAD|AG ARGES|BC BACAU|BH BIHOR|BN BISTRITA-NASAUD|BT BOTOSANI|" +
"BR BRAILA|BV BRASOV|BZ BUZAU|CL CALARASI|CS CARAS-SEVERIN|CJ CLUJ|CT CONSTANTA|" +
"CV COVASNA|DB DAMBOVITA|DJ DOLJ|GL GALATI|GR GIURGIU|GJ GORJ|HR HARGHITA|" +
"HD HUNEDOARA|IL IALOMITA|IS IASI|IF ILFOV|MM MARAMURES|MH MEHEDINTI|MS MURES|" +
"NT NEAMT|OT OLT|PH PRAHOVA|SJ SALAJ|SM SATU MARE|SB SIBIU|SV SUCEAVA|" +
"TR TELEORMAN|TM TIMIS|TL TULCEA|VL VALCEA|VS VASLUI|VN VRANCEA|B BUCURESTI")
.split("|").map((x) => [x.slice(x.indexOf(" ") + 1), x.split(" ")[0]]),
);
function codJudet(nume: string | null): string | null {
if (!nume) return null; // openapi.ro dă numele („Sibiu”), apifirme codul („SB”)
const n = nume.normalize("NFD").replace(/\p{M}/gu, "").toUpperCase();
return JUDETE.get(n.replace(/^MUNICIPIUL /, "").trim()) ?? null;
}
// npx tsx firma.ts RO13548146 [buget]
if (process.argv[1]?.endsWith("firma.ts")) {
firma(process.argv[2] ?? "", Number(process.argv[3] ?? 5)).then(
(f) => {
console.log(JSON.stringify(f, null, 2));
process.exit(0); // nu aștepta conexiunile abandonate
},
(e) => {
console.error(`${e.constructor.name}: ${e.message}`);
process.exit(1);
});
}
AbortSignal.timeout() taie toată cererea, inclusiv citirea corpului, dar vrea milisecunde întregi. Prima versiune îi dădea 1980.9999999999998, primea The value of "delay" is out of range, iar eroarea, prinsă ca „conexiune căzută”, trimitea totul pe rezervă; a scos-o la iveală doar scenariul cu 202. Tot din teste a ieșit că, la o adresă care nu răspunde deloc, rezultatul vine după 3 s, dar procesul se închidea după 10: conectarea abandonată rămâne în bucla de evenimente până la timeout-ul propriu al clientului HTTP din Node. Într-un server nu contează; în linia de comandă, programul iese explicit.
Firma de exemplu 90000014 e inactivă fiscal. Întâi cu apifirme căzut și openapi.ro răspunzând cu 202, apoi cu 200; apoi direct prin apifirme:
$ curl -s -X POST "$MOCK/_mock/scenario?apifirme=down&openapi=pending" > /dev/null
$ time npx tsx firma.ts 90000014 | grep -E 'inactiv|e_factura|source'
apifirme: conexiune căzută: UND_ERR_SOCKET, după 0.0 s; întreb openapi.ro
"inactiv": null,
"e_factura": null,
"source": "openapi.ro"
real 0m2,385s
$ curl -s -X POST "$MOCK/_mock/scenario?apifirme=ok&openapi=ok" > /dev/null
$ npx tsx firma.ts 90000014 | grep -E 'inactiv|e_factura|source'
"inactiv": true,
"e_factura": false,
"source": "apifirme"
Dacă adaptorul ar fi completat câmpul lipsă cu false, programul de facturare ar fi emis liniștit o factură unui client inactiv. Cu null poate decide singur: blochează, cere confirmare sau reverifică mai târziu.
Ce rămâne de făcut după rezervă
- Nu suprascrie date bune cu
null. Un răspuns venit pe rezervă actualizează în baza ta doar câmpurile care nu suntnull. - Reverifică ce a venit pe rezervă. Păstrează
sourcelângă înregistrare și, înainte de factură, întreabă din nou apifirme, de exemplu prin invoice-check, care răspunde exact la inactivitate, TVA și e-Factura. - Nu plăti timeout-ul la fiecare cerere. Dacă apifirme nu răspunde un minut, fiecare căutare așteaptă 3 s până la rezervă. Un întrerupător (circuit breaker) ține minte eșecurile și trimite direct la openapi.ro o vreme; variantele pentru Laravel și .NET din seria aceasta au unul. Iar dacă linia de log apare des, uită-te la timeout-ul tău, la rețea și la pagina de stare.
De încercat
Rulează programul pe un CUI din facturile de luna trecută, apoi pune în APIFIRME_BASE o adresă care nu răspunde deloc, de exemplu http://10.255.255.1/rest/v1. La noi, rezerva a pornit după 1,5 s în Python (timeout-ul de conectare) și după 3 s în TypeScript. Dacă la tine durează mai mult decât are răbdare cel din fața formularului, scade timeout-urile, nu mări bugetul.