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>
This commit is contained in:
s.esmaeili
2026-09-22 08:08:50 +03:30
co-authored by Claude Opus 5
parent 7b598e9e8e
commit 8133a2bdf4
18 changed files with 1013 additions and 51 deletions
+35 -11
View File
@@ -10,8 +10,8 @@
| **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** | **Migrations are written and shown — awaiting approval to run them** (`migrate dev`), then `01-ADMIN-MATRIX.md` field-by-field (D12) |
| **Blocked on** | approval to run the two migrations; approval to implement the SMS provider |
| **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
@@ -51,7 +51,7 @@ while the id and the id-addressed routes remain.**
## ⚠️ Read `DECISIONS.md` before doing anything
All user rulings live there (D0–D13). Highlights that change how work is done:
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
@@ -79,8 +79,8 @@ All user rulings live there (D0–D13). Highlights that change how work is done:
- [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)
- [ ] **P1.0 — user approves: 5 ledger routes, `percent` column, D0 migration** ← HERE
- [ ] Phase 1 — Admin parity (branch `parity/phase-1-admin`, `01-ADMIN-MATRIX.md`)
- [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)
@@ -103,12 +103,12 @@ All user rulings live there (D0–D13). Highlights that change how work is done:
| 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
1. **Protect the 5 `@Public` ledger-write routes** (`DECISIONS.md` group A).
2. **`percent` schema change** — add `makingChargeOffsetPercent` to `Customer`.
3. **D0 migration file** — must be shown and approved before it runs.
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)
@@ -166,9 +166,33 @@ show the total is unchanged for an ordinary customer. Depends on
`makingChargeOffsetPercent` existing. Full reasoning: `DECISIONS.md`
§"Report — MON-08".
## Waiting on the owner (2026-09-22)
## Done 2026-09-22 (was: waiting on the owner)
| # | Item | What is ready |
|---|---|---|
| 1 | Run the **D0** and **percent** migrations | Both SQL files written, storefront impact assessed, backfill report script written, 11 unit tests passing. Command: `npx prisma migrate dev`. |
| 2 | Implement the **SMS provider** (Trez) | Provider, all 12 templates and env-var names documented in `DECISIONS.md`. Nothing written yet. **SEC-12: the reference's committed credentials must be rotated.** |
| 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`.