Files
vreau-digital/services/seap-scraper/HANDOFF-anaf-datornici-2captcha.md
T
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

301 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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ă:
```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ă `<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`