# 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.sql` — **APLICATĂ.** 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

```bash
# 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):

```sql
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 direct** → `firms.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-revolving`** — `aep.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 an` → `aep.subventii` (banii de la stat per partid per an, 2008+)
- `Contracte subventii` → `aep.contracte_subventii` (cum cheltuie partidele subvențiile)
- `Contributii campanie` + `Venituri și cheltuieli campanie` → `aep.campanie_*` (date electorale specifice, finanțare campanii)
- `Rezultate alegeri` × `Subvenții pe an` → recipe "subvenția per vot" (paritatea democratică)

## 7. Observații GDPR & legal

- **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.

```cron
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).
