Files
vreau-digital/services/seap-scraper/sql/026_bugetar.sql
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

225 lines
15 KiB
SQL
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.
-- 026_bugetar.sql
-- Transparență Bugetară MFP — execuții bugetare ale entităților publice.
-- Source primar: https://mfinante.gov.ro/apps/transparenta-bugetara/index.htm
-- → redirecționează spre aplicația activă: extranet.anaf.mfinante.gov.ro/anaf/extranet/EXECUTIEBUGETARA
--
-- KILLER USE CASE: cross-reference cu seap.announcements pentru a calcula
-- "ponderea unui furnizor SEAP în cheltuielile totale ale unei UAT" — adică
-- "Comuna X a cheltuit 80% din buget cu 1 furnizor". Plus "Capitole bugetare
-- consumate disproporționat de 1 firmă" (cap 51 admin pub × top vendor).
--
-- Volum estimat: ~13.700 entități × 12 luni × 5 ani × ~30 linii/raport
-- ≈ 25M rânduri pentru detaliat (FXB-EXB-900). Pentru raport
-- COFOG3 agregat (FXB-EXB-901), ~822K rânduri pentru perioada
-- 2021-2025 la nivel ordonator principal.
--
-- ─── LIMITĂRI SURSE DE DATE (2026-05-09) ────────────────────────────────────
-- 1. Aplicația oficială (extranet.anaf.mfinante.gov.ro/EXECUTIEBUGETARA) e
-- IBM WebSphere Portal cu CAPTCHA imagine pe fiecare căutare. Endpoint-ul
-- de căutare e POST cu un URL stateful + `seccode`. Nu există URL deep
-- direct per (CUI, perioadă) fără sesiune + captcha solver.
-- 2. Există un endpoint de autocomplete EXPUS fără captcha care întoarce TOATE
-- denumirile entităților publice per (sector_bugetar, județ):
-- POST /Rapoarte_Forexe/.../res/id=populateEpAJAX/.../
-- data: idSector=02&idJudet=CJ
-- response: ["BIBLIOTECA JUDETEANA OCTAVIAN GOGA CLUJ", ...] (JSON array).
-- Util pentru a construi universul ~13.7K entități, dar NU întoarce CUI-urile.
-- 3. Fișiere XML/XLSX detaliate (FXB-EXB-900) se descarcă DOAR dintr-un raport
-- de rezultate randat după captcha. Hash-uri de URL sunt valide ~minute.
-- 4. data.gov.ro publică doar agregate naționale (BGC = Bugetul General
-- Consolidat) ca XLS lunar — NU per-CUI.
-- 5. Multe primării publică propriile execuții pe site-urile lor (PDF/XLSX),
-- dar formatele variază — Plan B pentru top-N municipii.
--
-- ─── STRATEGIA INGEST (faze) ────────────────────────────────────────────────
-- Faza 1 (THIS migration): schema completă pregătită pentru parser FXB-EXB-900
-- + tabelă auxiliară bugetar.entitate cu universul EP din autocomplete API
-- (~13.7K rânduri × 5 sectoare × 42 județe ≈ 30K seed-uri ce vor fi dedupe-uite).
-- + descrierea formatelor XML/XLSX (din PDF-urile MFP "Structura fisier XML
-- raport FXB-900/901/905") așa încât parserul să fie deterministic.
-- Faza 2 (TODO ~80h): integrare captcha solver (2captcha/anti-captcha) +
-- crawler asincron care urmează (sector × județ × tipRaport × an × lună).
-- Faza 3: cross-link cu firms.entities + seap.announcements pentru recipe-uri
-- "buget vs procurement".
CREATE SCHEMA IF NOT EXISTS bugetar;
-- ────────────────────────────────────────────────────────────────────────────
-- Tabel principal: linii de execuție bugetară per (entitate × perioadă × clasificație)
-- ────────────────────────────────────────────────────────────────────────────
-- Format aliniat pe FXB-EXB-900 (raport detaliat per entitate publică) +
-- agregatele FXB-EXB-901 (ordonator principal) și FXB-EXB-905 (ordonator secundar).
-- Câmpurile clasificării bugetare urmăresc structura ROMC (Clasificația
-- Bugetară Românească): Capitol → Subcapitol → Paragraf → Articol → Aliniat.
CREATE TABLE IF NOT EXISTS bugetar.executie (
id bigserial PRIMARY KEY,
-- Identificare entitate
cui text NOT NULL, -- CUI entitate publică (fără prefix RO)
cui_ordonator text, -- CUI ordonator principal (poate diferi de cui)
entity_name text, -- denumire la momentul raportării
sector_bugetar text, -- '01' BS, '02' BL, '03' BASS, '04' SOMAJ, '05' FNUASS
judet text, -- cod 2 litere (AB, CJ, B, ...)
-- Perioada raportării
period text NOT NULL, -- 'YYYY-MM' (cumulat de la 1 ian până la sfârșitul lunii)
period_year smallint NOT NULL,
period_month smallint NOT NULL, -- 1..12
-- Tip raport sursă
raport_tip text NOT NULL, -- 'FXB-EXB-900' | 'FXB-EXB-901' | 'FXB-EXB-905' | 'FXB-RBG-003' | 'FXB-EXB-902'
raport_nivel text, -- 'entitate' | 'ordonator_principal' | 'ordonator_secundar'
-- Clasificația bugetară (5 niveluri Cf. ROMC)
side text NOT NULL, -- 'venituri' | 'cheltuieli'
capitol text, -- 4 cifre, ex '5101' = Autorități publice
subcapitol text, -- 6 cifre, ex '510102'
paragraf text, -- 8 cifre
articol text, -- 10 cifre, ex '5101010101'
aliniat text, -- 12 cifre (rar folosit)
classification_label text, -- denumire human-readable
cofog3 text, -- cod COFOG3 (Classification of Functions of Government, agregat)
-- Sumele cheie (toate în RON, cumulat de la 1 ian)
-- Înțeles per FXB-EXB-900:
-- credite_bug_aprobate_ini = bugetul aprobat inițial pentru anul curent
-- credite_bug_aprobate_def = bugetul aprobat definitiv (după rectificări) la sfârșit perioadă
-- credite_bug_trimestru = creditele bugetare trimestriale cumulate
-- angajamente_bugetare = sumele angajate (FXB-EXB-902)
-- angajamente_legale = sumele angajate prin contracte ferme
-- plati_efectuate = plăți efective la sfârșit perioadă (= "execuția cumulată")
-- incasari_realizate = pentru side='venituri', sumele încasate
credite_bug_aprobate_ini numeric(20,2),
credite_bug_aprobate_def numeric(20,2),
credite_bug_trimestru numeric(20,2),
angajamente_bugetare numeric(20,2),
angajamente_legale numeric(20,2),
plati_efectuate numeric(20,2),
incasari_realizate numeric(20,2),
-- Sumă "primary" pentru queries simple — pentru side='cheltuieli' = plati_efectuate,
-- pentru side='venituri' = incasari_realizate. Calculat la INSERT.
suma_executat numeric(20,2),
-- Metadata sursă
source_url text, -- URL original al fișierului XML/XLSX
source_hash text, -- sha256(URL + filename) pentru dedup
fetched_at timestamptz NOT NULL DEFAULT now(),
-- Constraint de unicitate: un (entitate, perioadă, side, clasificare, raport_tip, sursa) e unic.
-- aliniat poate fi NULL — folosim COALESCE prin index parțial.
CONSTRAINT uq_bugetar_executie_full UNIQUE
(cui, period, raport_tip, side, capitol, subcapitol, paragraf, articol, aliniat, sector_bugetar)
);
CREATE INDEX IF NOT EXISTS idx_bugetar_executie_cui_year ON bugetar.executie(cui, period_year DESC);
CREATE INDEX IF NOT EXISTS idx_bugetar_executie_period ON bugetar.executie(period_year, period_month);
CREATE INDEX IF NOT EXISTS idx_bugetar_executie_capitol ON bugetar.executie(capitol) WHERE capitol IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_bugetar_executie_judet_sector ON bugetar.executie(judet, sector_bugetar);
CREATE INDEX IF NOT EXISTS idx_bugetar_executie_side_amount ON bugetar.executie(side, suma_executat DESC NULLS LAST);
COMMENT ON TABLE bugetar.executie IS
'Linii de execuție bugetară (FXB-EXB-900/901/905). Un rând per (entitate, perioadă, clasificație, side).';
COMMENT ON COLUMN bugetar.executie.suma_executat IS
'Suma "principală" pentru queries: plati_efectuate la cheltuieli, incasari_realizate la venituri.';
-- ────────────────────────────────────────────────────────────────────────────
-- Universul entităților publice raportoare (descoperit din autocomplete API)
-- ────────────────────────────────────────────────────────────────────────────
-- Pasul 1 al ingest-ului: enumeră (sector_bugetar × județ) → descarcă lista
-- denumirilor de entități publice. Apoi fuzzy-match cu firms.entities pentru a
-- atașa CUI. Asta deblochează crawl-ul Fazei 2 (când avem captcha solver).
CREATE TABLE IF NOT EXISTS bugetar.entitate (
id bigserial PRIMARY KEY,
entity_name text NOT NULL, -- denumire raw din MFP (case-sensitive)
sector_bugetar text NOT NULL, -- '01' .. '05'
judet text NOT NULL, -- cod 2 litere
cui text, -- atașat post-hoc prin fuzzy match
cui_match_score numeric(5,2), -- 0..1 confidence pentru matching
cui_match_method text, -- 'exact' | 'fuzzy_anaf' | 'manual'
is_ordonator_principal boolean DEFAULT false, -- true dacă apare în lista populateOcpAJAX
raport_count integer DEFAULT 0, -- număr de raport-perioade descărcate cu succes
last_fetched_period text, -- 'YYYY-MM' al ultimei perioade ingest-uate
fetched_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (entity_name, sector_bugetar, judet)
);
CREATE INDEX IF NOT EXISTS idx_bugetar_entitate_cui ON bugetar.entitate(cui) WHERE cui IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_bugetar_entitate_judet ON bugetar.entitate(judet, sector_bugetar);
CREATE INDEX IF NOT EXISTS idx_bugetar_entitate_name_trgm ON bugetar.entitate USING gin(entity_name gin_trgm_ops);
COMMENT ON TABLE bugetar.entitate IS
'Universul entităților publice raportoare descoperit din autocomplete API MFP. CUI-ul se atașează post-hoc prin fuzzy match cu firms.entities.';
-- ────────────────────────────────────────────────────────────────────────────
-- Job tracking — pentru reluare crawl la întreruperi
-- ────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS bugetar.crawl_job (
id bigserial PRIMARY KEY,
cui text,
entity_name text,
period text NOT NULL, -- 'YYYY-MM'
raport_tip text NOT NULL,
status text NOT NULL DEFAULT 'pending', -- 'pending' | 'fetched' | 'parsed' | 'failed' | 'no_data'
attempts smallint NOT NULL DEFAULT 0,
last_error text,
rows_inserted integer,
fetched_at timestamptz,
parsed_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (cui, period, raport_tip)
);
CREATE INDEX IF NOT EXISTS idx_bugetar_crawl_status ON bugetar.crawl_job(status, period);
-- ────────────────────────────────────────────────────────────────────────────
-- Materialized views pentru dashboard rapid
-- ────────────────────────────────────────────────────────────────────────────
-- Sumar per (CUI × an): venituri totale + cheltuieli totale + nr luni raportate.
CREATE MATERIALIZED VIEW IF NOT EXISTS bugetar.mv_per_cui_year AS
SELECT
cui,
period_year,
SUM(suma_executat) FILTER (WHERE side = 'venituri') AS venituri_total,
SUM(suma_executat) FILTER (WHERE side = 'cheltuieli') AS cheltuieli_total,
COUNT(DISTINCT period) AS months_reported,
MAX(entity_name) AS entity_name_sample,
MAX(judet) AS judet,
MAX(sector_bugetar) AS sector_bugetar
FROM bugetar.executie
GROUP BY cui, period_year;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_bugetar_cui_year ON bugetar.mv_per_cui_year(cui, period_year);
CREATE INDEX IF NOT EXISTS idx_mv_bugetar_year_chelt ON bugetar.mv_per_cui_year(period_year, cheltuieli_total DESC NULLS LAST);
COMMENT ON MATERIALIZED VIEW bugetar.mv_per_cui_year IS
'Sumar venituri+cheltuieli per (CUI × an). Refresh după fiecare ingest batch.';
-- Sumar per (CUI × an × capitol) — pentru analiza distribuției pe capitole bugetare.
CREATE MATERIALIZED VIEW IF NOT EXISTS bugetar.mv_per_cui_capitol_year AS
SELECT
cui,
period_year,
capitol,
side,
SUM(suma_executat) AS suma_total,
MAX(classification_label) AS capitol_label
FROM bugetar.executie
WHERE capitol IS NOT NULL
GROUP BY cui, period_year, capitol, side;
CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_bugetar_cui_cap_year
ON bugetar.mv_per_cui_capitol_year(cui, period_year, capitol, side);
COMMENT ON MATERIALIZED VIEW bugetar.mv_per_cui_capitol_year IS
'Sumar pe capitol bugetar per (CUI × an). Pentru analiza "pe ce s-au cheltuit banii".';
-- ────────────────────────────────────────────────────────────────────────────
-- COMMENTS & schema-level metadata
-- ────────────────────────────────────────────────────────────────────────────
COMMENT ON SCHEMA bugetar IS
'Transparență Bugetară MFP — execuția bugetară lunară a entităților publice. Sursă: https://mfinante.gov.ro/apps/transparenta-bugetara/';