Files
vreau-digital/services/seap-scraper/BUGETAR-PLAN.md
T
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

346 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.