Reported: from the profile page, going to login and pressing back landed
on home instead of the profile — with the instruction to audit the whole
structure rather than that one case. Seven defects, four reachable in
ordinary use.
NAVIGATION
PageBar's back arrow pushed a FIXED route name (`backTo`) and ignored
history, so it could not know where the person came from. That was true
of 14 of the 15 PageBar uses in each app — only SearchView used
router.back() — so it was never one screen's bug. Inherited from the
reference, which pushed a fixed name in every component's back arrow;
not replicated (D4).
`backTo` is now a fallback, used only when there is nothing to go back
to (a shared link, a refresh). All 14 declarations are kept untouched,
because that is exactly when they are right.
Answering "is there anywhere to go back to?" needs care: history.length
counts entries from before the app was opened, so a link opened in a
fresh tab would look navigable and back would walk the person out of the
app. navStore records the history `position` of the app's first
navigation and compares the current one against it — direction-proof,
unlike a counter, which only grows and would be wrong after one
back-and-forward. Confirmed vue-router writes `position` on every entry
rather than assuming it.
Three pushes became replaces, each of which left behind a page the
guards immediately bounce, making "back" a no-op or a trip home:
- after sign-in the login screen stayed in history — the worst, since
the same file already honoured ?redirect=, so signing in and
pressing back disagreed about where the person belonged;
- the 401 handler kept the page whose request had just failed;
- complete-profile kept a form that is unreachable once the documents
are in.
CartView's post-order push is left alone: back lands on the emptied
cart, which is a real screen, not a dead end.
READ FRESHNESS
Owner's rule: fetch what a screen needs when it is opened, rather than
caching with a lifetime. Better than the TTL I had proposed — a TTL
guesses how long data stays true, while entering the screen is when it
actually matters.
OrdersView was the worst: it is in the keep-alive list but only loaded
in onMounted, so the second and every later visit made no request at
all, and the first read the cache. Order status changes on the admin
side and nothing here invalidates it, so the list could be stale for a
whole session. Ledger entries had the same shape and a stronger reason —
they are written by the accounting side, which this app can never learn
about. The gold price was forced only by pull-to-refresh on the two
screens that show it most prominently.
The catalogue is deliberately NOT re-read, by the owner's own reasoning
that this must not add server load: it is the entire product list in one
response, it changes when an admin edits it, and pull-to-refresh and
placing an order already invalidate it.
The cache layer itself is sound — concurrent callers share one in-flight
promise, and a response arriving after an invalidate is returned but not
stored, so a write cannot lose a race against a read in flight. Its one
real gap was that nothing ever went stale. The policy is now written at
the top of queryCache.ts so a TTL is not re-introduced.
Both apps vue-tsc clean. Not click-tested in a browser; the back-button
behaviour is worth one manual pass.
اپ: دکمه بازگشت به صفحه قبلی واقعی برمیگردد و اطلاعات هر صفحه در هر بازدید بهروز میشود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
331 lines
22 KiB
Markdown
331 lines
22 KiB
Markdown
# PROGRESS — Reference Parity Project
|
||
|
||
> Read this file FIRST at the start of every session and after every `/compact`.
|
||
> Rules live in `CLAUDE.md` (workspace root). Method lives in the `reference-parity-audit` skill.
|
||
> Neither is in this repository — see `docs/GO-LIVE-PLAN.md` §0.
|
||
|
||
## New workstreams (2026-10-04) — plans only, nothing executed
|
||
|
||
| Plan | What | Waiting on |
|
||
|---|---|---|
|
||
| `docs/GO-LIVE-PLAN.md` | Stack upgrade, release isolation, data migration and cutover from the live henza system. **§0 explains how to continue on another machine.** | Owner go-ahead per step |
|
||
| `docs/PWA-PORT-PLAN.md` | Third user app `apps/user-app-classic`: a pixel-exact port of `henza_pwa@b171fff` onto the new API. | Owner rulings on its tensions 1–5 and Q1–Q9 |
|
||
| `docs/ACCOUNTING-INTEGRATION-PLAN.md` | Tahesab / Zarrin integration, switchable in the admin panel (D27). **Implemented 2026-10-05**, off until switched on; progress in its §10. | Zarrin trial on a real install (ACC-01); ACC-03 ruling |
|
||
| `docs/DATA-MIGRATION-MAP.md` | Column-by-column map of the live henza MySQL schema (`oapi-struncture.sql`, 80 tables) onto Prisma, checked against real row counts via the read-only `docs/migration-precheck*.sql`. Branch `migration/legacy-data`. **2026-10-07:** additive prep migration applied (nullable `nationalCode`, `birthDate`, rejection trail + `REJECTED`, `CustomerFavorite`), bcrypt guard, REJECTED consumer audit. ⚠ On this machine a file in `prisma/migrations` is applied by the watch-mode dev server on the next `src/` edit — see its §10.5.5. | `precheck-2` (where the ledger amount lives) and `precheck-3` output; two `verify()` rulings |
|
||
|
||
The "Current state" table below is the parity project's and was last updated 2026-09-22.
|
||
|
||
## Current state
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| **Phase** | **Phase 2 — apps parity — items #1 and #2 done** (Phase 1 closed, `83e5d28`) |
|
||
| **Branch** | `main` (the parity branches were merged; see git log 2026-10-08..10-12) |
|
||
| **Status** | Phase 1: all in-scope sections closed (§2 §4 §5 §6 §7 §8 §9 ✅ · §3 ⏸️ by D16). Phase 2: **#1 MON-08 ✅**, **#2 D3 ✅**, app-login error reporting (**AUTH-01/02/03**, **DOC-01**), **«ورود به حساب مشتری» restored** (**EXP-01/02/03**, D29 — supersedes D21's omission), and the apps' back-navigation + read-freshness audit (**NAV-01…04**, **READ-01…03**). **308/308** jest tests; both apps `vue-tsc` clean. |
|
||
| **Next step** | Build **`02-APPS-MATRIX.md`** — the field-by-field apps matrix (D12), which Phase 2 has been running without. Then the remaining app rows it turns up. |
|
||
| **Blocked on** | nothing. Awaiting an owner ruling when convenient: **PERM-02**. Deferred by the owner: **ANLY-01** (route analytics, lowest priority). |
|
||
| **Last updated** | 2026-10-12 |
|
||
|
||
### Phase 1 close-out (2026-10-12)
|
||
|
||
The two items that were still genuinely unfinished:
|
||
|
||
| Item | Result |
|
||
|---|---|
|
||
| §2 — the D1 rid matrix (stood as *(pending)* all phase) | ✅ filled: 163 rid-guarded routes, 68 distinct rids, 176 tree keys, mapped per module. 66/68 grantable from the UI; the 2 that are not are **PERM-02**. |
|
||
| Order-placement SMS | ✅ **SMS-03** — the matrix had logged one missing message; in fact all four were missing and `OrdersService` had no `SmsService`. Fixed with 6 tests. |
|
||
|
||
**What is NOT done, by decision rather than by omission:**
|
||
|
||
| | Why |
|
||
|---|---|
|
||
| §3 — product price display (PRICE-01) | ⏸️ retail storefront, out of scope by **D13/D16**. `variant-price.ts` + 11 tests kept but unused. |
|
||
| Firebase push beside each SMS (PUSH-01) | ⏸️ deferred to the PWA port. |
|
||
| «سفارش ویرایش شد» admin SMS | «عمداً تکرار نشد» (D4) — no target path edits a placed order. |
|
||
| PERM-02 (2 ungrantable rids) | additive, changes the permission vocabulary → awaits the owner, as PERM-01 did. |
|
||
| `02-APPS-MATRIX.md` | Phase 2's file; not created yet. |
|
||
|
||
⚠️ **Not verified in a browser.** Every ✅ above rests on reading both sides,
|
||
unit tests and smoke scripts. The prior port's note still stands: no admin view
|
||
has been visually checked in a browser. A full click-through is the honest
|
||
remaining risk on Phase 1.
|
||
|
||
### Security hotfix — final result
|
||
|
||
| Route | Was | Now |
|
||
|---|---|---|
|
||
| `GET accounting/customers/:id/balance` | public | admin, rid `wallet.list` |
|
||
| `POST accounting/customers/:id/reconcile` | public **write** | admin, rid `wallet.charge` |
|
||
| `POST accounting/repayments/gold` | public **write** | admin, rid `document.create` |
|
||
| `POST accounting/repayments/rial` | public **write** | admin, rid `document.create` |
|
||
| `POST accounting/entries/:id/confirm` | public **write** | admin, rid `document.list.confirm` |
|
||
| `PATCH orders/:id/status` | public **write** | admin, rid `order.list.sts.submit` |
|
||
| `GET customers` | every customer, 18 fields | **route deleted** |
|
||
| `GET customers/:id` | public | **route deleted** → `GET customer/me` (token) |
|
||
| `GET customers/:id/orders` | public | **route deleted** → `GET customer/me/orders` (token) |
|
||
| `GET customers/:id/ledger-entries` | public | **route deleted** → `GET customer/me/ledger-entries` (token) |
|
||
| — | — | **new** `POST customer/auth/request-otp` · `verify-otp` |
|
||
|
||
Still public by design: `catalog/products`, `pricing/latest`, `home/slider`,
|
||
`uploads/:filename` (ruling T2), and `POST /orders` + `GET /orders/:id` — the
|
||
last two are logged as SEC-02/B1-B2 and want a customer-token pass of their own.
|
||
|
||
**Tests**
|
||
- `apps/api/scripts/auth-smoke.sh` (live server): **46 passed, 0 failed**.
|
||
Run it with `API=http://localhost:3000/api CODE_LOG=/path/to/api.log`.
|
||
- `pnpm test` in `apps/api` (jest, newly installed): **7 passed**.
|
||
- Typecheck: api ✅ · user-app ✅ · user-app-v2 ✅. Both apps build ✅.
|
||
|
||
### ⚠️ Correction worth remembering
|
||
|
||
Stage 1 reported the customer-book leak as "mitigated" after narrowing the
|
||
`GET /customers` field list. That was wrong: the response still returned every
|
||
customer **id**, and the three `/customers/:id*` routes were still public, so the
|
||
full data was still two requests away. The user caught it. Stage 2 deleted the
|
||
whole id-bearing surface instead. **Narrowing a response is not closing a leak
|
||
while the id and the id-addressed routes remain.**
|
||
|
||
## ⚠️ Read `DECISIONS.md` before doing anything
|
||
|
||
All user rulings live there (D0–D17). Highlights that change how work is done:
|
||
|
||
- **D12 — the matrix is FIELD-BY-FIELD, not page-by-page**, and a 4-step
|
||
round-trip test per entity is mandatory. Both rules were added to the
|
||
`reference-parity-audit` skill (Step C rule C0, Step E rule E0).
|
||
- **D0** — split `fullName` into `firstName`/`lastName` via an additive migration;
|
||
**show the migration and get approval before running it**.
|
||
- **D4** — never port broken behaviour, but match calculations/rounding/messages
|
||
exactly; annotate each non-replication «عمداً تکرار نشد — ISSUE #n».
|
||
- **D2** — target-only features get status ➕ and are left alone.
|
||
- **D13** — the **retail storefront** (48 `customer.php` + 30 `site.php` + 31
|
||
storefront-management admin routes = **109 of the reference's 412 endpoints**)
|
||
is **preserved but out of scope**: status ⏸️, no gap row, no work row, nothing
|
||
deleted or disabled. **288 endpoints stay in scope** (218 admin + 70 B2B app).
|
||
Note: the reference total is **412, not 369** — `admin.php` was undercounted
|
||
in Phase 0; corrected in `00-INVENTORY.md` §2.1.
|
||
Ask before changing shared surfaces (product, variant, price, order, customer).
|
||
|
||
## Phase checklist
|
||
|
||
- [x] P0.1 — All 3 reference folders readable (`henza_server`, `henza_admin`, `henza_pwa`)
|
||
- [x] P0.2 — Branch `parity/phase-0` + `docs/parity/` created
|
||
- [x] P0.3 — Inventory all 7 projects → `_inv-*.md` (8111 lines total) → consolidated in `00-INVENTORY.md`
|
||
- [x] P0.4 — Mapping tables, missing list, extra list (`00-INVENTORY.md` §2–4)
|
||
- [x] P0.5 — Draft `PROTECTED.md`
|
||
- [x] **P0.6 — GATE passed** — `PROTECTED.md` confirmed, D0–D12 recorded
|
||
- [x] P0.7 — Urgent traces delivered: `percent`, `@Public` list, login leak
|
||
- [x] P0.8 — Skill updated with rules C0 (field-by-field) and E0 (round-trip test)
|
||
- [x] **P1.0 — approved and DONE:** 5 ledger routes protected, `percent` column added, D0 migration applied, SMS provider implemented
|
||
- [x] Phase 1 — Admin parity (`01-ADMIN-MATRIX.md`) — all in-scope sections closed 2026-10-12
|
||
- [ ] **GATE** ← HERE — asked 2026-10-12: "Admin parity is complete and tested. Shall we move on to the apps (user-app & user-app-v2)?"
|
||
- [ ] Phase 2 — Apps parity (`02-APPS-MATRIX.md`, both user apps)
|
||
- [ ] Phase 3 — Work through `ISSUES.md` (only when user asks)
|
||
|
||
## Answered — see `DECISIONS.md` for the full text
|
||
|
||
| # | Ruling |
|
||
|---|---|
|
||
| D0 | Split `fullName` → `firstName`/`lastName`, additive migration, backfill, sync, report multi-token rows. Approval needed before running. |
|
||
| D1 | Target's stricter auth stands. Build the "reference operation ↔ rid" matrix; explain 172 vs 174. |
|
||
| D2 | Audit only what the reference has. Target-only features → ➕, untouched. |
|
||
| D3 | Build the 5 missing screens in Phase 2, both apps, plus analytics, `isLoaded`/`sts_verify_step_two` guards, guest toast+home, and v2's cart ceiling. |
|
||
| D4 | Don't port breakage; do match calculations/rounding/messages. Unit-test the price formula. |
|
||
| D5 | Leave the stray folder; delete last, own commit. |
|
||
| D6 | **OTP must verify** — parity gap, fix it (plus expiry + attempt limits, which exceed the reference). |
|
||
| D7 | `@Public` ledger routes: list delivered, awaiting approval to protect. |
|
||
| D8 | Login leak: **target**, full customer book incl. national IDs and debts. |
|
||
| D9 | `percent`: trace delivered — **the target's implementation is wrong**. |
|
||
| D10 | Sales expert: keep `AdminUser`; investigate the 5 missing fields; propose `SalesExpertProfile`. |
|
||
| D11 | `singleCustomer.vue` → diff-send. |
|
||
| D12 | **Field-by-field matrix + mandatory round-trip test.** Added to the skill. |
|
||
| D13 | **Retail storefront (wallet, cashback, `ConfigStatic['percent']`) preserved but OUT OF SCOPE.** Nothing deleted or disabled; no parity work; matrices list it ⏸️ with no gap/work row; ask before touching shared surfaces. |
|
||
| D14 | **`migrate dev` in development, `migrate deploy` in production and nowhere else.** Never `migrate reset`; never edit `_prisma_migrations`. On drift + a reset offer: **stop and report**. |
|
||
| D15 | **`_goldbank_migrations` is KEPT** — it is the tracking table of the app's own migration runner (`src/prisma/migrate.ts`), needed because the Docker image has no Prisma CLI. Declared `@@ignore`d; drift is zero. **Persian test input must come from a FILE, never a shell argument.** |
|
||
| D16 | **PRICE-01 STOPPED** — price display is retail-storefront (⏸️ D13); the B2B app works in making charge only. `variant-price.ts` + tests kept but unused. **Making charge stays in scope.** New work order: Persons → Products/variants/order-detail → the rest of the admin → gate → apps. **Per section: implement gaps, then field-by-field test, and show a gap summary BEFORE fixing.** |
|
||
| D17 | **SalesExpertProfile** (cityId, gramLevel, phone, nationalCode) + **firstName/lastName on all AdminUsers**. **`reagent_id` deliberately NOT ported** — in the reference it was a side effect of the shared customers form and filling it removed the expert from the selectable-agent list (queryReagent:420); the referrer logic lives on `Customer.referredByAgentId`. |
|
||
| D29 | **«ورود به حساب مشتری» restored** (2026-10-12) — a sales expert can work inside any of their referred customers' accounts with the reference's **full** scope (every customer route, documents included). Mechanism deliberately not the reference's: no customer token is minted; `X-Act-As-Customer` rides the expert's own token and is authorised in `CustomerJwtStrategy.validateExpert`, re-checking `referredByAgentId` on **every** request. **Supersedes D21's "deliberately not ported".** |
|
||
| D28 | **App login step 1 must say what went wrong** (2026-10-12): an unregistered number → 404, a failed SMS send → 503, an unusable sales-expert account → 403, each with its own Persian message. Restores the reference's `firstOrFail()`. **Trades away part of the anti-enumeration protection, deliberately** — the per-mobile/per-IP send throttles still bound it and `verifyOtp` stays single-message. See D28 for the full reasoning and why D20 is not violated. |
|
||
|
||
## Open — awaiting user approval
|
||
|
||
Nothing. The 5 ledger routes, the `percent` column and the D0 migration are all
|
||
done and verified (see CHANGELOG 2026-09-22).
|
||
|
||
## Key context (survives context loss)
|
||
|
||
- **Domain correction:** the reference is a **gold-jewellery B2B/B2C e-commerce**
|
||
system, not a gold bank. No buy/sell order book, no spread, no deposit/withdraw,
|
||
no P2P transfer, no KYC levels, no daily limits (only `gram_level`), no payment
|
||
callback route, no cron, no websockets. See `00-INVENTORY.md` §0.
|
||
- **The gold price formula** (Phase 1 row #1) —
|
||
`henza_server/app/Models/ProductVariant.php:183-190`: `+extra%` → `+7%` margin →
|
||
`+9%` VAT **on the non-metal portion only** → `×grams` → `floor(/1000)*1000`.
|
||
Three superseded commented-out variants above it apply VAT to the whole price —
|
||
do **not** port those.
|
||
- **A large prior port already happened** — see `docs/ADMIN-PORT-PLAN.md` and
|
||
`docs/CHANGELOG-PORT-HISTORY.md` (80 views, 25 Nest modules, 29 Prisma models,
|
||
154 admin routes, 144/144 rids). Prior port notes **no view was ever visually
|
||
verified in a browser**.
|
||
- **Stack changed across the port:** Laravel→NestJS/Prisma, Vue2/Vuesax→Vue3/antd.
|
||
Port behaviour, not code style.
|
||
- **Reference scale is inflated:** 541 admin `.vue` files but only ~69 reachable
|
||
(rest are Vuexy template demos); 9 reference router files are dead (not imported);
|
||
~28 legacy Django-era DB tables have no migrations.
|
||
- **v1 ⇄ v2 logic gaps** (5, all v2 behind v1) are parity work, not design:
|
||
`ISSUES.md` MON-01, V2-01..05.
|
||
|
||
## Inventory files (Phase 0 working files)
|
||
|
||
| File | Scope | Lines |
|
||
|---|---|---|
|
||
| `_inv-ref-server.md` | henza_server | 1313 |
|
||
| `_inv-tgt-api.md` | apps/api | 1179 |
|
||
| `_inv-ref-admin.md` | henza_admin | 1705 |
|
||
| `_inv-tgt-admin.md` | apps/admin-panel | 1261 |
|
||
| `_inv-ref-pwa.md` | henza_pwa | 1178 |
|
||
| `_inv-tgt-userapp.md` | apps/user-app | 612 |
|
||
| `_inv-tgt-userappv2.md` | apps/user-app-v2 | 700 |
|
||
|
||
Consolidated into `00-INVENTORY.md`. Issues found: `ISSUES.md` (26 entries).
|
||
|
||
## Standing rules added 2026-09-22
|
||
|
||
1. **Migrations run only via the standard Prisma workflow** (`migrate dev` in
|
||
development). **`prisma db execute` is not used for schema changes.**
|
||
2. **Every data deletion is logged in `CHANGELOG.md`** — including throwaway
|
||
rows created during a session.
|
||
3. **D13** — the retail storefront is preserved but out of scope; see
|
||
`DECISIONS.md` §D13 and `PROTECTED.md` §H.
|
||
|
||
## Phase 2 — item #1 (MON-08) — ✅ DONE 2026-10-12
|
||
|
||
All four steps of the ruling are in place; see `ISSUES.md` §"Phase 2 — item #1".
|
||
Steps 1, 3 and 4 had landed with the wage-formula work; **step 2 — logging the
|
||
client figures that get discarded — was added 2026-10-12**, along with the
|
||
comparison test the ruling asked for («تست مقایسهای»), which step 3 had not
|
||
actually had: the suite only covered a tampered request, not an honest one.
|
||
`orders.service.spec.ts` is now 16 tests.
|
||
|
||
An operator ordering from the panel still keeps the numbers they typed — only an
|
||
app order is re-priced by the server.
|
||
|
||
## Phase 2 — item #2 (D3) — ✅ DONE 2026-10-12
|
||
|
||
D3 was ten items, not one. **Seven were already built** — all five screens
|
||
(`/report`, `/report/detail`, `/ChildOrder`, `/customerChoice`,
|
||
`/complete_profile`) exist in **both** apps, as do the `sts_verify_step_two`
|
||
guard and v2's cart stock ceiling. Checked in the code, not read off a
|
||
checkmark.
|
||
|
||
| Outstanding item | Result |
|
||
|---|---|
|
||
| guest toast «ابتدا وارد شوید» | ✅ both apps (**APP-03**). The destination stays the target's `/login?redirect=…`, better than legacy's `home` — owner's call, logged as «عمداً تکرار نشد — D3/الف». |
|
||
| `isLoaded` router guard | ⏹️ **not ported** (**APP-04**) — guards a bootstrap request the target does not have; would only bounce a shared product link to `home` (D4). Owner agreed. |
|
||
| route analytics | ⏸️ **deferred, lowest priority** (**ANLY-01**) — design parked in `docs/PHASE2-D3-PLAN.md`, with the two reference defects not to copy and the two retention/privacy questions still open. |
|
||
|
||
⚠️ **Phase 2 is running without its matrix.** `02-APPS-MATRIX.md` still does not
|
||
exist, so "what else do the apps miss" rests on the earlier survey rather than a
|
||
field-by-field comparison (D12). That is the next item.
|
||
|
||
The original requirement, for the record:
|
||
|
||
Before anything else in Phase 2: the server must derive `wagePercent`,
|
||
`discountPercent` and `flatRialCharge` from the DB and **ignore** the client's
|
||
values, because `henza_pwa` computes them rather than asking the user
|
||
(`cart.vue:170-179`, `Mixins/product/index.ts:69-75`). A comparison test must
|
||
show the total is unchanged for an ordinary customer. Depends on
|
||
`makingChargeOffsetPercent` existing. Full reasoning: `DECISIONS.md`
|
||
§"Report — MON-08".
|
||
|
||
## Done 2026-09-22 (was: waiting on the owner)
|
||
|
||
| # | Item | What is ready |
|
||
|---|---|---|
|
||
| 1 | Run the **D0** and **percent** migrations | ✅ **Applied** with `migrate deploy` (`migrate dev` is interactive-only and this shell is not a TTY). Drift checked read-only first — the only difference is the pre-existing `_goldbank_migrations` table, which was **NOT** dropped (DEAD-07). Service layer, all three forms, trigger test and the full round-trip test done. |
|
||
| 2 | Implement the **SMS provider** (Trez) | ✅ `SmsModule`/`SmsService` live, OTP wired, text byte-identical, boot check fails in production on a missing key, `.env.example` has names only. The 12 B2B templates land per flow in their own phase. **SEC-12: the owner is rotating the credentials and setting them in `.env`.** |
|
||
|
||
|
||
## ⚠️ Standing rules added 2026-09-22 (D14)
|
||
|
||
1. **`migrate dev` in development, `migrate deploy` in production and nowhere
|
||
else.** Never `migrate reset`, never edit `_prisma_migrations` by hand. If
|
||
drift is reported and a reset is offered: **stop and report**, do not accept.
|
||
`prisma db execute` is no longer used for schema changes.
|
||
2. **Every data deletion is logged in `CHANGELOG.md`**, including throwaway rows
|
||
a session creates itself.
|
||
|
||
## ⚠️ `customer_sync_full_name` is a DATABASE TRIGGER
|
||
|
||
Prisma knows nothing about it, so writing `Customer` can store something
|
||
different from what you wrote:
|
||
|
||
| Write | Result |
|
||
|---|---|
|
||
| the parts only | `fullName` is rebuilt |
|
||
| `fullName` only | the parts are re-derived (first token / the rest) |
|
||
| **both at once** | **THE PARTS WIN**; the submitted `fullName` is discarded |
|
||
|
||
Re-read after writing if you need the stored value. See `DECISIONS.md` §D14 and
|
||
the doc comment on the `Customer` model in `schema.prisma`.
|
||
|
||
## ⚠️ Two migration ledgers, on purpose (D15)
|
||
|
||
| Ledger | Written by | Used |
|
||
|---|---|---|
|
||
| `_prisma_migrations` | the Prisma CLI | development |
|
||
| `_goldbank_migrations` | `apps/api/src/prisma/migrate.ts` | the deployed container, at boot |
|
||
|
||
The Docker deploy image is copy-only and ships **no Prisma CLI**, so the app
|
||
applies migrations itself at startup. **Every new migration must be safe under
|
||
both paths** — keep the SQL idempotent (`IF NOT EXISTS`, `CREATE OR REPLACE`).
|
||
|
||
## ⚠️ Persian test input comes from a FILE (D15)
|
||
|
||
The Windows shell replaces Persian characters with `?`. A test that passes its
|
||
data inline can "pass" while storing `????`. Write fixtures in the test file or
|
||
a UTF-8 JSON file and send bodies with `curl --data-binary @file`. Console
|
||
output of Persian is never evidence — compare values in Node.
|
||
|
||
## ⚠️ Phase 1 work order (D16 — supersedes the earlier ordering)
|
||
|
||
**(a) Persons (اشخاص)** ← current
|
||
1. The 5 missing sales-expert fields (`ncode`, `phone`, `city_id`, `gram_level`,
|
||
`reagent_id`): **report first** where the reference uses each for `type=1`,
|
||
then add accordingly (proposal: `SalesExpertProfile` related to `AdminUser`,
|
||
D10). Unused fields are listed for the owner, not added speculatively.
|
||
2. `singleCustomer` → diff-send (D11).
|
||
3. Field-by-field audit of every Persons form and grid (retail, wholesale,
|
||
sales expert) per D12, each with its round-trip test.
|
||
|
||
**(b) Products, categories, variants, admin order detail** — deep field-by-field.
|
||
The variant image picker is PROTECTED (§B) — additive only. Making charge on
|
||
every variant and order line must display and persist exactly as the reference.
|
||
|
||
**(c) The rest, in order:** financial documents → users & permissions → admin SMS
|
||
templates → promotions / notifications / cities / branches / workshops → reports.
|
||
|
||
**(d)** Full admin test pass → Phase 1 report → gate question → **STOP and wait**.
|
||
|
||
### Method per section (D16)
|
||
|
||
Implement the gaps first, then field-by-field test that same section. **Show the
|
||
owner a short gap summary BEFORE fixing each section.**
|
||
|
||
## ✅ SEC-02 — verified complete (re-tested 2026-09-22)
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| `POST /orders` without a token | 401 (was `@Public`) |
|
||
| Body `customerId: "customer-2"` with a `customer-dev-test` token | order landed on **customer-dev-test**; spoof blocked and warn-logged |
|
||
| `GET /orders/:id` own order | 200 |
|
||
| `GET /orders/:id` another customer's order | **404**, not 403 — ids stay unenumerable |
|
||
| `GET /orders/:id` unknown id | 404, identical response |
|
||
| `GET /orders/admin/:id` with a customer token | 401 |
|
||
|
||
**9/9 passed** live, plus **32/32** in `auth-smoke.sh`. The server derives the
|
||
customer from the token only (`orders.controller.ts:26-29`,
|
||
`orders.service.ts` `createOrder(dto, customerId)`).
|
||
|
||
Client-supplied `wagePercent` / `discountPercent` / `flatRialCharge` remain —
|
||
that is **MON-08**, scheduled as Phase 2 item #1 per the owner's ruling.
|