Files
tehran_taj/docs/parity/PROGRESS.md
T
s.esmaeiliandClaude Opus 5 89e2d7937e feat(apps): tell a guest why they were bounced to the login page (D3)
A guest tapping a personal page — orders, transactions, profile,
favourites, complete-profile — was redirected in silence, so they landed
on the login box with nothing explaining what happened. Legacy showed
«ابتدا وارد شوید» (henza_pwa/src/router/index.ts:259).

The message is now shown in both apps. The destination deliberately
stays the target's /login?redirect=…, which legacy did not have: legacy
sent the guest to home and lost where they were going, while here
signing in returns them to the page they asked for. The owner chose that
trade-off, so it is logged as «عمداً تکرار نشد — D3/الف» rather than
quietly left as a divergence. The guard also keys off meta.requiresAuth
instead of legacy's hand-kept list of five route names, so the two
sales-expert pages are covered too.

Legacy's isLoaded router guard is NOT ported, and the reasoning is
written into both apps' configStore beside the line that says so: it
guarded legacy's one big bootstrap request, which this target does not
have, so it would guard nothing and only add a bounce — a shared product
link would open home instead of the product. That is porting the
breakage, not the behaviour (D4).

Route analytics is deferred at the owner's request; its design is parked
in docs/PHASE2-D3-PLAN.md so it need not be re-derived, along with the
two reference defects not to copy (log/create was unauthenticated and
accepted any title; log/update was a GET that wrote) and the open
retention and IP questions.

Seven of D3's ten items turned out to be built already — all five
screens in both apps, the sts_verify_step_two guard, v2's cart ceiling —
verified in the code rather than from the checkmarks.

Neither app has a test runner: verified with vue-tsc -b on both (clean)
plus confirming the toast can actually appear, not merely compile —
toast.info exists and <ToastHost /> is mounted in each App.vue.

اپ: وقتی بدون ورود صفحه شخصی را باز کنید، پیام «ابتدا وارد شوید» نشان داده می‌شود.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 10:40:22 +03:30

20 KiB
Raw Blame History

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 ✅. 286/286 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

  • 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 in 00-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.md confirmed, D0–D12 recorded
  • P0.7 — Urgent traces delivered: percent, @Public list, 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, percent column added, D0 migration applied, SMS provider implemented
  • 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 @@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.
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.

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.