jey_wizard/CLAUDE.md

90 lines
7.9 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this app does
`jey_wizard` replaces Frappe's stock setup wizard with a fixed flow tailored for Azerbaijani tenants. The flow is hard-wired to: `language → asan → employees → company → confirm`. Steps past Asan Imza authentication assume the company is Azerbaijani (country/currency/timezone/CoA are hardcoded in `build_setup_args`) and that the installer wants e-taxes + ƏMAS data pulled and materialised as part of setup.
Depends on **invoice_az**, declared in `hooks.py:required_apps`. The wizard does not re-implement e-taxes or ƏMAS integration — it thinly wraps `invoice_az.auth.*`, `invoice_az.amas_api.*` and `invoice_az.company_api.*`. Read those modules first before touching the flow.
## Architecture
### Stock wizard override (one big trick)
`hooks.py:setup_wizard_requires` loads `jey_wizard/public/js/jey_setup.js` inside `/app/setup-wizard`. At the top of that file we **overwrite `frappe.setup.SetupWizard`** (the class) and wipe `frappe.setup.slides` / `slides_settings` / `events` so ERPNext's default `before_load`/`after_load` hooks don't mutate a wizard that is no longer slide-based. Frappe's page handler then calls `new frappe.setup.SetupWizard(wizard_settings)` and gets our class instead. There is no slide system — we drive navigation manually with a `current_step` index and per-step sub-state machines (`asan_state`, `amas_state`).
On finalize we call the unmodified `frappe.desk.page.setup_wizard.setup_wizard.setup_complete` with a full args dict (see `build_setup_args` in `jey_setup.js`). Staying on that codepath keeps typing_validations / sanitize_input / setup_complete hooks working.
### Two authentication systems, two phone taps
e-taxes and ƏMAS are **separate** auth flows — this is not a limitation of our code, it's how the Azerbaijani services work:
- **e-taxes (Asan Imza direct)**: `asanImza/start` → SMS → user tap → `bearer_token``chooseTaxpayer` consumes bearer, returns `main_token`. The `bearer_token` is **single-use per chooseTaxpayer call**. Switching taxpayer requires another phone tap (`auth.py:526` returns 401 otherwise). `invoice_az.client.etaxes_common.js` follows the same pattern.
- **ƏMAS (via MyGovID)**: `asanSignLogin` → SMS → user tap → `mygovid_token` (JWT) → OAuth code exchange → cookie session. AMAS requires a fresh CSRF token before every request; `invoice_az.amas_api.make_amas_request` handles that.
The UI warns about the second tap on the employees step intro. Do not try to unify the two — there is no e-taxes MyGovID OAuth path in the current code.
### Cache doctype: `Jey Wizard Etaxes Cache`
Single doctype that carries state between wizard steps and `setup_wizard_complete`:
- `objects_json` / `cash_registers_json` / `pos_terminals_json` / `bank_accounts_json` / `presented_certs_json` — written by `etaxes.py:fetch_all_etaxes` during the Asan step, **not read back** on finalize (see next point).
- `amas_selected_employees_json` — written by `amas.py:cache_selected_employees`, read by `etaxes.py:_materialize_amas_employees` in the `setup_wizard_complete` hook.
- `last_summary` / `last_error` / `fetched_at` — displayed on the Asan done screen.
Schema changes to this doctype require `bench migrate`.
### Two-phase e-taxes materialisation
During Asan step we fetch the 6 e-taxes lists into the cache doctype. On `setup_wizard_complete` (`etaxes.py:materialize_after_setup`) we call `invoice_az.company_api.load_company_*` — these **re-fetch** from e-taxes and write to real DocTypes (`E-Taxes Object`, `E-Taxes Cash Register`, …) linked to the freshly-created Company. The mid-wizard cache exists so future wizard steps can consume the data before Company exists; the loaders are invoice_az's authoritative path. Don't try to read from the cache on finalize.
AMAS employees go through `invoice_az.amas_api.import_bulk_employees` which `frappe.enqueue`s a background job with realtime progress (events `amas_import_progress` / `amas_import_complete`). The wizard fires-and-forgets — the user lands in `/app` while employees continue importing.
### "Wrong company? Change" rollback
Clicking rollback on asan/done calls `api.py:reset_company_selection` which wipes `bearer_token`, `main_token`, `certificates_json`, `selected_certificate*`, `verification_code`, sets `auth_status="Not Authenticated"`, and empties the cache doctype. The JS then calls `trigger_asan_authentication` again, which sends a fresh SMS. Re-using the consumed bearer_token is what caused the historical 401 "Authentication required" bug — do not skip the backend reset.
## Common commands
### From any bench site
```bash
bench --site <site> migrate # pick up schema changes to Jey Wizard Etaxes Cache
bench build --app jey_wizard # rebuild /assets/jey_wizard/js/jey_setup.js
bench restart # reload Python changes
```
### Pre-commit (from `apps/jey_wizard`)
```bash
pre-commit install
pre-commit run --all-files
```
Configured tools: `ruff`, `eslint`, `prettier`, `pyupgrade`.
### Version badge
`jey_wizard/__init__.py:__version__` and `JEY_WIZARD_VERSION` in `jey_setup.js` are kept in sync and bumped on every commit that changes wizard code. The badge in the top-right of the wizard UI reads this string — it's how you tell at a glance which revision is actually loaded on a given site.
## Translations
Translations live in **gettext PO files** under `jey_wizard/locale/` (`main.pot` is the extracted template; `az.po` / `ru.po` carry the strings). The legacy `translations/*.csv` are gone — Frappe v16 still reads CSV, but PO/MO is the modern path and overrides it.
**Source of truth is the `az_locale` app, not this repo.** `az_locale/locale/translations_az/jey_wizard/az.po` and `translations_ru/jey_wizard/ru.po` are the masters. On every `bench migrate`, `az_locale`'s `after_migrate``setup_locale()` **copies those over `jey_wizard/locale/*.po` and force-compiles** the runtime `.mo` into `sites/assets/locale/<lang>/LC_MESSAGES/jey_wizard.mo`. So: edit the PO in `az_locale` (or here, then mirror to `az_locale`), never hand-edit the `.mo`. The copies committed in this repo are kept in sync but will be clobbered by `az_locale` on the next migrate.
Workflow to add/refresh strings:
1. `bench generate-pot-file --app jey_wizard` — re-extract `__()` (JS) + `_()` (Python) + DocType labels into `locale/main.pot`.
2. `bench update-po-files --app jey_wizard` — fold new msgids into `az.po` / `ru.po` (empty msgstr = falls back to the English source; fine for proper nouns like `IBAN`, `ƏMAS`, `Stores`).
3. Fill the empty `msgstr`, copy both PO into `az_locale`, then `bench migrate` (or run `az_locale.locale.setup_locale.setup_locale`) to compile + activate. `bench clear-cache` to drop the `merged_translations` Redis cache.
Switching language on step 1 calls `frappe.desk.page.setup_wizard.setup_wizard.load_messages``get_messages_for_boot()``get_all_translations(lang)`, which merges every app's CSV **and** MO, so the new PO strings render after compile. The constructor does this automatically on boot if `frappe.boot.lang` differs from the default (`az`).
## Gotchas
- **Admin-only.** `_only_admin()` in `api.py` / `etaxes.py` / `amas.py` throws unless `frappe.session.user == "Administrator"`. The wizard is not designed for any other user.
- **Phone input.** UI collects a 2-digit carrier prefix (values `50/51/10/55/60/70/77/99`) + 7-digit local number. On submit we assemble `+994{prefix}{local}`. Don't strip the `+` — MyGovID accepts it; invoice_az stores it verbatim.
- **Back navigation.** Within a sub-state machine, Back rewinds to intro/input first; only Back from intro/done crosses a wizard-step boundary. See `prev()` in `jey_setup.js`.
- **Placement of JS.** `public/js/jey_setup.js` is served raw via `setup_wizard_requires`. There is no bundler; `bench build` just symlinks `public/` to `sites/assets/jey_wizard/`.