diff --git a/.etaxes_notes/cabinet_internals.md b/.etaxes_notes/cabinet_internals.md deleted file mode 100644 index 5c3079c..0000000 --- a/.etaxes_notes/cabinet_internals.md +++ /dev/null @@ -1,533 +0,0 @@ -# e-taxes.gov.az Cabinet Internals — Operational Notes - -Persistent notes for future Claude sessions about the ГНС cabinet (new.e-taxes.gov.az). -Add to this file when you learn something non-obvious. Keep entries dated. - ---- - -## High-level architecture (verified 2026-05-26) - -Two separate SPAs under one domain: - -| Path prefix | Mount point | Purpose | webpackJsonp name | -|---|---|---|---| -| `/etaxes/` | `
` | Public services (lookup taxpayer, debt query, etc.) | `webpackJsonpetaxes` | -| `/eportal/` | `
` | **Cabinet for authenticated users — declarations live here** | (varies) | - -Both: React + Redux Toolkit + RTK Query, CRA-ish bundle layout (`static/js/N.HASH.chunk.js`). -Source maps NOT exposed. - -**Pitfall:** the cabinet's mount point is `#app`, NOT `#root`. A snippet that walks fibers from `#root` will return `hasRoot: false`. - ---- - -## Auth — how the cabinet checks login - -REST (Python `requests`) — **simple**: send `x-authorization: Bearer ` header. Works for every documented endpoint. No cookies needed, no session. - -Browser SPA — **NOT** simple. Just injecting `x-authorization` into outgoing requests via Playwright's `route` is **NOT enough**. The SPA has a route guard that checks **Redux state** for the auth token. If Redux is empty → redirect to `/eportal/login`, regardless of what's in HTTP headers. - -Verified storage usage (grepped eportal bundle): -- `localStorage` — only `surveyModal` (popup tracking) and `aztax-lang`. **No auth token**. -- `sessionStorage` — empty after fresh page load. -- Cookies — **none set** by any cabinet endpoint (verified `Set-Cookie` headers: empty on all responses including `/chooseTaxpayer`). - -**Implication:** to drive the cabinet headlessly you must EITHER: -1. Login through the SPA UI (PIN1 push to phone), OR -2. Inject the token into the Redux store via `page.evaluate()` after the SPA mounts. Action type is dynamically constructed in Redux Toolkit — grep `*-api`, `Slice/` patterns to find it. Untested as of 2026-05-26. - -Easier in practice: UI login + Playwright's `storage_state` to persist cookies+localStorage between runs (note: storage_state is mostly empty here, so reuse value is limited — the actual auth lives in in-memory Redux, which Playwright cannot serialize). - -### REST ASAN flow (works without browser) - -``` -POST /api/po/auth/public/v1/asanImza/start body={"phone": "+994...", "userId": "..."} - → returns 200 with bearer_token in x-authorization response header - → response body: {"verificationCode": "1234"} (shown on user's phone screen) - -POLL GET /api/po/auth/public/v1/asanImza/status headers={x-authorization: Bearer } - → returns {"successful": false, "error": null} while waiting - → when user approves PIN1, eventually returns successful: true (or just becomes silently true) - → BETTER: poll /asanImza/certificates instead — when it returns 200 with non-empty list, you're authed - -GET /api/po/auth/public/v1/asanImza/certificates - → returns [{taxpayerType: "individual"|"legal", legalInfo|individualInfo, ...}, ...] - -POST /api/po/auth/public/v1/asanImza/chooseTaxpayer body={"ownerType": "legal", "legalTin": "VOEN"} - → returns 200 with main_token in x-authorization response header - → main_token JWT validity: 10 min (exp = iat + 600s) -``` - -**Pitfall — polling format:** my first explorer.py polled `data.get("status")` but the actual field is `data.get("successful")` (bool). Wrong field name → silent infinite loop. Now using "did certificates endpoint return non-empty data?" as success signal — more robust. - -**Pitfall — ГНС polling latency:** user approves PIN1 quickly, but ГНС server takes several seconds (5-30s observed) to flip `successful: true`. Use timeout >= 90s with 2s interval. - -### Renewing tokens - -`RENEW_URL = "https://new.e-taxes.gov.az/api/po/auth/public/v1/renew"` — used by `taxes_az/auth.py:renew_token`. Returns 401 if token expired without recent activity. There's an `ACTIVITY_TIMEOUT_MINUTES = 10` gate in the project code. - ---- - -## Login form (UI flow, /eportal/az/login) - -Initial page: `/eportal/az/login` — shows 5 method tiles. Click "Asan İmza" link (`` with text "Asan İmza", class `login-item-link`) → SPA navigates to `/eportal/az/login/asan` with form. - -ASAN form selectors (verified 2026-05-26): -| Selector | Field | Notes | -|---|---|---| -| `#phone` | Phone (type=text) | placeholder `00 000 00 00` — enter LOCAL format only (e.g. `558239674`), **without** +994 | -| `#userId` | User ID (type=password) | The Asan İmza user ID, e.g. `171082` | -| `#loginPageSignInButton` | Submit (type=submit) | Text: `Daxil ol` | - -Page also has a language `