a6c03a091e
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)
346 lines
14 KiB
Markdown
346 lines
14 KiB
Markdown
# 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.
|