-- 024_aep_donatii.sql -- AEP — Autoritatea Electorală Permanentă — donații electorale & finanțare partide. -- -- Sursă oficială (mandatată prin Legea 334/2006): rapoartele anuale + rapoartele -- de venituri și cheltuieli (RVC) ale partidelor + listele de donatori publicate -- în Monitorul Oficial pentru donații > 10 salarii minime brute. -- -- Vehicul de ingest: portalul Expert Forum (banipartide.ro) care a aggregat-o -- deja în SQLite și o expune via endpoint base64-SQL la -- https://www.banipartide.ro/app/json.php?mode=dt&ssid=. -- (Sursele primare AEP sunt PDF/Excel + reCAPTCHA, deci EFOR este path de -- minim efort. Validate against AEP RVC PDFs as v2.) -- -- Volume @ 2026-05-09: -- Donatori persoane juridice (>10 sal MO): 3,612 (2006-2024) -- Donatori persoane fizice (>10 sal MO): 30,792 (2006-2024) -- Donatori RVC (rapoarte venituri/cheltuieli, granular complet): 353,473 -- -- GDPR: CNP-urile sunt expuse în clear pe banipartide.ro (publicate în MO conf. -- legii). Le hash-ăm SHA256 pe ingest — la noi NU stocăm CNP raw. Numele -- complet e public prin lege și rămâne. Adresa pe firme (PJ) e public, -- la persoane fizice (PF) NU avem adresă la sursă. -- -- Cross-source value: aep.donatii_pj.donator_cui ⨯ seap.announcements.supplier_cui -- = "donator X a donat Y RON partidului Z, apoi a câștigat W RON contracte SEAP". CREATE SCHEMA IF NOT EXISTS aep; COMMENT ON SCHEMA aep IS 'Autoritatea Electorală Permanentă — donații, finanțare partide, RVC. Sursă: banipartide.ro (EFOR) → AEP/MO.'; -- ────────────────────────────────────────────────────────────────────────── -- aep.partide — registru partide normalizat (codes from banipartide source) -- ────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS aep.partide ( id text PRIMARY KEY, -- 'PSD', 'PNL', 'USR', 'AUR', 'UDMR', etc. nume_oficial text, -- 'Partidul Social Democrat' fondat date, sediu_cui text, -- CIF al partidului dacă cunoscut status text, -- 'activ' | 'dizolvat' | 'fuzionat' fetched_at timestamptz DEFAULT now() ); COMMENT ON TABLE aep.partide IS 'Registru partide politice (cheie naturală = abreviere normalizată din sursa banipartide).'; -- ────────────────────────────────────────────────────────────────────────── -- aep.donatii_pj — donații de la persoane juridice (>10 salarii minime, MO) -- ────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS aep.donatii_pj ( id bigserial PRIMARY KEY, source_hash char(40) NOT NULL UNIQUE, -- sha1(nume|cui|partid|an|suma|data_donatie) for idempotent upsert donator_nume text NOT NULL, donator_cui text, -- CUI normalizat (numerals only, RO prefix stripped) donator_cui_raw text, -- forma originală (poate avea typos / "RO") reprezentant text, sediu text, nationalitate text, -- "română" / etc. partid_id text REFERENCES aep.partide(id) ON UPDATE CASCADE, filiala_partid text, suma_lei numeric(14,2) NOT NULL, an smallint NOT NULL, data_donatie_text text, -- format mixt în sursă: "11.10.2019; 13.11.2019" sau "10042010" — păstrăm raw data_donatie date, -- best-effort parsed (NULL când format incompatibil sau multiple) tip_donatie text, -- "Bani" / "Natură" / etc. felul_donatie text, -- "Bani" / "Ordin De Plată" / "Spațiu Publicitar" source_url text NOT NULL DEFAULT 'https://www.banipartide.ro/donatori-persoane-juridice.html', fetched_at timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_aep_donatii_pj_cui ON aep.donatii_pj(donator_cui) WHERE donator_cui IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_aep_donatii_pj_partid ON aep.donatii_pj(partid_id); CREATE INDEX IF NOT EXISTS idx_aep_donatii_pj_an ON aep.donatii_pj(an); CREATE INDEX IF NOT EXISTS idx_aep_donatii_pj_suma ON aep.donatii_pj(suma_lei DESC); COMMENT ON TABLE aep.donatii_pj IS 'Donații de la persoane juridice către partide, peste pragul de 10 salarii minime brute (publicate în MO). Sursă: banipartide.ro → AEP. Granularitate: o linie per (donator, partid, an, sumă, dată).'; COMMENT ON COLUMN aep.donatii_pj.source_hash IS 'sha1(nume_lower|cui|partid|an|suma|data_text). Garantează idempotenta scraperului.'; -- ────────────────────────────────────────────────────────────────────────── -- aep.donatii_pf — donații de la persoane fizice (>10 sal min, MO) -- CNP hash-uit (NICIODATĂ raw în DB). -- ────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS aep.donatii_pf ( id bigserial PRIMARY KEY, source_hash char(40) NOT NULL UNIQUE, -- sha1(nume|cnp_hash|partid|an|suma|data) donator_nume text NOT NULL, donator_cnp_sha256 char(64), -- SHA-256 hex of CNP (only if CNP was non-empty in source) partid_id text REFERENCES aep.partide(id) ON UPDATE CASCADE, organizatia text, -- filiala / organizatia partidului suma_lei numeric(14,2) NOT NULL, an smallint NOT NULL, data_donatie_text text, data_donatie date, tip_donatie text, -- "Donație" / "Cotizație" / "Împrumut" ce_s_a_donat text, -- "Bani" / "Bunuri" / etc. source_url text NOT NULL DEFAULT 'https://www.banipartide.ro/donatori-persoane-fizice.html', fetched_at timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_aep_donatii_pf_cnp_hash ON aep.donatii_pf(donator_cnp_sha256) WHERE donator_cnp_sha256 IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_aep_donatii_pf_nume ON aep.donatii_pf(donator_nume); CREATE INDEX IF NOT EXISTS idx_aep_donatii_pf_partid ON aep.donatii_pf(partid_id); CREATE INDEX IF NOT EXISTS idx_aep_donatii_pf_an ON aep.donatii_pf(an); COMMENT ON TABLE aep.donatii_pf IS 'Donații de la persoane fizice către partide, peste pragul de 10 salarii minime (publicate în MO). CNP-urile sunt SHA-256 hashed la ingest. Sursă: banipartide.ro.'; COMMENT ON COLUMN aep.donatii_pf.donator_cnp_sha256 IS 'SHA-256 hex digest al CNP. Permite re-identificare dacă cineva are CNP-ul, dar nu dezvăluie CNP-ul. NU e key-uit cu salt — scopul e doar de-duplicare cross-an, nu protecție criptografică împotriva brute-force pe spațiul CNP-urilor românești.'; -- ────────────────────────────────────────────────────────────────────────── -- aep.donatii_rvc — toți donatorii din rapoartele de venituri/cheltuieli -- (donații + cotizații + împrumuturi, fără pragul de 10 salarii) -- ────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS aep.donatii_rvc ( id bigserial PRIMARY KEY, source_hash char(40) NOT NULL UNIQUE, donator_nume text NOT NULL, judet text, -- "Alba", "București" cod_judet text, -- "AB", "B" tip_venit text, -- "Cotizație" | "Donație" | "Împrumut" partid_id text REFERENCES aep.partide(id) ON UPDATE CASCADE, suma_lei numeric(14,2) NOT NULL, mod_incasare text, -- "Banca" | "Numerar" | etc. an smallint NOT NULL, data_donatie_text text, data_donatie date, source_url text NOT NULL DEFAULT 'https://www.banipartide.ro/donatori-rvc.html', fetched_at timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_aep_donatii_rvc_partid ON aep.donatii_rvc(partid_id); CREATE INDEX IF NOT EXISTS idx_aep_donatii_rvc_an ON aep.donatii_rvc(an); CREATE INDEX IF NOT EXISTS idx_aep_donatii_rvc_nume ON aep.donatii_rvc(donator_nume); CREATE INDEX IF NOT EXISTS idx_aep_donatii_rvc_judet ON aep.donatii_rvc(judet); COMMENT ON TABLE aep.donatii_rvc IS 'Toate donațiile/cotizațiile/împrumuturile din rapoartele de venituri și cheltuieli (RVC) ale partidelor, fără pragul de 10 salarii. ~353K rânduri. Sursă: banipartide.ro → AEP.'; -- ────────────────────────────────────────────────────────────────────────── -- aep.scrape_log — audit trail al scraperelor (per tabel × per zi) -- ────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS aep.scrape_log ( id bigserial PRIMARY KEY, scraper text NOT NULL, -- 'donatii_pj' | 'donatii_pf' | 'donatii_rvc' source_url text NOT NULL, rows_seen integer NOT NULL, rows_inserted integer NOT NULL, rows_updated integer NOT NULL, rows_skipped integer NOT NULL, duration_ms integer NOT NULL, started_at timestamptz NOT NULL, finished_at timestamptz NOT NULL DEFAULT now(), error text ); CREATE INDEX IF NOT EXISTS idx_aep_scrape_log_scraper ON aep.scrape_log(scraper, started_at DESC); -- ────────────────────────────────────────────────────────────────────────── -- Materialized view: agregare per CUI pentru profile firmă rapid. -- (refresh-ed by cron post-scrape; see refresh-mvs.sh) -- ────────────────────────────────────────────────────────────────────────── CREATE MATERIALIZED VIEW IF NOT EXISTS aep.mv_donatii_per_cui AS SELECT donator_cui AS cui, COUNT(*) AS nr_donatii, SUM(suma_lei) AS total_lei, COUNT(DISTINCT partid_id) AS nr_partide, array_agg(DISTINCT partid_id) FILTER (WHERE partid_id IS NOT NULL) AS partide, MIN(an) AS prima_donatie_an, MAX(an) AS ultima_donatie_an FROM aep.donatii_pj WHERE donator_cui IS NOT NULL GROUP BY donator_cui; CREATE UNIQUE INDEX IF NOT EXISTS idx_aep_mv_donatii_per_cui ON aep.mv_donatii_per_cui(cui); COMMENT ON MATERIALIZED VIEW aep.mv_donatii_per_cui IS 'Pre-aggregat pentru profile firmă: donații totale per CUI. Refresh după fiecare scrape.'; -- ────────────────────────────────────────────────────────────────────────── -- Materialized view: top donatori per partid (folosit pe pagini publice) -- ────────────────────────────────────────────────────────────────────────── CREATE MATERIALIZED VIEW IF NOT EXISTS aep.mv_top_donatori_partid AS SELECT partid_id, donator_nume, donator_cui, COUNT(*) AS nr_donatii, SUM(suma_lei) AS total_lei, MIN(an) AS prima_donatie_an, MAX(an) AS ultima_donatie_an FROM aep.donatii_pj WHERE partid_id IS NOT NULL GROUP BY partid_id, donator_nume, donator_cui; CREATE INDEX IF NOT EXISTS idx_aep_mv_top_donatori_partid_partid ON aep.mv_top_donatori_partid(partid_id, total_lei DESC); CREATE INDEX IF NOT EXISTS idx_aep_mv_top_donatori_partid_cui ON aep.mv_top_donatori_partid(donator_cui) WHERE donator_cui IS NOT NULL; COMMENT ON MATERIALIZED VIEW aep.mv_top_donatori_partid IS 'Top donatori per partid pentru afișare publică. Datele sunt deja publice prin lege (MO).';