Files
Claude VM a6c03a091e initial: split from gov-agreg — vreau.digital standalone platform
Moved from gov-agreg/src/pages/achizitii/* to root (drop prefix).
- 22 pages migrated, 127 files total
- All internal links: /achizitii/X → /X (176 occurrences fixed)
- AchizitiiLayout subnav rewritten: /X paths, top-right link to vreaudigital.ro hub
- BaseLayout new (vreau.digital branding, OG tags, site URL)
- astro.config.mjs: site https://vreau.digital, server output (was static)
- docker-compose: port 5096 (vreaudigital is 5095), container vreau-digital
- deploy.sh: paths /opt/vreau-digital, log /var/log/vreau-digital-deploy.log

Backend shared with gov-agreg:
- PostgreSQL satra (same schemas: seap, firms, anaf, anre, ...)
- Photon, Martin tiles
- Infisical /vreaudigital path (DATABASE_URL etc. shared)

build: PASS (npx astro check 0 errors, npm run build 5s vite + 10s server)
2026-05-13 00:10:32 +03:00

14 KiB
Raw Permalink Blame History

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):

<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"

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ă"

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).

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

# 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.