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)
This commit is contained in:
Claude VM
2026-05-13 00:10:32 +03:00
commit a6c03a091e
352 changed files with 75295 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
-- 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/';