parity(api,apps): delivery-mode gaps — master OTP, non-blocking SMS, faToEn, v2 cart cap

D20: OTP_MASTER_CODE login bypass read only from the environment, disabled when
unset, audited on every use, with a boot banner while armed. SMS no longer
aborts the boot in production — it warns and declines silently, so no message
can stop an operation.

D19: port Utility::faTOen onto admin-entered mobiles (before the uniqueness
lookup, which could previously be bypassed with Persian digits), and the
create-only welcome SMS, byte-identical to the reference and fire-and-forget.

user-app-v2 was missing the cart stock ceiling entirely: capQty/maxQty, the
un-orderable guard and the qty clamp are ported, leaving v2's own design intact.
The two cart stores are now byte-identical. isVariantOrderable and the
orderable-first default selection ported too.

jest 45/45; typecheck clean on api, admin-panel, user-app, user-app-v2.

[ref: henza_server/app/Http/Controllers/Adm/AdminController.php:333,348,364,378-387]
[ref: henza_server/app/Models/Utility.php:26]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-22 18:35:33 +03:30
co-authored by Claude Opus 5
parent 2194b3d4d9
commit 460cf276b6
128 changed files with 756 additions and 181 deletions
+51
View File
@@ -540,3 +540,54 @@ pass.**
|---|---|---|
| `AdminUser` rows created by the round-trip test (one full expert, one profile-less) + their `SalesExpertProfile` rows | 2 per run | Test fixtures, removed by the test itself. |
| 1 temporary `Customer` row per `test-name-trigger.cjs` run | 1 per run | Test fixture. |
## 2026-09-22 — Delivery mode (D19/D20)
### API
- **`OTP_MASTER_CODE`** (D20) — `customer-auth.service.ts`. A delivery-only login
bypass, read **only** from the env var and disabled when unset or blank. It is
checked before the stored challenge so it works with no `CustomerOtp` row, and
it still enforces "an active, non-archived customer must exist". Every use is
logged with the customer's mobile. `main.ts` prints a boot banner while armed.
Token issuance was factored into `issueToken()` so the bypass and the real path
produce an identical token. Logged as **REL-01 (Critical)**.
- **SMS never blocks the boot** (D20) — `sms.service.ts`. `assertSmsConfigured()`
no longer throws in production; it warns and returns. `send()` already never
threw. Reverses D14. Logged as **REL-02**.
- **`faToEn` on admin-entered mobiles** (D19 gap 2) — `admin-customers.service.ts`
(`create`, `update`) and `admin-users.service.ts` (`create`, `update`).
Ports `Utility::faTOen` (`AdminController.php:333,364`). In `admin-users` the
normalisation runs **before** the uniqueness lookup, which previously let the
same number be registered twice as `۰۹۱۲…` and `0912…`.
- **Welcome SMS on customer create** (D19 gap 1) — `admin-customers.service.ts`.
Byte-identical to `AdminController.php:378-387` (verified by comparison,
including the trailing space in `لینک pwa `). Create-only, as in the reference,
and fire-and-forget so no gateway problem can delay or fail the save.
### Apps — user-app-v2 brought level with user-app
- **Cart stock ceiling** — `stores/cartStore.ts`. Ported `capQty`, `maxQty`, the
un-orderable guard in `addToCart` (now returning a boolean) and the clamp in
`updateFeild`. The two stores are now **byte-identical**.
- `composables/useProduct.ts` — ported `isVariantOrderable` and the
orderable-first default-variant selection. v2's variant-image `image` computed
is PROTECTED and was left untouched.
- `components/product/CartItem.vue`, `SingleProduct.vue` — the `+` button is
disabled at the ceiling; un-orderable variant chips are disabled and dimmed.
Only logic was ported: v2 keeps its own stepper DOM order, borderless rows and
chip design.
- `user-app/.../SingleProduct.vue` — `addToCart` now checks the store's return
value, so the "added" toast no longer fires when the store refused the item.
Applied to BOTH apps, per the rule that a logic fix lands in each.
### Tests
- New `customer-auth/master-otp.spec.ts` — 13 cases: the env switch, login with
the master code (including Persian digits), the audit log, refusal for
inactive/unknown customers, rejection when the var is absent, and the real code
still working both with and without the bypass armed.
- `sms/sms.service.spec.ts` — the boot-contract block rewritten for D20: never
throws, warns and names the missing variables.
- jest: **45 passed** (was 32). typecheck clean on api, admin-panel, user-app,
user-app-v2.
### Data
- No data was deleted or modified.