diff --git a/.etaxes_notes/cabinet_internals.md b/.etaxes_notes/cabinet_internals.md new file mode 100644 index 0000000..5c3079c --- /dev/null +++ b/.etaxes_notes/cabinet_internals.md @@ -0,0 +1,533 @@ +# 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 `