Files
tehran_taj/docs/parity/PROGRESS.md
T
s.esmaeiliandClaude Opus 5 654b98520d parity(api): declare _goldbank_migrations as an ignored model; drift now zero
Prisma migrate diff proposed DROP TABLE "_goldbank_migrations" because the
table exists in the database but was not declared in the schema. Declaring
it with @@ignore removes the proposal without exposing it to Prisma Client,
and the table and all its rows are kept.

Correcting the earlier DEAD-07 note: this is not a leftover from the
pre-Prisma port. It is the tracking table of the application's own
migration runner (src/prisma/migrate.ts:5), which exists because the Docker
deploy image is copy-only and ships no Prisma CLI, so migrate deploy cannot
run in the container (migrate.ts:7-13). The project therefore keeps two
ledgers deliberately: _prisma_migrations for the CLI in development, and
_goldbank_migrations for the runtime runner in the container. That runner
already filtered out any DROP of its own table (migrate.ts:190-196); the
schema declaration removes the cause rather than relying on that guard.

The practical consequence, recorded in DECISIONS.md D15: a new migration
must be safe under both paths, so its SQL stays idempotent.

The ledger grew 12 -> 14 rows while testing, with nothing deleted: starting
the API ran prepareDatabase(), which appended the two migrations applied
earlier today. Its $$-aware statement splitter handled the PL/pgSQL trigger
body correctly — trigger and function are both present and the 9 trigger
assertions pass against the live database.

Also records the UTF-8 testing rule: Persian test input must come from a
file, never a shell argument, because the Windows shell replaces Persian
characters with '?' and a test can then pass while storing '????'.

Verified: migrate diff -> "This is an empty migration" (zero drift);
migrate status -> up to date; tsc clean; 32 jest; 9 trigger assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 08:25:03 +03:30

12 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.

Current state

Field Value
Phase Security hotfix — stages 1 and 2 ✅ complete
Branch 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 Start 01-ADMIN-MATRIX.md — field-by-field per D12, rid mapping matrix per D1
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 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–D15). 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 (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.

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 (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)

  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.