jey_wizard/CLAUDE.md

7.9 KiB

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_tokenchooseTaxpayer 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.enqueues 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

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)

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_migratesetup_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_messagesget_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/.