Files
tehran_taj/docs/parity/PROGRESS.md
T
s.esmaeiliandClaude Opus 5 8133a2bdf4 parity(api,admin): split customer name, add making-charge offset, wire SMS
Applies the two approved additive migrations and fixes every consumer.

D0 — Customer.firstName / lastName
The reference has had two columns since 2020
(2020_10_01_120430_customers_table.php:18-19); the port collapsed them, so
the edit form showed the whole name in the "first name" box and saved an
empty last name. Adds both columns, backfills first-token/rest, and keeps
fullName in sync with a customer_sync_full_name trigger. Nothing dropped.

The trigger is SQL, so Prisma cannot see it: writing the parts rebuilds
fullName, writing fullName re-derives the parts, and writing both in one
statement lets THE PARTS WIN. Documented in the migration header, the
Customer model doc comment and DECISIONS.md §D14, because a write can now
store something other than what was passed.

D9 — Customer.makingChargeOffsetPercent
Legacy customers.percent, the per-customer making-charge offset that the
app adds to the variant's making charge (henza_pwa
Mixins/product/index.ts:69-75). toRow() was returning the referring
AGENT's commission rate instead (CUST-02). DB default 0 reproduces
today's behaviour exactly, so no existing customer's price moves; the
reference's application-layer default of 3 for NEW customers
(AdminController.php:338) lives in the create form, and the field is
required on create with the reference's own message (:354).

update() and updateCredit() both accepted percent and silently discarded
it; both now persist it. Sort/filter aliases fname/lname pointed at
fullName and now point at the real columns.

SMS — Trez provider (Sms.php:57-82)
New global SmsModule/SmsService: form-urlencoded POST, 20s timeout, body
trimmed, every message persisted. send() never throws, matching the
reference, which discarded Sms::Send's result so a gateway outage never
aborted the order or approval that triggered it. The OTP text is
byte-identical, including the Android SMS Retriever markers. Credentials
come from env only; production refuses to boot without them rather than
silently locking every customer out. .env.example carries names, no values.

Admin panel: the split(' ') fallback is removed from singleCustomer.vue,
and bulkCustomer.vue defaults percent to 3, requires it, coerces the
Decimal on load so diff-send works, and only re-sends `name` when a name
part changed. ReagentCustomer.vue is untouched — its «درصد کمیسیون» is
AdminUser.wagePercent, a different field.

Tests: 32 jest (incl. 14 new SMS config/template), 9 trigger assertions,
53 auth smoke, and the full D12 round-trip on the customer entity —
«محمد علی» + «احمدی» survives create, edit, a byte-identical no-op save,
and single-field edits. Typecheck clean on api and admin-panel.

Note: migrate dev is interactive-only and could not run here, so the
migrations were applied with migrate deploy after a read-only drift check.
The only drift is the pre-existing _goldbank_migrations table, which
Prisma would DROP; it was NOT dropped (DEAD-07).

[ref: henza_server/app/Models/Sms.php, app/Http/Controllers/Adm/AdminController.php]

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

199 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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–D14). 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
- [x] P0.1 — All 3 reference folders readable (`henza_server`, `henza_admin`, `henza_pwa`)
- [x] P0.2 — Branch `parity/phase-0` + `docs/parity/` created
- [x] P0.3 — Inventory all 7 projects → `_inv-*.md` (8111 lines total) → consolidated in `00-INVENTORY.md`
- [x] P0.4 — Mapping tables, missing list, extra list (`00-INVENTORY.md` §2–4)
- [x] P0.5 — Draft `PROTECTED.md`
- [x] **P0.6 — GATE passed** — `PROTECTED.md` confirmed, D0–D12 recorded
- [x] P0.7 — Urgent traces delivered: `percent`, `@Public` list, login leak
- [x] P0.8 — Skill updated with rules C0 (field-by-field) and E0 (round-trip test)
- [x] **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**. |
## 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`.