# Transparență Bugetară MFP — Ingest Plan

**Sursă primară:** `https://mfinante.gov.ro/apps/transparenta-bugetara/index.htm` →
redirect spre aplicația activă `https://extranet.anaf.mfinante.gov.ro/anaf/extranet/EXECUTIEBUGETARA`.

**Scop:** Înregistrarea execuției bugetare lunare (venituri + cheltuieli) pentru
toate cele ~13.700 entități publice din România (UAT-uri, primării, consilii
județene, ministere) și cross-link cu SEAP/firms/regas pentru recipe-uri
"buget vs procurement".

---

## Status la 2026-05-09

| Fază | Stare | Descriere |
|---|---|---|
| 0. Investigație | DONE | Surse identificate, structură XML documentată |
| 1. Schema + universul entităților | DONE | 18.822 nume EP în `bugetar.entitate`, 11.971 distincte; 7.855 exact-matched cu CUI |
| 2. Ingest rapoarte XML detaliate | BLOCKED | CAPTCHA pe portalul oficial — necesită captcha solver extern |
| 3. Cross-source recipes & UI | TODO | După Faza 2 |

---

## Faza 0 — Investigație (DONE)

### 0.1 Sursele identificate

1. **Portal interactiv (CAPTCHA-protected):**
   `extranet.anaf.mfinante.gov.ro/anaf/extranet/EXECUTIEBUGETARA/Rapoarte_Forexe`
   - Filtre: tip raport (FXB-EXB-900..905, FXB-RBG-003), perioada (lună/an
     2016-2026), sector bugetar (5 valori: 01 BS, 02 BL, 03 BASS, 04 SOMAJ,
     05 FNUASS), județ, CUI/denumire entitate.
   - Output: HTML cu link-uri ad-hoc spre XML/XLSX/PDF (link-urile expiră
     după câteva minute).
   - **Blocaj:** fiecare submit cere `seccode` (CAPTCHA imagine). Endpoint-ul
     `/res/id=captchaAJAX/...` validează codul; dacă e corect, browserul
     redirectează spre URL stateful cu rezultatele.

2. **Endpoint autocomplete (NO CAPTCHA — exploited de Faza 1):**
   `POST /Rapoarte_Forexe/.../res/id=populateEpAJAX/...`
   - Body: `idSector=02&idJudet=CJ`
   - Response: `["BIBLIOTECA JUDETEANA OCTAVIAN GOGA CLUJ", ...]` (JSON array).
   - Există și `populateOcpAJAX` pentru ordonatori principali.
   - **Întoarce DOAR denumirile, NU CUI-urile.** CUI se atașează post-hoc
     prin fuzzy match cu `firms.entities`.

3. **data.gov.ro — agregate naționale:**
   `data.gov.ro/dataset/executii-bugetare` — XLS lunar BGC (Bugetul General
   Consolidat). NU per-CUI. Util pentru rollup național, nu pentru recipe-uri
   cross-source.

4. **Site-uri primării (Plan B):** Multe primării publică propriile execuții
   pe site-urile oficiale (PDF/XLSX). Utile pentru top-N municipii dacă
   captcha solver e prea scump.

### 0.2 Structura datelor (FXB-EXB-900 — raport detaliat per entitate)

Documentație MFP: PDF "Structura fisier XML raport FXB-900" la
`mfinante.gov.ro/anaf/wcm/connect/dd57bcbd-3b79-4d40-a1a9-e54c824898b9/`.

Schema XML aproximativă (de validat la Faza 2 cu un sample real):

```xml
<RAPORT id="FXB-EXB-900" cui="..." an="2024" luna="12">
  <ENTITATE cui="" denumire="" sector_bugetar="" cod_judet=""/>
  <LINIE side="cheltuieli" capitol="5101" subcapitol="510102"
         paragraf="" articol="510101" aliniat="">
    <DENUMIRE>Cheltuieli de personal</DENUMIRE>
    <CREDITE_BUG_APROBATE_INI>...</CREDITE_BUG_APROBATE_INI>
    <CREDITE_BUG_APROBATE_DEF>...</CREDITE_BUG_APROBATE_DEF>
    <CREDITE_BUG_TRIM>...</CREDITE_BUG_TRIM>
    <ANGAJAMENTE_BUG>...</ANGAJAMENTE_BUG>
    <ANGAJAMENTE_LEG>...</ANGAJAMENTE_LEG>
    <PLATI>...</PLATI>      <!-- = "execuție cumulată" -->
  </LINIE>
  ...
</RAPORT>
```

