# 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 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//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/`.