# 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 Cheltuieli de personal ... ... ... ... ... ... ... ``` **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.