Files
tehran_tala/DELIVERY.md
T
s.esmaeiliandClaude Opus 5 f71fd5bfe1 feat(apps): sales-expert screens in both apps + v2 admin home slider (D21, APP-02)
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>
2026-09-23 01:12:53 +03:30

10 KiB
Raw Blame History

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

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:

  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.