Files
tehran_taj/docs/parity/PROGRESS.md
T
s.esmaeiliandClaude Opus 4.6 19d490e790 feat: accounting integration (D27), SMS relay driver (D26), rebuild user apps
- Add Tahesab/Zarrin accounting sync module with admin panel UI
- Add SMS relay driver as default provider, refactor SMS into driver pattern
- Update customer services, orders, products, and catalog queries
- Rebuild user-app and user-app-v2 dist assets
- Add Prisma migration for accounting tables
- Update docker-compose and env examples
- Update parity docs (DECISIONS, ISSUES, PROGRESS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-06 16:48:36 +03:30

16 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

The "Current state" table below is the parity project's and was last updated 2026-09-22.

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) continued: singleCustomer diff-send, then the field-by-field audit of all Persons forms and grids
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–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 (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.
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 (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.

⚠️ 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.