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:
co-authored by
Claude Opus 5
parent
7b598e9e8e
commit
8133a2bdf4
@@ -170,8 +170,29 @@ model AdminUser {
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(cuid())
|
||||
fullName String
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// ⚠️ `fullName`, `firstName` and `lastName` are kept in sync BY A DATABASE
|
||||
/// TRIGGER — `customer_sync_full_name`, created in migration
|
||||
/// 20260922120000_customer_name_parts. Prisma does not know about it, so a
|
||||
/// write here is NOT the whole story:
|
||||
///
|
||||
/// * write the parts -> fullName is rebuilt as "first last"
|
||||
/// * write fullName only -> the parts are re-derived from it
|
||||
/// * write BOTH in one update -> THE PARTS WIN; fullName is overwritten
|
||||
/// with "first last" and whatever you passed for fullName is discarded
|
||||
///
|
||||
/// So a value you wrote can differ from the value that lands in the row.
|
||||
/// Re-read after writing if you need the stored value. Prefer writing the
|
||||
/// parts; `fullName` exists for the legacy readers that predate D0.
|
||||
///
|
||||
/// Reference has these as two columns from the start
|
||||
/// (henza_server 2020_10_01_120430_customers_table.php:18-19 fname/lname);
|
||||
/// the port collapsed them, which is the D0 defect.
|
||||
fullName String
|
||||
firstName String?
|
||||
lastName String?
|
||||
|
||||
mobile String @unique
|
||||
nationalCode String @unique
|
||||
passwordHash String
|
||||
@@ -201,6 +222,22 @@ model Customer {
|
||||
goldDebtGrams Decimal @default(0) @db.Decimal(14, 4) // legacy remain
|
||||
fiatDebtRial Decimal @default(0) @db.Decimal(20, 2) // legacy rial_remain
|
||||
|
||||
/// Legacy `customers.percent` — the per-customer making-charge offset. It is
|
||||
/// ADDED to the variant's making charge when pricing an order line
|
||||
/// (henza_pwa src/Mixins/product/index.ts:69-75:
|
||||
/// getWage(v) = state.percent + v.percentage_pay)
|
||||
/// and it shifts the catalogue making-charge filter bounds
|
||||
/// (ApiMobileController.php:391-396,76-77). It is NOT part of the price
|
||||
/// formula itself — that extra percent is ConfigStatic['product_price_extra_percent'].
|
||||
///
|
||||
/// NOT the wallet cashback rate (storefront ConfigStatic['percent'], D13) and
|
||||
/// NOT the wholesale per-line charge (OrderItem.wagePercentSnapshot).
|
||||
///
|
||||
/// DB default 0 keeps every existing customer's price unchanged. The
|
||||
/// reference's application-layer default for NEW customers is 3
|
||||
/// (AdminController.php:338) and the field is required on create (:354).
|
||||
makingChargeOffsetPercent Decimal @default(0) @db.Decimal(6, 3)
|
||||
|
||||
verificationStep VerificationStep @default(UNVERIFIED)
|
||||
nationalIdImageUrl String?
|
||||
businessLicenseUrl String?
|
||||
@@ -236,6 +273,8 @@ model Customer {
|
||||
otpChallenges CustomerOtp[]
|
||||
|
||||
@@index([referredByAgentId])
|
||||
@@index([firstName])
|
||||
@@index([lastName])
|
||||
}
|
||||
|
||||
model CustomerAddress {
|
||||
|
||||
Reference in New Issue
Block a user