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

12 KiB
Raw Permalink Blame History

AEP Donații — Plan de Ingest pentru vreaudigital.ro

Status: scaffold complet, smoke test reușit (99 rows în 0.9s, 11/62 donatori PJ au și contracte SEAP — match instant pentru recipe-ul "money-to-power"). Last update: 2026-05-09 Author: Phase 5 AEP agent

1. Sursa și de ce nu direct AEP

Legea 334/2006 mandatează AEP (Autoritatea Electorală Permanentă) să publice:

  1. Donațiile peste 10 salarii minime brute — în Monitorul Oficial, anual, per partid.
  2. Rapoartele de venituri și cheltuieli (RVC) — anual, per partid + filiale.
  3. Subvențiile de la stat — lunar, per partid (75% senate + 25% local).

Surse oficiale candidate:

Sursa Format Acces Pro Contra
roaep.ro/finantare/ HTML + PDF reCAPTCHA pe nivel rădăcină Sursa primară mandatată Bot detection blochează WebFetch & curl simplu; PDF parsing dureros
finantarepartide.ro Portal AEP reCAPTCHA Date oficiale Idem reCAPTCHA + structură variabilă per an
banipartide.ro / Expert Forum SQLite expus prin endpoint base64-SQL HTTP simplu, fără protecție Date deja agregate, normalizate, cu CUI Proiect terț; cuprinde aceleași date publice prin lege
data.gov.ro (CSV-uri DataGov) CSV neagregat HTTP simplu Oficial Lipsesc anii vechi; mapping per partid manual; nu acoperă RVC granular

Decizie: ingest-ăm întâi din banipartide.ro (path de minim efort, calitate maximă), apoi cross-validăm cu AEP RVC PDFs ca v2 (citație în UI: "sursa primară: AEP — agregare via Expert Forum").

Endpoint-ul banipartide.ro

GET https://www.banipartide.ro/app/json.php?mode=dt&ssid=<base64(SQL)>
→ { "data": [ [col1, col2, ...], ...], "distinctData": {} }

Backendul e SQLite (verificat cu SELECT name FROM sqlite_master WHERE type="table"). 18 tabele, dintre care relevante:

Tabelă SQLite Rows → Tabela noastră
Donatori persoane juridice 3,612 aep.donatii_pj
Donatori persoane fizice 30,792 aep.donatii_pf
Donatori rapoarte de venituri și cheltuieli 353,473 aep.donatii_rvc
Subvenții pe an, Cheltuieli subvenții, ... varies (faza 2 — vezi §6)

Riscul de schimbare: EFOR poate scoate offline endpoint-ul. Mitigare: într-o iterație v2, scrape-ăm direct PDF-urile AEP cu Playwright headless (rezolvă reCAPTCHA-ul) și cross-validăm cu banipartide.

2. Schema DB — aep.*

Migrație: services/seap-scraper/sql/024_aep_donatii.sqlAPLICATĂ. 5 tabele + 2 MVs.

aep.partide              (id PK, nume_oficial, fondat, sediu_cui, status)
aep.donatii_pj           (source_hash UNIQUE, donator_nume, donator_cui, partid_id, suma_lei, an, ...)
aep.donatii_pf           (source_hash UNIQUE, donator_nume, donator_cnp_sha256, partid_id, suma_lei, an, ...)
aep.donatii_rvc          (source_hash UNIQUE, donator_nume, judet, tip_venit, partid_id, suma_lei, an, ...)
aep.scrape_log           (audit per scrape run)
aep.mv_donatii_per_cui          MV → folosit pe pagina firmă
aep.mv_top_donatori_partid      MV → folosit pe pagina partid

Decizii de design

  • source_hash (sha1 al cheilor naturale) ca UNIQUE constraint → ON CONFLICT DO UPDATE: scraperul e 100% idempotent, poate rula zilnic fără duplicare.
  • donator_cui_raw păstrat lângă donator_cui normalizat — sursa are typos / "RO" prefix / stringuri non-numerice; cui_matcher (deja în firms) poate ajuta la rezoluție fuzzy în faza 2.
  • CNP-urile sunt SHA-256 hashed la ingest. Niciodată stocate raw în DB. Numele rămâne pentru că e public prin lege (publicat în MO).
  • Partidele sunt auto-create la prima donație observată — registru natural, no manual seeding required.
  • Date parsing — best effort. Sursa are format haotic ("11.10.2019; 13.11.2019", 10042010, 4102019, 9/7/20). În tested smoke (99 rows): 94% parsate, 6% null pe multi-date strings (intenționat — nu putem alege una).

