Owner ruling: computing and displaying price / price_with_discount is retail storefront work, so it falls under D13 and is marked out of scope. The B2B app works in making charge only. variant-price.ts and its 11 hand-checked tests are kept and still run, just unused. Nothing needed reverting: computeVariantPrice was never called from outside its own file and spec, verified by grep over apps/api/src and apps/admin-panel/src, so no price had been wired into any screen. PRICE-01 is re-marked in ISSUES.md rather than deleted so the evidence survives if the storefront is ever taken up. Making-charge work stays fully in scope: percentage_pay, makingChargeOffsetPercent, and wage display on variants and order lines. New Phase 1 order: Persons -> products/categories/variants/order detail -> financial documents -> users and permissions -> admin SMS -> promotions, notifications, cities, branches, workshops -> reports -> gate. Per section: implement the gaps, then field-by-field test that section, and show a gap summary before fixing. SEC-02 re-verified live rather than quoted: 9/9 assertions, plus 32/32 in auth-smoke.sh. POST /orders is 401 without a token; a body customerId of "customer-2" sent with a customer-dev-test token still lands the order on customer-dev-test and is warn-logged; a foreign order id returns 404 rather than 403 so ids stay unenumerable; the admin read rejects a customer token. Client-supplied wagePercent/discountPercent/flatRialCharge remain as MON-08, already scheduled as Phase 2 item #1. Also records why the dev OTP log briefly looked broken: a stale API process still held port 3077, so the new instance died with EADDRINUSE and the tailed log belonged to the old process. Not a regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
PROGRESS — Reference Parity Project
Read this file FIRST at the start of every session and after every
/compact. Rules live inCLAUDE.md(workspace root). Method lives in thereference-parity-auditskill.
Current state
| Field | Value |
|---|---|
| Phase | Phase 1 — admin parity, matrix in progress |
| Branch | parity/phase-1-admin (cut from parity/security-hotfix) |
| Status | Both stages done and verified live: 46/46 smoke assertions, 7/7 jest unit tests, typecheck + build clean on all three projects. |
| Next step | Phase 1 (a) Persons — report where the reference uses the 5 sales-expert fields for type=1, then implement per D16 |
| Blocked on | nothing |
| Last updated | 2026-09-22 |
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 withAPI=http://localhost:3000/api CODE_LOG=/path/to/api.log.pnpm testinapps/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–D16). 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-auditskill (Step C rule C0, Step E rule E0). - D0 — split
fullNameintofirstName/lastNamevia 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+ 30site.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.phpwas undercounted in Phase 0; corrected in00-INVENTORY.md§2.1. Ask before changing shared surfaces (product, variant, price, order, customer).
Phase checklist
- P0.1 — All 3 reference folders readable (
henza_server,henza_admin,henza_pwa) - P0.2 — Branch
parity/phase-0+docs/parity/created - P0.3 — Inventory all 7 projects →
_inv-*.md(8111 lines total) → consolidated in00-INVENTORY.md - P0.4 — Mapping tables, missing list, extra list (
00-INVENTORY.md§2–4) - P0.5 — Draft
PROTECTED.md - P0.6 — GATE passed —
PROTECTED.mdconfirmed, D0–D12 recorded - P0.7 — Urgent traces delivered:
percent,@Publiclist, login leak - P0.8 — Skill updated with rules C0 (field-by-field) and E0 (round-trip test)
- P1.0 — approved and DONE: 5 ledger routes protected,
percentcolumn added, D0 migration applied, SMS provider implemented - Phase 1 — Admin parity (branch
parity/phase-1-admin,01-ADMIN-MATRIX.md) ← HERE - GATE: "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 @@ignored; 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. |
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. See00-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.mdanddocs/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
.vuefiles 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.mdMON-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
- Migrations run only via the standard Prisma workflow (
migrate devin development).prisma db executeis not used for schema changes. - Every data deletion is logged in
CHANGELOG.md— including throwaway rows created during a session. - D13 — the retail storefront is preserved but out of scope; see
DECISIONS.md§D13 andPROTECTED.md§H.
Phase 2 — item #1 (fixed by the MON-08 ruling)
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)
migrate devin development,migrate deployin production and nowhere else. Nevermigrate reset, never edit_prisma_migrationsby hand. If drift is reported and a reset is offered: stop and report, do not accept.prisma db executeis no longer used for schema changes.- 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
- The 5 missing sales-expert fields (
ncode,phone,city_id,gram_level,reagent_id): report first where the reference uses each fortype=1, then add accordingly (proposal:SalesExpertProfilerelated toAdminUser, D10). Unused fields are listed for the owner, not added speculatively. singleCustomer→ diff-send (D11).- 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.