---
title: "Fallback de la apifirme la openapi.ro: când treci pe rezervă și când nu"
description: "Timeout scurt, rezervă doar la timeout, conexiune căzută și 5xx, niciodată la 404. Același adaptor în Python și TypeScript, cu null unde nu se știe."
date: 2026-10-03
category: "Fiabilitate"
tags: ["fallback", "openapi.ro", "Python", "timeout", "TypeScript"]
author: "Echipa apifirme"
url: https://blog.apifirme.dev/fallback-apifirme-openapi-ro/
lang: ro-RO
---

# Fallback de la apifirme la openapi.ro: când treci pe rezervă și când nu

> Timeout scurt, rezervă doar la timeout, conexiune căzută și 5xx, niciodată la 404. Același adaptor în Python și TypeScript, cu null unde nu se știe.

**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-After` nu încape în buget. Niciodată la 400 sau 404.
- Ambele răspunsuri ajung în aceeași structură, cu `source` marcat și cu `null` pentru 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](https://apifirme.dev/docs#erori):

| 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](https://apifirme.dev/docs#limite); 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:

```console
$ 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`; `cif` e text, nu număr.
- `judet` e numele („Ilfov”), nu codul (`IF`). Adaptorul îl traduce, ca să nu ai două feluri de județe în aceeași coloană.
- `tva` e data înregistrării în scopuri de TVA sau `null`; din el iese `platitor_tva`.
- `tva_la_incasare` e o listă de perioade, nu da/nu. Am considerat că firma aplică azi TVA la încasare dacă are o perioadă de tip `I` care 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.

`firma.py`

```python
"""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](https://apifirme.dev/docs#get-rest-v1-validate-cui-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:

```console
$ 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`.

```console
$ 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ă:

```console
$ 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`

```typescript
// 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:

```console
$ 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 sunt `null`.
- **Reverifică ce a venit pe rezervă.** Păstrează `source` lângă înregistrare și, înainte de factură, întreabă din nou apifirme, de exemplu prin [invoice-check](https://apifirme.dev/docs#get-rest-v1-invoice-check-cui), 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](https://apifirme.dev/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.
---

*Versiunea Markdown a articolului [Fallback de la apifirme la openapi.ro: când treci pe rezervă și când nu](https://blog.apifirme.dev/fallback-apifirme-openapi-ro/) de pe blog.apifirme.dev, blogul API-ului apifirme (https://apifirme.dev). Toate articolele: https://blog.apifirme.dev/llms.txt*