3. Scraperul

Fișier: services/seap-scraper/src/scrape-aep-donatii.ts (~570 linii TS).

Comenzi

# Smoke test (100 rows)
npx tsx src/scrape-aep-donatii.ts --table=pj --limit=100

# Full ingest per tabel
npx tsx src/scrape-aep-donatii.ts --table=pj
npx tsx src/scrape-aep-donatii.ts --table=pf
npx tsx src/scrape-aep-donatii.ts --table=rvc

# Toate trei + refresh MVs
npx tsx src/scrape-aep-donatii.ts --table=all

Wrapper de cron: services/seap-scraper/cron/scrape-aep-donatii.sh — same pattern ca enrich-anaf.sh / scrape-regas.sh (Infisical MI → env-file → docker run --env-file → cleanup).

Smoke test result

[aep] table=pj limit=100
[aep:pj] fetching from banipartide.ro (limit=100)...
[aep:pj] fetched 100 rows; upserting...
[aep:pj] done in 0.9s seen=100 ins=99 upd=1 skip=0
[aep] refreshing materialized views...
[aep] done.

99 rows în 0.9s. Single 100-row "full" fetch ar fi ~30s pentru 3.6K PJ, ~5min pentru 30K PF, ~30min pentru 353K RVC. Ingest-ul total estimat: <40 min, single shot.

4. Cross-source — primele recipe-uri descoperite din 99-row sample

Test query împotriva seap.announcements (642K rows existing):

SELECT d.donator_nume, d.donator_cui, d.partid_id,
       SUM(d.suma_lei) AS donat_lei,
       COUNT(DISTINCT s.ref_number) AS nr_contracte_seap,
       SUM(s.awarded_value)::bigint AS valoare_seap_lei
FROM aep.donatii_pj d
JOIN seap.announcements s ON s.supplier_cui = d.donator_cui
GROUP BY d.donator_nume, d.donator_cui, d.partid_id
ORDER BY nr_contracte_seap DESC;

Rezultate (extras din primele 99 rânduri ingest):

Donator CUI Partid Donat (lei) Contracte SEAP Valoare SEAP (lei)
ORANGE ROMANIA - S.A. 9010105 UDMR 1,555,403 829 305,284,218
IGO S.A. 7186084 PDL 65,000 13 337,118
ROMEC SRL 2075123 PDL 1,500 10 6,843
SC Mokatti Exim SRL 4660530 UDMR 1,800 9 36,101
S.C. COMISION TRADE - S.R.L. 5443785 PNL 270,000 9 88,002
VALENTINO PRODEX 4813200 PDL 15,000 5 2,547,082
S.C. Iridex Group Import Export 398284 PSD+PC 48,100 1 69,853

62 donatori cu CUI, 51 match în firms.entities (82%), 11 cu contracte SEAP — toate astea din primele 99 rânduri. Full ingest = >>multe astfel de match-uri.

5. Entity resolution pe donator_cui

Sursa banipartide are CUI-ul ca text — uneori cu typos, "RO" prefix, sau gol. Plan de resolution în faza 2:

  1. CUI directfirms.entities.cui (text = text). Acoperă deja ~80%.
  2. CUI fuzzy → folosim firms.cui_matcher_index (deja existent — vezi 019_cui_matcher.sql) pentru match pe nume + sediu când CUI lipsește.
  3. Pentru donații PF (persoane fizice) — fără CUI. Match-ul cu ANI declarații de avere (când 030_ani_* aterizează) se face pe nume_normalized. Cross-recipe: "demnitarii care au donat partidului lor".

6. Roadmap — ce urmează (NU în această sesiune)

Faza 5b — full ingest + MV-uri pe RVC (1 sesiune)

  • Run --table=all (estimat 40 min total)
  • Add MV-uri și pe donatii_pf și donatii_rvc
  • Add MV cross-source aep.mv_donator_seap_overlap (donator + total donat + total câștigat SEAP, sortat după ratio)

