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)
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user