# ANAF datornici — 2captcha integration handoff Status la **2026-05-12**: codul scraper-ului live e committed și gata de producție, dar **NU rulează încă** — așteaptă două lucruri: 1. `TWOCAPTCHA_KEY` adăugat în Infisical (`/vreaudigital` path). 2. Credit pe contul 2captcha (~$60-100 pentru backfill istoric, apoi ~$15-25/an pentru cron-ul trimestrial). Acest document explică ce e 2captcha, cât costă, cum se setează și cum se activează scraper-ul când ești gata. --- ## De ce 2captcha? Pagina ANAF cu lista datornicilor: > https://www.anaf.ro/anaf/internet/ANAF/asistenta_contribuabili/listele-debitorilor-anaf/ e protejată de **Cloudflare Turnstile** (widget anti-bot care a înlocuit fostul kaptcha PrimeFaces). Submit-ul formularului (selecție trimestru + categorie + descarcă CSV) returnează HTML-ul paginii de challenge dacă token-ul `cf-turnstile-response` lipsește sau e invalid. Turnstile e gândit să fie nesolvabil headless: rulează JS în iframe sandboxed și verifică server-side că browser-ul a executat real heuristici (focus, mouse-move, fingerprint). **Singura cale automată e un solver extern** care delegă rezolvarea unei "human farm" sau ML pipeline cu rate de succes ~80-95%. **2captcha** (sau anti-captcha, capmonster, capsolver — echivalente) e serviciul care: 1. Primește `sitekey` + `pageurl` de la noi via API REST. 2. Returnează un `captcha_id`. 3. Pollăm la fiecare 5s — în 15-45s tipic returnează un token Turnstile valid. 4. Trimitem token-ul la ANAF împreună cu form-ul → CSV descărcat. Costul: **$0.001-0.003 per solve** (variabil cu cererea — Turnstile e ~2-3× mai scump decât reCAPTCHA v2 image). ## Estimare cost ### Backfill istoric (one-shot, opțional dar recomandat) ANAF a publicat datornici trimestrial din 2016-Q1 (Ord. 558/2016). Avem deja T1 2016 în DB (data.gov.ro snapshot). Pentru 2016-Q2 → 2026-Q1, sunt **40 de trimestre × 5 categorii = 200 solve-uri pentru datornici.** Optional: lista albă, +40 solve-uri (1/trim). ``` 200 datornici × $0.003 = $0.60 +40 lista_alba × $0.003 = $0.12 = ~$0.72 worst-case, ~$0.20 typical ($0.001/solve) ``` **Așteaptă** — de ce am zis "$60-100"? Pentru că: - Fiecare CSV export poate fi paginated (PrimeFaces vechi era ~5K rows/page; noul export poate fi single-shot full CSV — necunoscut până testăm). - Re-solveuri necesare dacă token-ul e rejected sau pagina returnează HTML în loc de CSV (re-bootstrap → re-solve). Rate de retry observat pe alte Turnstile-uri: 5-20%. - Worst-case 200 solve-uri × 5-10× retry overhead × $0.003 = ~$3-6 pentru backfill complet. **Buget de siguranță $20** acoperă orice surpriză. **Realist: $5-20 pentru backfill complet, NU $60-100.** Estimarea inițială era prea conservatoare — actualizată după ce am modelat workflow-ul concret. ### Operațiune curentă (ongoing) ``` Cron trimestrial: 4 runs/an × 5 categorii = 20 solve-uri/an + lista_alba (opțional): +4 solve-uri/an = ~24 solve-uri/an × $0.003 = $0.072/an worst-case ``` Cu retry overhead: **$1-5/an.** Practic neglijabil — funcționează ani de zile cu un credit de $20. > **Recomandare:** încarcă $20 inițial. Acoperă backfill + ~3 ani de cron > trimestrial. La $20 rămas <$5, top-up cu încă $20. ## Setup pas-cu-pas ### 1. Creează cont 2captcha 1. Mergi la https://2captcha.com și creează un cont (email + parolă). 2. Confirmă email-ul. 3. Dashboard → **Settings → API Key** → copiază cheia (32 caractere alfanumerice). 4. Dashboard → **Add funds** → încarcă cu card sau crypto (min $1, recomandat $20). Plata via Stripe-like, sosește instant în balance. > Alternative echivalente (același API): anti-captcha.com, capsolver.com, > capmonster.cloud. Toate au cost similar și clienții lor implementează > același endpoint `/in.php` + `/res.php` pattern. Codul nostru e tunat pe > 2captcha — pentru un alt provider, schimbă constantele `TWOCAPTCHA_*_URL`. ### 2. Adaugă `TWOCAPTCHA_KEY` în Infisical (NEW SECRET PROTOCOL) Conform `~/.claude/rules/infra-context.md`: ``` 1. UI Infisical: https://infisical.beletage.ro → Project: vreaudigital (sau cel curent) → Environment: prod → Path: /vreaudigital → Add Secret → Key: TWOCAPTCHA_KEY → Value: → Save ``` Spune-i lui Claude: ``` Adaugă TWOCAPTCHA_KEY în Infisical prod env, path /vreaudigital. Scop: bypass Cloudflare Turnstile pentru scraper-ul ANAF datornici. done ``` Claude rulează: ```bash source ~/Code/claude-dotfiles/require-secret.sh TWOCAPTCHA_KEY ``` Așteaptă exit 0 (cheia e în env). Dacă exit ≠ 0, vezi mesajele scriptului și remediază (typo în Infisical UI, env greșit, path greșit). ### 3. Smoke test offline (zero spend) Înainte de prima rulare cu credit, validează codul: ```bash ssh satra sudo DRY_RUN=1 /opt/vreaudigital/services/seap-scraper/cron/scrape-anaf-datornici-live.sh ``` `DRY_RUN=1` sare peste 2captcha + DB writes, dar parsează plan-ul de trimestre. Output așteptat: ``` RUN plan: quarters=1 (2026Q1..2026Q1) categories=['mari','mijlocii',...] estimated 2captcha solves: 5 (~$0.02 at $0.003/solve) DRY_RUN=1 — skipping network + DB, exiting DONE datornici_rows=0 lista_alba_rows=0 errors=0 ``` ### 4. Prima rulare reală (un trimestru, 5 solve-uri ~$0.02) ```bash ssh satra "sudo systemctl start vreaudigital-anaf-datornici.service" ssh satra "journalctl -u vreaudigital-anaf-datornici.service --since '5 min ago' --no-pager" ``` Verifică: ```bash ssh satra '/tmp/govq.sh "SELECT period_label, debtor_category, COUNT(*), ROUND(SUM(debt_total)/1e6,1) AS mil_ron FROM anaf.datornici WHERE publication_date > '\''2016-12-31'\'' GROUP BY 1,2 ORDER BY 1,2;"' ``` ### 5. Activează timer-ul quarterly ```bash # Copy unit files (din repo către satra): scp services/seap-scraper/systemd/vreaudigital-anaf-datornici.{service,timer} \ satra:/tmp/ ssh satra "sudo cp /tmp/vreaudigital-anaf-datornici.{service,timer} /etc/systemd/system/ && \ sudo systemctl daemon-reload && \ sudo systemctl enable --now vreaudigital-anaf-datornici.timer" # Verifică: ssh satra "systemctl list-timers vreaudigital-anaf-datornici.timer --no-pager" ``` Timer-ul rulează pe **1 Jan / 1 Apr / 1 Jul / 1 Oct la 04:00** (cu un RandomizedDelaySec=1800s ca să evite spike pe 2captcha la oră exactă). ### 6. (Opțional) Backfill istoric — 40 trimestre Doar dacă vrem date 2016-Q2 → present (foarte recomandat pentru recipes red-flag — vezi `ANAF-DATORNICI-RECIPES.md::firmeDatorniceCuContracteSeap`): ```bash ssh satra "sudo BACKFILL_FROM=2016-Q2 INCLUDE_LISTA_ALBA=1 \ /opt/vreaudigital/services/seap-scraper/cron/scrape-anaf-datornici-live.sh" ``` Durată estimată: 200 solve × ~30s/solve = ~1.5-2h. Buget: ~$5-10 worst case. Rulează după ce ai validat prima rulare la pasul 4. --- ## Output așteptat ### `anaf.datornici` - **Pe trimestru**: ~140K rânduri (mari 160 + mijlocii 2K + mici 138K + institutii ~50 + persoane fizice variabil). - **Backfill 2016-Q2 → 2026-Q1**: 40 × 140K = **~5.6M rânduri totale** (compresia repetitivă: aceeași firmă apare în 40 trimestre dacă a fost datornic continuu). - **DB size estimate**: ~2-3 GB (cu indexuri). Schema actuală (`sql/025_anaf_datornici.sql`) e dimensionată pentru asta. - **Recipe ready**: `firmeDatorniceCuContracteSeap` (definit deja în `ANAF-DATORNICI-RECIPES.md`) capătă acoperire completă temporală. ### `anaf.lista_alba` (cu `INCLUDE_LISTA_ALBA=1`) - **Pe trimestru**: ~50-100K rânduri (contribuabili fără datorii — overlap mare quarter-to-quarter, evident). - **Use case**: contrast pozitiv pe profile firme — badge verde "✅ Fără datorii la T_N". --- ## Architecture notes ### Resilience - **Per (category × quarter) try/except** — un fail nu omoară restul trimestrului. - **Re-bootstrap session** după orice eroare → fresh sitekey + cookies (rezolvă cazul "Turnstile cookie expired"). - **Hard cap 180s per solve** (2captcha typical 15-45s, dar uneori spike). - **Idempotent UPSERT** — re-rulare pe același trimestru e safe (UPDATE, nu duplicare). - **Exit code 2** dacă unele trimestre au erori dar restul a mers (partial). Systemd marchează service-ul `failed`, dar timer-ul continuă. ### Secret hygiene - `TWOCAPTCHA_KEY` citit doar din `os.environ.get()`. Nu apare în log-uri. - Wrapper-ul scrie cheia într-un envfile cu `umask 077`, șters după 3s. - `solve_turnstile()` loghează doar primele 8 caractere din sitekey, niciodată cheia 2captcha sau token-ul rezolvat. - Codul **nu pune secrete în URL** (vezi `~/.claude/rules/secret-safety.md`). ### Lista albă: același pattern `ANAF_LISTA_ALBA_PAGE` și `ANAF_LISTA_ALBA_EXPORT_PATH` reflectă endpoint-ul separate `.../listele-debitorilor-anaf/lista_alba/`. Folosește exact aceeași sitekey Turnstile (verificare empirică la prima rulare — fallback: re-extract din pagina aceea separată, codul deja face `AnafSession.bootstrap(page)` per endpoint). ### URL endpoint guesswork — VERIFICĂ la prima rulare Constantele `ANAF_EXPORT_PATH` și `ANAF_LISTA_ALBA_EXPORT_PATH` sunt **best guess** pe pattern observed. La prima rulare reală (pasul 4): 1. Dacă `fetch_export_csv` ridică `RuntimeError("ANAF returned HTML…")`, inspectează manual pagina cu DevTools: - Open https://www.anaf.ro/.../listele-debitorilor-anaf/ - Network tab → submit form → vezi URL-ul real al cererii POST - Update `ANAF_EXPORT_PATH` în `scrapers/anaf_datornici/scraper.py:51` 2. Verifică form-field names — codul trimite `year`, `quarter`, `category`, `cf-turnstile-response`. Numele reale pot fi diferite (ex. `an`, `trim`, `categorie`). Inspectează `
` HTML și actualizează `form` dict-ul în `fetch_export_csv`. Acesta e singurul piece de "interactive validation" — restul codului (parser CSV, DB upsert, plan iteration) e self-contained și testat conceptual. --- ## Defere & known limitations - **JS-rendered widget vs static HTML**: dacă ANAF a mutat sitekey-ul în config JS în loc de `data-sitekey="…"` attribute, regex-ul în `_RE_TURNSTILE_SITEKEY` returnează None și bootstrap-ul aruncă. Fix: inspectează `