Faza 5c — pagini publice pe vreaudigital.ro (1 sesiune)

  • /finantare-partide — landing cu top 20 donatori per partid, top partide după volum, evoluție temporală
  • /finantare-partide/[partid] — toate donațiile per partid, filtrabile per an, donator type, sumă
  • /finantare-partide/donator/[cui] — istoricul de donații per donator + cross-link cu profile-ul firmei (/firma/[cui])
  • Adăugare badge pe /firma/[cui] și /achizitii/firma/[cui] — "🪙 Donator politic — X RON către Y partide" (folosind aep.mv_donatii_per_cui)

Faza 5d — recipes (în lib/recipes.ts, după ce Phase 3 RegAS termină)

  • donatori-care-au-castigat-seap — JOIN pe aep.donatii_pj × seap.announcements. Sortabil după (suma_donata, valoare_seap, ratio). Coloane: donator, partid, suma donat, contracte câștigate, autoritate care a contractat.
  • concentrare-donatii-per-partid — top donatori per partid, % din total donații pentru partid, evoluție temporală.
  • donator-stat-revolvingaep.donatii_pj × firms.entities (filtru pe forma_juridica IN ('SA stat', 'CN', 'RA')) — companii de stat care au donat. (Ilegal după 2006, dar verificare empirică.)
  • demnitar-donator-propriul-partid — când 030_ani_declaratii aterizează, JOIN aep.donatii_pf.donator_nume cu ani.declaratii.nume_complet.

Faza 5e — validare cu sursa primară (opțional, 1 sesiune)

  • Scraper Playwright pentru roaep.ro/finantare/ (rezolvă reCAPTCHA cu un click manual la prima rulare, cookie-uri salvate)
  • Download PDF-urile MO oficiale per partid per an
  • Parse cu pdfplumber (Python sidecar, deja avem import_*.py)
  • Compară cu aep.donatii_pj — log diff → tabela aep.validation_diffs
  • Adaugă verified_by_aep_pdf boolean în aep.donatii_pj

Faza 5f — date suplimentare din banipartide

  • Subvenții pe anaep.subventii (banii de la stat per partid per an, 2008+)
  • Contracte subventiiaep.contracte_subventii (cum cheltuie partidele subvențiile)
  • Contributii campanie + Venituri și cheltuieli campanieaep.campanie_* (date electorale specifice, finanțare campanii)
  • Rezultate alegeri × Subvenții pe an → recipe "subvenția per vot" (paritatea democratică)
  • Date publicate prin Legea 334/2006, art. 13 (donații PJ) și art. 14 (donații PF >10 salarii) — explicit publicate în Monitorul Oficial. GDPR-safe prin baza legală art. 6(1)(c) GDPR (obligație legală de publicare).
  • CNP-urile apar în sursă în clear (în RVC publicat de partide). Le hash-uim SHA-256 — nu publicăm CNP-uri raw pe vreaudigital.ro. Numele complet rămâne (e public prin lege).
  • Adresa sediului PJ e publică (din MO + ONRC). Pentru PF, sursa NU are adresă, doar nume + organizație partid.
  • Right to be forgotten: dacă cineva contestă, păstrăm un endpoint /aep/redact care setează donator_nume = '(redactat la cerere)' și donator_cnp_sha256 = NULL cu audit log. Sumele/an/partid rămân (interes public).

8. Operare

Cron (sugerat): lunar, prima zi a lunii, 03:00. Date la AEP se publică anual la 30 aprilie pentru anul precedent (RVC) + ad-hoc în MO. Update lunar e suficient — nu e dataset live.

0 3 1 * * /opt/vreaudigital/services/seap-scraper/cron/scrape-aep-donatii.sh >> /var/log/vreaudigital-aep.log 2>&1

Volumul stocat (estimare full):

  • aep.donatii_pj: 3,612 rows × ~1KB = ~4MB
  • aep.donatii_pf: 30,792 × ~500B = ~15MB
  • aep.donatii_rvc: 353,473 × ~400B = ~140MB
  • Total: ~160MB cu indexuri, neglijabil față de seap (~3GB) sau firms (~1GB).

9. Files touched

services/seap-scraper/sql/024_aep_donatii.sql            (NEW, applied to satra)
services/seap-scraper/src/scrape-aep-donatii.ts          (NEW, ~570 LOC, smoke-tested)
services/seap-scraper/cron/scrape-aep-donatii.sh         (NEW, executable, cron pattern)
services/seap-scraper/AEP-PLAN.md                        (NEW, this file)

Zero edituri în src/lib/, src/pages/, src/components/ (per regulile de exclusion-zone — Phase 3/4 agents own those).