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>
10 KiB
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
- Remove
OTP_MASTER_CODEfrom 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) indocs/parity/ISSUES.md. - 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
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
pnpm build # all workspaces
pnpm build:release # the production/Docker build
Deploy (Docker)
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:
myChildminted anapi_tokenper listed customer so the agent could act as them — a session-hijack primitive.- 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_CODEfor the delivery. - D11 is cancelled — superseded by D13.