**Clasificația bugetară românească (ROMC):**
- **Capitol** (4 cifre, ex `5101` = "Autorități publice și acțiuni externe")
- **Subcapitol** (6 cifre, ex `510102` = "Autorități executive și legislative")
- **Paragraf** (8 cifre, sub-divizare funcțională)
- **Articol** (10 cifre, ex `5101010101` = "Salarii de bază")
- **Aliniat** (12 cifre, rar folosit)

**5 sectoare bugetare:**
| Cod | Denumire |
|---|---|
| 01 | Bugetul de stat (administrație centrală) |
| 02 | Bugetul local (administrație locală) |
| 03 | Bugetul asigurărilor sociale de stat |
| 04 | Bugetul fondului de șomaj |
| 05 | Bugetul FNUASS (sănătate) |

**Periodicitatea:** raportările sunt cumulate de la 1 ianuarie. Raportul
pentru luna `M` conține totalul ianuarie..M. Termen limită: ziua 15 a lunii
următoare.

### 0.3 Volum estimat

- ~13.700 entități × 12 luni × 5 ani × ~30 linii detaliu/raport ≈ **25M rânduri**
  pentru istoric complet 2020-2025 (FXB-EXB-900 detaliat).
- ~822K rânduri pentru raport agregat COFOG3 (FXB-EXB-901, ordonator principal).

---

## Faza 1 — Schema + universul entităților (DONE)

### Migrația aplicată

`services/seap-scraper/sql/026_bugetar.sql` aplicată pe satra. Obiecte create:

- `bugetar.executie` — tabela principală (fact), 7 sume cheie + clasificația
  pe 5 niveluri, UNIQUE (cui, perioadă, side, clasificare, raport_tip, sector).
- `bugetar.entitate` — universul EP descoperit din autocomplete API. Atașează
  CUI prin fuzzy match cu `firms.entities`.
- `bugetar.crawl_job` — tracking pentru job-uri de download (pentru reluare
  la întreruperi în Faza 2).
- `bugetar.mv_per_cui_year` — sumar venituri+cheltuieli per (CUI × an).
- `bugetar.mv_per_cui_capitol_year` — sumar pe capitol bugetar per (CUI × an).

### Rezultatele enumerării (rulare 2026-05-09 22:42)

| Metrică | Valoare |
|---|---|
| Combinații (sector × județ) interogate | 5 × 42 = 210 |
| Total nume entități întoarse de API | 18.822 |
| Nume distincte (după dedup) | 11.971 |
| Marcate ordonator principal | 4.142 |
| Timp execuție | ~3 minute (cu 300ms delay între cereri) |

### Match CUI (rulare 2026-05-09 22:45)

Faza match-cui rulează 2-pass:
1. **Exact-normalized** (lowercase + strip diacritice + strip non-alfanumerice):
   **7.855 entități** matched cu CUI din `firms.entities` (42% acoperire).
2. **Fuzzy pg_trgm** (similarity > 0.55) — DEFERRED.

**Rezultat final Faza 1 (după primul exact-match pass):**

| Metrică | Valoare | % |
|---|---|---|
| Total entități | 18.822 | 100% |
| Cu CUI atașat (exact match) | 7.855 | 42% |
| Fără CUI (necesită fuzzy / manual) | 10.967 | 58% |

**Notă fuzzy match:** Tentativa inițială (cross-product 11K × 3.9M) a depășit
20 min CPU și a fost terminată. Optimizarea cu pre-filtrare la firme cu
denumire de instituție publică (20.294 candidați) a fost de asemenea lentă
(>15 min). **TODO Faza 1.1:** rescrie fuzzy-pass în batch-uri de 500 entități
unmatched o dată, cu LATERAL join + hard limit pe candidați per entitate.
Sau: precomputează un index suplimentar pe `firms.entities.name` filtrat
doar la denumiri de instituții publice (CREATE TABLE bugetar.candidate_firms
AS SELECT ... ; CREATE INDEX ON ... USING gin(name gin_trgm_ops)).

---

## Faza 2 — Ingest rapoarte XML (BLOCKED, ~80h effort)

### Blocajele

1. **CAPTCHA pe orice search.** Aplicația WebSphere randează un PNG `kaptcha`
   pe pagina de formular și refuză submit-ul fără cod corect.
