# ANAF Datornici — recipes & integration handoff Status la **2026-05-09**: schema `anaf.*` aplicată, 140,777 firme T1-2016 ingerate (83.2 mld RON datorie totală). Surse live (anaf.ro/restante/) **CAPTCHA-blocked** — vezi limitări mai jos. ## Ce există acum în DB ```sql -- 140,777 firme cu obligații restante la 2016-03-31 anaf.datornici -- mari (164) + mijlocii (2,132) + mici (138,481) anaf.lista_alba -- gol (lista albă necesită live scrape — captcha-blocked) anaf.datornici_latest -- view DISTINCT ON (cui) ORDER BY pub_date DESC ``` Coloane importante: - `cui` (text, fără prefix RO) - `publication_date` (date) — `2016-03-31` pentru singura publicare ingerată - `period_label` — `'T1 2016'` - `debtor_category` — `'mari'` | `'mijlocii'` | `'mici'` - `debt_total`, `debt_principal`, `debt_penalty`, `debt_contested` (numeric RON) - detaliu per buget (state, social, unemployment, health) × (principal, penalty, contested) Index-uri: `cui`, `publication_date DESC`, `debt_total DESC`, `debtor_category`. ## Limitări — citește înainte de a planifica scraperul live 1. **anaf.ro/restante/index.xhtml** e o aplicație JSF/PrimeFaces cu **CAPTCHA** pe submit. Am încercat: - JSF AJAX submit fără CAPTCHA → `rowCount=0` silent (nu eroare, dar tabel gol) - Replay cu cookie + ViewState valid → același rezultat (CAPTCHA validată server-side, nu client-side) - Nu există endpoint JSON public alternativ 2. **anaf.ro nu publică arhive trimestriale istorice public**. Doar trimestrul curent e accesibil prin UI (cu CAPTCHA). Pentru istorie trebuie: - archive.org snapshots (manual, fragmentar) - sau colaborare cu listafirme.eu (paywall API ~€/lună) 3. **data.gov.ro** publică doar Q1-2016 ca CSV (3 fișiere mari/mijlocii/ micijuridice) — `dataset/datoriile-catre-bugetul-de-stat`. Nu se actualizează. Pentru live scrape, trebuie integrat un **captcha solver extern** (2captcha sau anti-captcha, ~$1-3 / 1000 captcha-uri). Stub în `src/scrape-anaf-datornici.ts::scrapeAnafLive()` (comentat). Workflow: ``` 1. GET /restante/index.xhtml → ViewState + JSESSIONID 2. GET /restante/kaptcha.jpg?pfdrid_c=true → bytes (PNG) 3. POST img la 2captcha.com/in.php → ID, polled la /res.php?action=get 4. POST /restante/index.xhtml cu form:inputc= 5. Parse XML → extract rows 6. PrimeFaces dataTable_paginator → POST cu page param până la `(N of N)` ``` Estimare: ~5-15K rânduri × ~30 secunde/captcha-iterație × 1 trimestru = ~1-2h per trimestru. Dacă vrem 4 trimestre × 5 ani = 20 trimestre = ~20h totale. ## Recipe propus pentru recipes.ts (Phase 4 ANI agent owns recipes.ts) > **NU edita recipes.ts în această sesiune** — Phase 4 ANI a commit-uit deja > `politicianFirmaFurnizorStat`. Această secțiune documentează ce **trebuie > adăugat** în următoarea sesiune unde recipes.ts e disponibil. ### `firmeDatorniceCuContracteSeap` — KILLER red-flag Firme care apăreau pe lista ANAF datornici la o dată X, ȘI au câștigat contracte publice SEAP **după** acea dată — interzis prin art. 165 Legea 98/2016 (pentru obligații executorii). **Date validation pe data live (Q1-2016 snapshot):** - 1,561 firme datornice → 36,403 contracte → 5.83 mld RON - Top: URBAN SA (485 mil debt → 64 contracte), SOCIETATEA COMPLEXUL ENERGETIC HUNEDOARA (477 mil debt), HIDROELECTRICA (214 mil debt → 48 contracte 79 mil RON post-publicare), ROMAERO, SRTV. ```ts { slug: 'firme-datornice-cu-contracte-seap', title: 'Firme datornice ANAF care au câștigat contracte SEAP', desc: 'Firme care apăreau pe lista ANAF cu datorii la stat — și au luat contracte publice imediat după (interzis Legea 98/2016 art. 165).', category: 'red-flags', badge: '🚨 datornic + contract', sql: ` SELECT d.cui, d.name AS firma, d.period_label, ROUND(d.debt_total/1000000.0, 2) AS datorie_mil_ron, d.debtor_category AS categorie_datornic, COUNT(DISTINCT a.id) AS contracte, ROUND(SUM(a.awarded_value)::numeric/1000000.0, 2) AS contracte_mil_ron, MAX(a.publication_date::date) AS ultim_contract, e.adr_judet AS judet FROM anaf.datornici d JOIN seap.announcements a ON a.supplier_cui = d.cui LEFT JOIN firms.entities e ON e.cui = d.cui WHERE a.publication_date::date > d.publication_date AND a.awarded_value IS NOT NULL AND a.awarded_value > 0 GROUP BY d.cui, d.name, d.period_label, d.debt_total, d.debtor_category, e.adr_judet HAVING SUM(a.awarded_value) > 100000 -- filter zgomot ORDER BY SUM(a.awarded_value) DESC LIMIT 200; `, cols: [ { key: 'cui', label: 'CUI' }, { key: 'firma', label: 'Firmă', link: (r) => `/achizitii/firma/${r.cui}` }, { key: 'period_label', label: 'Trimestrul publicării' }, { key: 'datorie_mil_ron', label: 'Datorie (mil RON)', numeric: true }, { key: 'categorie_datornic', label: 'Categorie ANAF' }, { key: 'contracte', label: 'Nr. contracte SEAP', numeric: true }, { key: 'contracte_mil_ron', label: 'Valoare contracte (mil RON)', numeric: true }, { key: 'ultim_contract', label: 'Ultim contract' }, { key: 'judet', label: 'Județ' }, ], } ``` **Caveats pentru recipe:** - Cu doar T1-2016 ingerat, recipe-ul reflectă **doar acel snapshot** — toate contractele post-2016-03-31, fără să știm dacă firma și-a plătit datoriile ulterior. Pentru rigoare, ar trebui să comparăm cu snapshot mai recent (live) ca să excludem firmele care au stins datoriile. - Multe state-owned (HIDROELECTRICA, ROMAERO, COMPLEXUL ENERGETIC HUNEDOARA) — legitimitate parțială (datorii încrucișate stat-stat). Filtru viitor: `EXCEPT companii cu acționar stat majoritar`. - `e.judet` join opțional — `firms.entities` are 100% acoperire CUI privat; unele datornic-i sunt dispărute / radiate. ## Integration points pentru profile pages (viitor) Pe `/achizitii/firma/[cui]` adaugă badge dacă apare în `anaf.datornici_latest`: ```sql SELECT period_label, debt_total, debt_principal, debt_penalty, debtor_category FROM anaf.datornici_latest WHERE cui = $1; ``` UI badge similar cu RegAS / EU funds: - 🚨 Roșu: `debt_total > 1_000_000` (datornic mare) - 🟠 Portocaliu: orice apariție în lista datornici Dacă vrem contrast pozitiv, când avem `anaf.lista_alba` populated: - ✅ Verde: cui în `lista_alba` la cel mai recent trimestru ## Cum re-rulez ingest-ul ```bash # Re-import data.gov.ro Q1-2016 (idempotent, ON CONFLICT DO UPDATE) ssh satra "sudo /opt/vreaudigital/services/seap-scraper/cron/scrape-anaf-datornici.sh" # Doar dry-run (parsează fără DB writes) ssh satra "sudo DRY_RUN=1 /opt/vreaudigital/services/seap-scraper/cron/scrape-anaf-datornici.sh" # Live scrape (NU e implementat — necesită captcha solver): # ssh satra "sudo SOURCE=live /opt/vreaudigital/services/seap-scraper/cron/scrape-anaf-datornici.sh" ``` ## Next steps prioritizate 1. **MVP scoreboard** (1h): adaugă `getAnafDebtStatus(cui)` în profile-queries.ts (după ce Phase 3/4 dau drumul la lib/) + badge pe firma profile. 2. **Recipe** (30 min): adaugă `firmeDatorniceCuContracteSeap` în recipes.ts. 3. **Live scraper cu captcha solver** (3-4h): integrare 2captcha în `scrapeAnafLive()` + cron lunar pentru trimestrul curent. 4. **Backfill istoric** (variabil): dacă găsim arhive (archive.org / partner) ingerăm trimestru-cu-trimestru. Schemă deja suportă (PK = cui+pub_date). 5. **Lista albă scrape**: același endpoint cu CAPTCHA, 100x mai rar lookup (~50-100K firme curate per trimestru). Useful pentru contraste. ## Files - Schema: `services/seap-scraper/sql/025_anaf_datornici.sql` - Scraper: `services/seap-scraper/src/scrape-anaf-datornici.ts` - Cron wrapper: `services/seap-scraper/cron/scrape-anaf-datornici.sh` - This doc: `services/seap-scraper/ANAF-DATORNICI-RECIPES.md`