Files
tehran_taj/docs/parity/PROGRESS.md
T
s.esmaeiliandClaude Opus 5 c518f9995b fix(auth): tell the user when the number is not registered (D28)
Reported by the owner: a number that is not in the system produced no
error at all. The app advanced to the code box and then nothing
happened, and there was no way to tell apart "the SMS is late", "the
gateway is down" and "this number was never registered" — the only one
of the three the person can actually do something about.

Step 1 now says which it is:
  - unregistered number          -> 404 «این شماره در سیستم ثبت نشده است…»
  - SMS send failed              -> 503 «ارسال پیام کوتاه … ممکن نیست…»
  - sales expert, account unusable -> 403, its own message

and both apps surface the server's Persian message for a 404 exactly as
they already did for a 429. Without that second half the person would
still have seen the generic «خطا در ارسال کد», which is the confusion
being removed.

This restores the reference's behaviour (firstOrFail() in
ApiMobileController::userAuth:43 returns 404); the opaque answer was our
deviation, not this. The anti-enumeration protection it was there for is
deliberately reduced at step 1 on the owner's instruction — the cost was
being paid by real users and by support. Still standing: the per-mobile
and per-IP send throttles, which the reference never had, so enumeration
is rate-bounded rather than free, and verifyOtp keeps one message for
every failure reason. ISSUES.md SEC-03's "no enumeration" claim is
corrected rather than left to go stale.

Two more instances of the same shape turned up while fixing it:

A failed SMS send also returned success, because SmsService never throws
(D20), so a dead gateway produced the identical dead end. D20 exists so
a failed SMS cannot break an operation — an order must still be placed —
but in a login the SMS IS the operation, so it now fails loudly. The
local-dev path keys off SmsService's own `sms-not-configured` result
rather than re-testing the config: with D26's default relay driver,
smsConfigured() is true on any machine with an external IPv4, so a
config test would have called a dev box configured and then failed its
login. The new test caught precisely that on its first run.

Three states return no trading account for a sales expert who does
exist. Under the new 404 each would have claimed the number was not
registered — false, and it would send support hunting a registration
problem that is not there.

Also corrects both apps' LoginView header comment, which still described
this flow as stubbed: no SMS provider, any 4-digit code accepted, step 1
matching the customer list in the client (that was the login leak, D8).
All three had been untrue for weeks and the stale text misled a reader
during this session.

requestOtp had NO test coverage: the 28-test customer-auth suite stayed
green through a change to its response for every unregistered number, so
its greenness was not treated as evidence. request-otp.spec.ts adds 9
cases, including that the dev fallback does not apply in production.

api: 295/295 tests, 27 suites, typecheck clean; both apps vue-tsc clean.

اپ: اگر شماره شما در سیستم ثبت نشده باشد، همان را می‌گوید و دیگر بی‌دلیل به صفحه کد نمی‌رود.

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

21 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 ✅, plus the app-login error reporting the owner asked for (AUTH-01/02/03, DOC-01). 295/295 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.
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.