# 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` | `/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. | | `ChildOrders` | `henza_pwa/src/views/user/src/components/ChildOrders.vue` | A referred customer's orders. | | `customerChoice` | `.../customerChoice.vue` | Acting-as / customer picker. | | `completeProfile` | `.../completeProfile.vue` | No trace in the target — no page and no route. | ### 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.