215 lines
9.0 KiB
Markdown
215 lines
9.0 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. |
|
|||
|
|
| `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.
|