2. **URL-uri stateful WebSphere.** Path-urile `!ut/p/a1/...` se schimbă per
   sesiune. Trebuie re-fetched la pornirea fiecărui crawler.
3. **Link-uri ad-hoc expirante.** Fișierele XML/XLSX au URL-uri valide doar
   ~minute după randarea paginii de rezultate.

### Plan implementare Faza 2

**Captcha solver:** integrare 2captcha sau anti-captcha (~$2/1000 captcha).
- Pentru ingest istoric complet (2020-2025): ~13.700 entități × 12 luni × 5
  ani × 2 tipuri raport × 1 captcha/cerere ≈ **1.6M captcha-uri ≈ $3.2K-$8K**.
- Optimizare: o sesiune validă (după captcha rezolvat) probabil permite
  multiple search-uri până expirare. Necesită experimentare empirică pentru
  a estima reduce.
- Optimizare alternativă: descarcă DOAR top-1000 entități (UAT-uri mari +
  ministere) × 5 ani × 12 luni = 60K cereri ≈ $120-300. Acoperă ~80% din
  cheltuielile publice.

**Crawler asincron (TypeScript):**
1. `bootstrapPortal()` — re-fetch URL stateful + cookie sesiune.
2. `solveCaptcha(imgUrl)` → 2captcha API → `seccode`.
3. `searchReports(filters)` → POST formular cu `seccode` → HTML rezultate.
4. `extractDownloadLinks(html)` → URL-uri XML.
5. `downloadAndParse(url)` → fișier XML → `bugetar.executie` rows.
6. `bugetar.crawl_job` urmărește (cui, period, raport_tip) → status, retries.

**Parser XML:** `fast-xml-parser` (de adăugat la dependencies). Tolerant
case-insensitive pentru numele tag-urilor (variază între versiuni MFP).

### Plan B — fără captcha solver

Multe primării publică propriile execuții pe site-urile lor:
- Format frecvent: PDF/XLSX cu același template MFP (ușor de parsat).
- Acoperire variabilă: primăriile mari (Cluj, București, Iași, Timișoara)
  publică lunar/anual; comunele mici doar anual sau deloc.
- Strategy: scraper per-domain pentru top-100 primării (acoperire ~70%
  populație). Parser uniform pe baza template-ului MFP standard.

---

## Faza 3 — Cross-source recipes (TODO)

### Recipe-uri propuse

#### Recipe 1: "Concentrare furnizor SEAP în bugetul UAT"

```sql
WITH chelt AS (
  SELECT cui, period_year, cheltuieli_total
  FROM bugetar.mv_per_cui_year
  WHERE period_year = 2024
),
seap_per_uat AS (
  SELECT
    a.authority_cui AS uat_cui,
    a.contractor_cui,
    SUM(a.value_eur * 5.0) AS suma_seap_ron  -- aproximativ
  FROM seap.announcements a
  WHERE a.is_award = true
    AND extract(year from a.publication_date) = 2024
  GROUP BY a.authority_cui, a.contractor_cui
),
top_vendor AS (
  SELECT DISTINCT ON (uat_cui)
    uat_cui, contractor_cui, suma_seap_ron
  FROM seap_per_uat
  ORDER BY uat_cui, suma_seap_ron DESC
)
SELECT
  c.cui AS uat_cui,
  e.entity_name_sample AS uat_name,
  c.cheltuieli_total::bigint AS buget_chelt_2024,
  tv.contractor_cui,
  tv.suma_seap_ron::bigint AS top_vendor_suma,
  round(100.0 * tv.suma_seap_ron / NULLIF(c.cheltuieli_total, 0), 2) AS pct_concentrare
FROM chelt c
JOIN bugetar.mv_per_cui_year e ON e.cui = c.cui AND e.period_year = c.period_year
LEFT JOIN top_vendor tv ON tv.uat_cui = c.cui
WHERE c.cheltuieli_total > 1000000  -- min 1M RON
ORDER BY pct_concentrare DESC NULLS LAST
LIMIT 50;
```

**Output așteptat:** "Comuna X: 80% din cheltuielile 2024 (1.2M RON din 1.5M)
au fost cheltuiți cu firma Y prin SEAP."

#### Recipe 2: "Capitol bugetar consumat disproporționat de 1 firmă"

