-- 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/';
