a6c03a091e
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)
225 lines
15 KiB
SQL
225 lines
15 KiB
SQL
-- 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/';
|