Contains everything from `parity/security-hotfix` and `parity/phase-1-admin`.
Both merged with **no conflicts** (`phase-1-admin` already contained
`security-hotfix`, so the merge was a fast-forward). `main` was not touched.
---
## ⚠ Before the real release — do these two things
1.**Remove `OTP_MASTER_CODE` from the environment.** While it is set, **any
active customer can sign in with that one fixed code**. Unset or empty
disables it completely, and the API prints a warning banner at boot for as
long as it is armed. Tracked as **REL-01 (Critical)** in
`docs/parity/ISSUES.md`.
2.**Configure the SMS credentials** (`SMS_TREZ_*`). Until then no customer can
receive a real login code — messages are written to the log instead of sent.
Tracked as **REL-02**.
---
## 1. Environment variables
Names only — no values here, and none are committed. The full annotated list is
in `.env.example`; `docker-compose.yml` passes each one through to the API
container.
### Required
| Variable | Purpose |
|---|---|
| `DATABASE_URL` | PostgreSQL connection string. Inside Docker the host is `db`. |
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Credentials for the bundled `db` service. |
| `JWT_SECRET` | Signs **admin** tokens. Without it the API falls back to a well-known development string — set it. |
| `JWT_CUSTOMER_SECRET` | Signs **customer** tokens. Deliberately separate from `JWT_SECRET` so an admin token can never validate as a customer token. |
Flows walked end to end: admin login, customer creation, sales-expert
create/edit, product and variant create/edit, order list/detail/status change,
wholesale order placement from the admin panel, cash document creation +
confirm + un-confirm; and in **both** customer apps: OTP login with the master
code, product browsing, cart, order placement, order history.
Money paths were checked against the reference's own formulas, not just for a
200: making charge `rash × (1 + (wage − discount)/100)` reproduced exactly, and
the cash-document conversion `price / (price_per_gram / 4.331802)` moved the
customer's gold debt by the expected amount and restored it exactly on
un-confirm.
Every test script deletes only the rows it created; the write test also asserts
the customer's balances end exactly as they started.
---
## 5. Known issues — not done tonight
Nothing here blocks the delivery. Full detail in `docs/parity/ISSUES.md`.
### Missing app pages (not started)
Present in the reference PWA, absent in both apps. Listed in the owner's own
priority order:
| Page | Reference | Notes |
|---|---|---|
| `report` + `report/detail` | `henza_pwa/src/views/report/` | The accounting statement — a store plus two components (`Accounting.vue`, `AccountingDetail.vue`). The largest of the four. |
| `completeProfile` | `.../completeProfile.vue` | No trace in the target — no page and no route. |
| **Sales-expert app login (D21)** | ✅ Done. An expert signs into both apps with their AdminUser account and the ordinary OTP flow; the master code works for them too. They get the whole app — browsing, cart, ordering, order history — plus their own two screens. |
| **«سفارش مشتریان» (ChildOrder)** | ✅ Done in both apps. Their referred customers' orders at «تحویل مسئول فروش», searchable, infinite-scrolled. |
| **«مشتریان من» (customerChoice)** | ✅ Done in both apps. Their referred customers, searchable, with a read-only detail sheet. |
| **APP-02 — admin home slider in v2** | ✅ Fixed. v2 now prefers admin-configured slides and keeps its category-derived hero as the fallback. |
An expert's own order lands on their **own trading account**, with the making
charge from `AdminUser.wagePercent` and the credit limit from
`SalesExpertProfile.gramLevel` — the same two numbers the reference read off
the agent's own customer row. See D21.
**Two reference behaviours were deliberately not ported**, both money/session
risks and both recorded in D21:
1.`myChild` minted an `api_token` per listed customer so the agent could act
**as** them — a session-hijack primitive.
2. The child-order edit drawer (`PUT /order/child/{id}`) recalculated an
order's weights and prices from client-sent values. The card opens a
| **APP-02** | `user-app-v2` has no `getHomeSlider`, so slides configured in the admin panel appear in **user-app only**; v2 always derives its hero from categories. Not fixed because v2's home is its own design and rewiring it during a delivery risks the page being demonstrated. |
| **PERS-02** | The reference's cities are a province → city **tree** (provinces not selectable); the target flattened this to a `province` string and a flat select on all three Persons forms. The city itself is still captured correctly. |
| **PERS-03** | The reference collects a Jalali **birth date** on the customer forms; the target has no column for it. |
| **PERS-01** | The reference makes a customer's **mobile immutable on edit**; the target allows editing it. Owner ruled this NOT ported — the target keeps its current behaviour. |
| **PERF-01** | `listReagents()` runs one aggregate per row (N+1) for the per-agent sales total. Harmless at the current agent count. |
### Parity work not reached
Phase 1 was not completed: the field-by-field audit stopped after the Persons
section, and products/categories/variants, financial documents, users and
permissions, SMS templates, promotions, cities, branches, workshops and reports
have had only the breakage scan above, not a full parity audit. The
**Phase 1 gate was lifted for tonight only** (D19) and still stands as the next
milestone.
`PERM-01` (the database holds 178 distinct permission ids against the UI's 174)
is still awaiting a decision, because resolving it means deleting rows.
---
## 6. Decisions that shaped this build
Recorded in `docs/parity/DECISIONS.md`:
- **D13** — the retail storefront (`single*`, wallet, comments, `customer.php`,
`site.php`) is **preserved but out of scope**. Nothing there was changed.
- **D19** — delivery mode: implement the main gaps, do not stop to ask, choose
the lowest-risk option, change nothing protected.
- **D20** — SMS can never block a boot or an operation; `OTP_MASTER_CODE` for