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)
This commit is contained in:
Claude VM
2026-05-13 00:10:32 +03:00
commit a6c03a091e
352 changed files with 75295 additions and 0 deletions
+345
View File
@@ -0,0 +1,345 @@
# 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.