```sql
WITH cap AS (
  SELECT cui, period_year, capitol, suma_total AS chelt_capitol
  FROM bugetar.mv_per_cui_capitol_year
  WHERE period_year = 2024 AND side = 'cheltuieli'
),
seap_cap AS (
  -- TODO: mapping CAEN/cpv_code → capitol bugetar (ex: cpv 71300000 → cap 7001 invest)
  SELECT a.authority_cui, a.contractor_cui, SUM(a.value_eur * 5.0) suma
  FROM seap.announcements a WHERE a.is_award AND extract(year from a.publication_date) = 2024
  GROUP BY 1, 2
)
SELECT cap.cui, cap.capitol, cap.chelt_capitol, sc.contractor_cui, sc.suma,
       round(100.0 * sc.suma / NULLIF(cap.chelt_capitol, 0), 2) AS pct
FROM cap JOIN seap_cap sc ON sc.authority_cui = cap.cui
WHERE pct > 50
ORDER BY pct DESC;
```

#### Recipe 3: "UAT cu execuție bugetară < 30% din credite aprobate"

Indicator de "primării care nu reușesc să cheltuie banii alocați" — semn de
incompetență administrativă sau corupție (banii returnați la centru și
rocate ulterior).

```sql
SELECT cui, period, side, capitol, classification_label,
       credite_bug_aprobate_def AS aprobat,
       plati_efectuate AS executat,
       round(100.0 * plati_efectuate / NULLIF(credite_bug_aprobate_def, 0), 1) AS pct_executie
FROM bugetar.executie
WHERE side = 'cheltuieli' AND period_year = 2024 AND period_month = 12
  AND credite_bug_aprobate_def > 100000
  AND plati_efectuate / NULLIF(credite_bug_aprobate_def, 0) < 0.30
ORDER BY (credite_bug_aprobate_def - plati_efectuate) DESC
LIMIT 100;
```

### UI propus (Faza 3)

- **Profil UAT** (`/uat/[cui]`): sumar venituri/cheltuieli pe ultimii 5 ani,
  evoluția pe capitol bugetar, top furnizori SEAP cu pondere bugetară.
- **Recipe page** (`/recipe/concentrare-furnizor`): listă top 50 primării cu
  cea mai mare concentrare 1-furnizor, drill-down per UAT.
- **Hartă capitol bugetar:** Romania map colorat după "% buget consumat pe
  cap 51 admin" — primării care cheltuie disproporționat pe propria
  birocrație.

---

## Comenzi utile

```bash
# Faza 1 — enumerate (idempotent, ~3 min)
ssh satra "sudo MODE=enumerate /opt/vreaudigital/services/seap-scraper/cron/scrape-bugetar.sh"

# Faza 1 — fuzzy match nume → CUI (după ce firms.entities e populat)
ssh satra "sudo MODE=match-cui /opt/vreaudigital/services/seap-scraper/cron/scrape-bugetar.sh"

# Verificare status
ssh satra "/tmp/baseline.sh -c \"
  SELECT count(*) total,
         count(cui) with_cui,
         count(*) FILTER (WHERE is_ordonator_principal) ocp,
         count(DISTINCT entity_name) distinct_names
  FROM bugetar.entitate;
\""

# Refresh MV (după ingest Faza 2)
ssh satra "/tmp/baseline.sh -c \"
  REFRESH MATERIALIZED VIEW CONCURRENTLY bugetar.mv_per_cui_year;
  REFRESH MATERIALIZED VIEW CONCURRENTLY bugetar.mv_per_cui_capitol_year;
\""
```

---

## Effort estimate pentru Faza 2

| Task | Effort | Cost |
|---|---|---|
| Captcha solver integration (2captcha API) | 4h | - |
| Crawler asincron (cu retry/backoff) | 12h | - |
| Parser FXB-EXB-900 + validare pe 10 sample-uri | 8h | - |
| Test pe 100 entități × 12 luni | 4h | ~$3 |
| Run istoric top-1000 entități × 60 luni | 8h | $120-300 |
| Run istoric COMPLET 13.7K × 60 luni | 40h | $3.2K-8K |
| MV refresh + indexare suplimentară | 4h | - |
| **Total Faza 2 (top-1000 only)** | **~40h** | **~$300** |
| **Total Faza 2 (complet)** | **~80h** | **~$5K** |

**Recomandare:** Start cu top-1000 (UAT-uri mari + ministere + agenții
centrale) — acoperă ~80% din volumul cheltuielilor publice cu 5% din cost.
Scaling la full doar dacă Faza 3 demonstrează tracțiune.
