Both expert screens are built in user-app and user-app-v2, with byte-identical
template and script and a per-app style block only — verified by diffing
everything above <style>:
ChildOrdersView "سفارش مشتریان" — legacy ChildOrders.vue. Keeps the
reference's title, search, infinite paging, and per-card item count, total
weight, customer name and the fixed «تحویل مسئول فروش» chip (the endpoint
only returns that status, ApiMobileController:766).
CustomerChoiceView "لیست مشتریان" — legacy customerChoice.vue. Keeps its
title, per-search re-query and the reference's choice to LIST inactive
customers (its active() scope is commented out at :187).
Reachable from inside the app, not just by URL: routes carry the reference's own
paths and names, the dashboard grows the two entries only for an expert (v1 as
tiles, v2 as its own rows), and a router guard sends an ordinary customer back
to their dashboard. None of that is relied on for safety — the server scopes
both endpoints on the AdminUser id in the token and answers 403 otherwise.
Not ported, both recorded in D21: the act-as api_token minting (:197-201) and
the child-order edit drawer, which recomputed an order's money from client-sent
values. The card opens a read-only summary instead.
APP-02 fixed: user-app-v2 gained getHomeSlider and now prefers admin-configured
slides, keeping its category-derived hero as the fallback and its own design.
Session stores stay byte-identical across the apps.
build 5/5, jest 45/45, expert login 36/36, apps smoke 43/43, typecheck clean.
[ref: henza_pwa/src/views/user/src/components/ChildOrders.vue, customerChoice.vue]
[ref: henza_pwa/src/router/index.ts:171-188]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
235 lines
10 KiB
Markdown
235 lines
10 KiB
Markdown
# DELIVERY — gold-banking-monorepo
|
||
|
||
Branch: **`release/delivery`** · Prepared 2026-09-23
|
||
|
||
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. |
|
||
|
||
### Optional
|
||
|
||
| Variable | Default | Purpose |
|
||
|---|---|---|
|
||
| `PORT` / `API_PORT` | `3000` | API listen port. |
|
||
| `JWT_EXPIRES_IN` | `12h` | Admin token lifetime. |
|
||
| `JWT_CUSTOMER_EXPIRES_IN` | `30d` | Customer token lifetime. |
|
||
| `UPLOAD_DIR` | `<api>/storage/uploads` | Where uploaded files are written. |
|
||
| `ADMIN_PANEL_PORT`, `USER_APP_PORT`, `USER_APP_V2_PORT` | 5180–5182 | Dev servers only. |
|
||
| `RUN_MIGRATIONS` | `true` | Set `false` to skip the boot-time migration run. |
|
||
| `RUN_SEED` | `true` | Set `false` to skip seeding. |
|
||
|
||
### SMS — all optional; blank means "log, do not send"
|
||
|
||
`SMS_PROVIDER` (`trez` default, or `none`), `SMS_TREZ_URL`,
|
||
`SMS_TREZ_USERNAME`, `SMS_TREZ_PASSWORD`, `SMS_TREZ_SENDER_LINE`.
|
||
|
||
With these unset the server **still starts normally** and logs a warning; a
|
||
failed or skipped SMS never blocks an operation, including customer creation.
|
||
|
||
### ⚠ `OTP_MASTER_CODE` — delivery only
|
||
|
||
A fixed OTP accepted for **any active customer**, alongside the real code.
|
||
|
||
- Read only from the environment; never hardcoded.
|
||
- Unset or empty → completely disabled.
|
||
- **Must be exactly 4 digits.** Both apps' login box is four single-character
|
||
inputs and only submits at four, so a longer code works over the API but
|
||
cannot be typed into either app.
|
||
- Every login through it is logged with the customer's mobile.
|
||
- The real OTP keeps working unchanged while it is armed.
|
||
|
||
---
|
||
|
||
## 2. Running it
|
||
|
||
### Development
|
||
|
||
```bash
|
||
pnpm install
|
||
pnpm dev # all apps via turbo
|
||
```
|
||
|
||
Ports come from the repo-root `.env` (single source of truth):
|
||
API 3000, admin-panel 5180, user-app 5181, user-app-v2 5182.
|
||
|
||
### Build
|
||
|
||
```bash
|
||
pnpm build # all workspaces
|
||
pnpm build:release # the production/Docker build
|
||
```
|
||
|
||
### Deploy (Docker)
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Services: `db` (Postgres 15), `api` (port 3000), `admin-panel`, `user-app`,
|
||
`user-app-v2` (each nginx on port 80). The apps expose no host ports —
|
||
Dokploy/Traefik routes to them by domain. Uploads persist in the `api_uploads`
|
||
volume; without it every previously uploaded image 404s after a redeploy.
|
||
|
||
---
|
||
|
||
## 3. How migrations run
|
||
|
||
**Automatically, inside the API container, at boot.** No manual step.
|
||
|
||
`src/prisma/bootstrap.ts` calls `runMigrations()` before Nest starts. That
|
||
runner (`src/prisma/migrate.ts`) applies the `.sql` files from
|
||
`prisma/migrations` and records each one in its own tracking table,
|
||
**`_goldbank_migrations`**, so every file runs exactly once.
|
||
|
||
It exists because **the deploy image is copy-only and ships no Prisma CLI**, so
|
||
`prisma migrate deploy` is not available there. This is why the database has two
|
||
migration ledgers — `_prisma_migrations` (written by the Prisma CLI in
|
||
development) and `_goldbank_migrations` (written by the runtime runner). Both
|
||
are intentional. `_goldbank_migrations` is declared in `schema.prisma` with
|
||
`@@ignore` so drift checks do not propose dropping it.
|
||
|
||
**Never** run `prisma migrate reset`, and never edit either table by hand.
|
||
In development, migrations are created with `prisma migrate dev`; in production
|
||
the container runner applies them.
|
||
|
||
Seeding runs once after migrations, gated on a marker row, and is skipped when
|
||
no seed script is present.
|
||
|
||
---
|
||
|
||
## 4. What was verified on this branch
|
||
|
||
All green on `release/delivery`:
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| `pnpm build` (5 workspaces) | ✅ all pass |
|
||
| jest (api unit) | **45/45** |
|
||
| `smoke-admin.cjs` — admin flows | **43/43** |
|
||
| `smoke-apps.cjs` — customer flows, both apps | **43/43** |
|
||
| `scan-admin-b2b.cjs` — every B2B read endpoint | **34 ok, 0 broken** |
|
||
| `smoke-admin-b2b-writes.cjs` — order + document writes | **25/25** |
|
||
| `auth-smoke.sh` — permissions/401/403 | **32/32** |
|
||
| `test-sales-expert-roundtrip.cjs` | **33/33** |
|
||
| `test-name-trigger.cjs` | **9/9** |
|
||
| typecheck (api, admin-panel, user-app, user-app-v2) | ✅ clean |
|
||
|
||
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. |
|
||
|
||
### Delivered tonight
|
||
|
||
| Feature | State |
|
||
|---|---|
|
||
| **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
|
||
read-only summary instead.
|
||
|
||
### Behavioural gaps
|
||
|
||
| ID | What |
|
||
|---|---|
|
||
| **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
|
||
the delivery.
|
||
- **D11 is cancelled** — superseded by D13.
|