Files
vreau-digital/services/seap-scraper/HANDOFF-anaf-datornici-2captcha.md
Claude VM a6c03a091e 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)
2026-05-13 00:10:32 +03:00

12 KiB
Raw Permalink Blame History

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: <cheia 2captcha>
   → 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ă:

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:

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)

ssh satra "sudo systemctl start vreaudigital-anaf-datornici.service"
ssh satra "journalctl -u vreaudigital-anaf-datornici.service --since '5 min ago' --no-pager"

Verifică:

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

# 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):

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:
  2. Verifică form-field names — codul trimite year, quarter, category, cf-turnstile-response. Numele reale pot fi diferite (ex. an, trim, categorie). Inspectează <form> 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ă <script> blocks, extragetimer-vector cu un al doilea regex.
  • Pagination: dacă export-ul CSV e paginat (nu single-shot), trebuie loop suplimentar — codul curent presupune un single CSV per (category, quarter). Verifică la prima rulare cu un trimestru recent.
  • Backfill historic depinde de ANAF: ANAF s-ar putea să nu mai expună arhive vechi prin același endpoint (au păstrat doar trimestrul curent în trecut). Dacă fetch_export_csv returnează 0 rânduri pentru trimestre vechi, alternativa e archive.org (manual download).
  • PDF lista albă: la un moment dat ANAF a publicat lista albă ca PDF (nu CSV). Dacă endpoint-ul returnează Content-Type: application/pdf, parser-ul trebuie extins cu pdftotext (vezi pattern din scrape-cnas.ts).

Files

  • Scraper: services/seap-scraper/scrapers/anaf_datornici/scraper.py (Python 3.12)
  • Wrapper: services/seap-scraper/cron/scrape-anaf-datornici-live.sh
  • Systemd: services/seap-scraper/systemd/vreaudigital-anaf-datornici.{service,timer}
  • Schema: services/seap-scraper/sql/025_anaf_datornici.sql (deja aplicată)
  • Old TS importer (data.gov.ro Q1-2016): services/seap-scraper/src/scrape-anaf-datornici.ts
  • Old wrapper: services/seap-scraper/cron/scrape-anaf-datornici.sh (data.gov.ro)
  • Recipes: services/seap-scraper/ANAF-DATORNICI-RECIPES.md

Activation checklist

  • Add TWOCAPTCHA_KEY to Infisical (/vreaudigital, prod env)
  • Confirm: source ~/Code/claude-dotfiles/require-secret.sh TWOCAPTCHA_KEY exits 0
  • Fund 2captcha account ($20 recommended)
  • Dry-run smoke test: sudo DRY_RUN=1 .../scrape-anaf-datornici-live.sh
  • First real run (1 quarter, ~$0.02): sudo systemctl start vreaudigital-anaf-datornici.service
  • Verify rows in anaf.datornici for the new quarter
  • Verify endpoint URLs and form field names if first run failed (see "URL endpoint guesswork")
  • Enable timer: sudo systemctl enable --now vreaudigital-anaf-datornici.timer
  • (Optional) Run backfill: sudo BACKFILL_FROM=2016-Q2 INCLUDE_LISTA_ALBA=1 .../scrape-anaf-datornici-live.sh