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)
14 KiB
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
-
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.
-
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
populateOcpAJAXpentru ordonatori principali. - Întoarce DOAR denumirile, NU CUI-urile. CUI se atașează post-hoc
prin fuzzy match cu
firms.entities.
- Body:
-
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. -
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 cufirms.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:
- Exact-normalized (lowercase + strip diacritice + strip non-alfanumerice):
7.855 entități matched cu CUI din
firms.entities(42% acoperire). - 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
- CAPTCHA pe orice search. Aplicația WebSphere randează un PNG
kaptchape pagina de formular și refuză submit-ul fără cod corect. - URL-uri stateful WebSphere. Path-urile
!ut/p/a1/...se schimbă per sesiune. Trebuie re-fetched la pornirea fiecărui crawler. - 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):
bootstrapPortal()— re-fetch URL stateful + cookie sesiune.solveCaptcha(imgUrl)→ 2captcha API →seccode.searchReports(filters)→ POST formular cuseccode→ HTML rezultate.extractDownloadLinks(html)→ URL-uri XML.downloadAndParse(url)→ fișier XML →bugetar.executierows.bugetar.crawl_joburmă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.