feat(orders): send the order-placement SMS and close Phase 1

The reference texted three audiences on every placed order
(henza_server Api/ApiMobileController::setOrder:733-766). None of it was
wired here — OrdersService had no SmsService at all, so a customer could
place an order and nobody was told: not the customer, not the admins, not
the referring sales expert.

Now, after the transaction commits (so a rolled-back order texts nobody)
and without ever being awaited (D20 — a dead gateway must not fail an
order):
  - every mobile in ORDER_ADMIN_MOBILES, with the customer's name and
    the rounded gold weight;
  - the customer, with distinct text for a normal and an over-limit
    suspended order;
  - the referring sales expert, looked up on AdminUser (the target's
    referring expert is an admin, not a customer as in the reference).

The reference's four hardcoded personal numbers stay out of git, behind
the same env-var pattern as PROFILE_ADMIN_MOBILES.

The «در سیستم ویرایش شد» variant was deliberately not ported: the target
has no order-edit route, so it would be dead code — «عمداً تکرار نشد».

Also fills §2 of the admin matrix, the D1 deliverable that had been
(pending) since the phase opened: 163 rid-guarded routes, 68 distinct
rids, 176 grantable tree keys, and a per-module map to the reference
controllers. Only two enforced rids are ungrantable from the permission
screen (newsletters, persons.list.sellers.view) — filed as PERM-02,
awaiting an owner ruling because it changes the permission vocabulary.

Phase 1 (admin parity) is at its gate. api: 283/283 tests, 26 suites.

پنل: پیامک ثبت سفارش برای مشتری، ادمین‌ها و کارشناس فروش فعال شد.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-12 10:24:46 +03:30
co-authored by Claude Opus 5
parent 759625d2a1
commit 83e5d2868b
8 changed files with 351 additions and 19 deletions
+62 -6
View File
@@ -138,12 +138,61 @@ prove no operation is left without an equivalent permission.
Per-module mapping of the reference's admin controller operations onto the
target's rid, grouped by the module of `apps/api/src/modules/`:
| Reference operation (controller) | Target endpoint | rid |
|---|---|---|
| *(pending — filled per module as each section below is audited)* | | |
Counted mechanically from the target's `@RequirePermission` decorators and the
permission tree, 2026-10-12 (§3–§9 are all complete, so both sides have been
read):
> Populated as §3 onward completes, so no row is written before both sides have
> been read.
| | Count |
|---|---|
| Reference admin routes (`routes/admin.php`) | 154 |
| Target admin routes carrying a rid | 163 |
| Distinct rids enforced by those routes | 68 |
| Keys in the permission tree (`permissionConst.ts`) | 176 |
Per module, against the reference controller it ports:
| Target module | Reference controller(s) | Routes | Distinct rids |
|---|---|---|---|
| `accounting` | `WalletController`, `CashController` | 5 | `wallet.list`, `wallet.charge`, `document.create`, `document.list.confirm` |
| `accounting-sync` | — target-only (D27) | 2 | `persons.list.client.omde`, `product.list` |
| `admin-branches` | `BranchController` | 6 | `branch.list`, `branch.create` |
| `admin-categories` | `CategoryController` | 11 | `collection.list`, `collection.create`, `collection.list.edit` |
| `admin-comments` | `CommentController` | 6 | `comment` |
| `admin-customers` | `AdminController`, `MajorCustomerController`, `SingleCustomerController` | 23 | 10 `persons.*` + `wallet` |
| `admin-dashboard` | `DashboardController` | 5 | `dashboard.view`, `report.order.item`, `report.product` |
| `admin-documents` | `CashController`, `WalletController` | 13 | `document.*`, `config.wallet`, `wallet` |
| `admin-invoices` | `CinyController` | 6 | `invoice.list`, `invoice.create` |
| `admin-misc` | `CitiesController`, `SmsController`, `NotifyController`, `ConstController`, `BrandController` | 27 | 16 (`cities.*`, `sms.*`, `notify.*`, `config.statics*`, `statics`, `newsletters`) |
| `admin-orders` | `OrderController`, `MajorOrderController` | 9 | `order.list`, `order.create`, `order.omde`, `order.tamin` |
| `admin-preorders` | `OrderController` (tamin) | 5 | `order.makeOrder` |
| `admin-products` | `ProductController` | 15 | `product.*`, `order.create` |
| `admin-promotions` | `PromotionController`, `IgnoreController` | 12 | `offers.*`, `offersIgnore` |
| `admin-report` | `ReportController` | 1 | `report.order.item` |
| `admin-users` | `UserController`, `PermissionController` | 11 | `user.*` (7) |
| `admin-workshops` | `WorkshopController` | 4 | `workshop.list`, `workshop.create` |
| `orders` | `ApiMobileController::setOrder` | 2 | `order.list`, `order.list.sts.submit` |
**D1's requirement — "no operation left without an equivalent permission" — is
met:** every one of the 163 rid-guarded routes names a rid, and 66 of the 68
resolve to a key the permission screen can grant.
### The 2 exceptions — PERM-02
Two enforced rids are **absent from the permission tree**, so a route checks them
but no operator can grant them from the UI:
| rid | Enforced by | Effect |
|---|---|---|
| `newsletters` | `admin-misc` (subscriber list) | only a role that already holds it (from the seed) can read the newsletter list |
| `persons.list.sellers.view` | `admin-customers` (`GET /reagent/view/:id`, the ➕ target-only agent-detail screen) | the new screen cannot be granted to a new role |
This is **inherited from the reference, not introduced here** — the header of
`permissionConst.ts` already recorded four such rids. Two of the four
(`buy.order.list`, `persons.create.customer.tak`) now have no route at all, so
they are dead on both sides and need no action.
Logged as **PERM-02**. Additive fix (add the two keys to the tree); it touches
the permission vocabulary, so it waits for the owner like PERM-01.
---
@@ -225,7 +274,12 @@ Contrary to the Phase 0 expectation, this area is in **good** shape.
| Over-limit outcome | order still created, `sts = 0` (معلق) | `OrderStatus.SUSPENDED` (`orders.service.ts:92`) | ✅ match |
| Debt recognition point | `sts > 4`, first true at `sts = 5` | `DELIVERED_TO_AGENT`, documented at `admin-orders.service.ts:14` and mapped `5 → DELIVERED_TO_AGENT` at `:359` | ✅ match |
| Removing that status | legacy allowed it, losing the posted debt | deliberately refused (`admin-orders.service.ts:382-390`) | ➕ stricter — a data-loss guard, keep (D4) |
| Suspended-order SMS | `ApiMobileController.php:739` | not yet sent | ❌ — lands with the Phase 1 SMS templates |
| Suspended-order SMS | `ApiMobileController.php:754` | `orders.service.ts` `notifyOrderPlaced` | ✅ fixed 2026-10-12 |
| Order-placed SMS to the customer | `:757` | same | ✅ fixed 2026-10-12 |
| Order-placed SMS to the admins | `:739-743` (4 hardcoded numbers) | same, via `ORDER_ADMIN_MOBILES` | ✅ fixed 2026-10-12 |
| Order-placed SMS to the sales expert | `:762-764` | `notifySalesAgent` | ✅ fixed 2026-10-12 |
| Order-**edited** SMS to the admins | `:734-737` | none | «عمداً تکرار نشد — SMS-03» — no target path lets a customer edit a placed order |
| `Notify::store` push beside each SMS | `:755,758,765` | none | ⏸️ PUSH-01, deferred to the PWA port |
The one **money hole** here is already logged and scheduled: the server accepts
client-supplied `wagePercent` / `discountPercent` / `flatRialCharge`
@@ -424,4 +478,6 @@ layout as the reference. The target reads from `OrderItem` + `LedgerEntry`
| RPT-01 | Medium | The reference's 32-column Excel export (`ReportController::getOrderItemWithDetails`) had no target equivalent. | ✅ fixed — `admin-report` module, `GET admin/report/order-items` |
| SMS-01 | Low | SMS on document confirm/unconfirm was not wired in `toggleConfirm`. | ✅ fixed — `admin-documents.service.ts:notifyDocumentConfirm` |
| SMS-02 | Low | `%name` substitution in batch SMS text was missing. | ✅ fixed — `admin-misc.service.ts:sendSms` + `customerNamesByMobile` |
| SMS-03 | Medium | **None of the four order-placement SMS messages was wired** — `OrdersService` did not even inject `SmsService`. The matrix had logged only the suspended-order one. | ✅ fixed 2026-10-12 — `notifyOrderPlaced` + `notifySalesAgent`, 6 tests. The «ویرایش شد» variant is deliberately not ported (no such path). |
| PERM-02 | Medium | `newsletters` and `persons.list.sellers.view` are enforced by routes but absent from the permission tree, so they cannot be granted from the UI. Inherited from the reference. | awaiting owner (additive) |
| PUSH-01 | Low | Firebase push notifications not implemented — deferred to PWA port phase. | ⏸️ deferred |
+60
View File
@@ -769,3 +769,63 @@ REL-01/REL-02 in ISSUES.
build 5/5 · jest 45/45 · admin smoke 43/43 · apps smoke 43/43 ·
B2B scan 34 ok / 0 broken · B2B writes 25/25 · auth-smoke 32/32 ·
sales-expert 33/33 · name trigger 9/9 · typecheck clean on all four apps.
---
## 2026-10-12 — Phase 1 close-out — order SMS (SMS-03) and the §2 rid matrix
### 1. Order-placement SMS — SMS-03
- **What:** `OrdersService` now injects `SmsService` and, after the order
transaction commits, texts three audiences: the admins listed in
`ORDER_ADMIN_MOBILES`, the customer (different text when the order was
suspended for exceeding the gram ceiling), and the customer's referring sales
expert. Brand «منتزر» replaces «هنزا گلد»; every other character of the four
texts is the reference's.
- **Why:** the matrix had logged only the suspended-order message as missing. On
reading the reference, **all four** messages were absent and `OrdersService`
did not inject `SmsService` at all — a customer placing an order was told
nothing, and neither was anyone else.
- **Reference:** `henza_server/app/Http/Controllers/Api/ApiMobileController.php:733-766`
- **Target:** `apps/api/src/modules/orders/orders.service.ts`
(`notifyOrderPlaced`, `notifySalesAgent`), `orders.module.ts`, `.env.example`
- **Matrix row:** `01-ADMIN-MATRIX.md` §4, SMS-03
- **Deliberate non-replication:** the «در سیستم ویرایش شد» variant (`:734-737`)
is **not** ported — no target path lets a customer edit a placed order, so the
branch could never fire. Annotated «عمداً تکرار نشد — SMS-03» per D4.
- **Decisions carried over:** admin numbers come from an env var rather than the
reference's four hardcoded personal numbers, matching the existing
`PROFILE_ADMIN_MOBILES` pattern; SMS is fire-and-forget per D20, so a dead
gateway can never fail an order; the messages are sent **after** the
transaction commits, as the reference did, so a rolled-back order texts nobody.
`Notify::store` (Firebase push) beside each message stays deferred as PUSH-01.
- **Tests:** 6 new cases in `orders.service.spec.ts` (customer placed/suspended
text, admin fan-out, no-admins-configured, sales expert, expert without a
mobile). Full api suite **283/283**, typecheck clean.
- **Found while testing:** the pre-existing "sales expert credit" suite mocked no
`adminUser`, so the new agent lookup hit its catch and logged an error while
the test still passed green. Mock completed — a passing test now means the
path actually ran.
### 2. §2 of the admin matrix — the D1 rid matrix
- **What:** filled in the section that had stood as *(pending)* since the phase
began, mechanically from the target's `@RequirePermission` decorators and
`permissionConst.ts`: 163 rid-guarded routes, 68 distinct rids, 176 tree keys,
mapped per module onto the reference controller each one ports.
- **Why:** D1 required proof that no admin operation is left without an
equivalent permission. It is the last unfinished item of Phase 1.
- **Result:** 66 of the 68 enforced rids are grantable from the permission
screen. The 2 that are not — `newsletters` and `persons.list.sellers.view` —
are logged as **PERM-02**; both are inherited from the reference, whose own
`permissionConst.ts` header had already recorded four such rids (the other two
now have no route on either side).
- **Target:** `docs/parity/01-ADMIN-MATRIX.md` §2
- **Note on method:** the first extraction pass read the rid that followed each
HTTP decorator and produced an off-by-one map (it claimed
`reconcile → wallet.list`). Caught by spot-checking against this CHANGELOG's
own stage-1 entry, which records `reconcile → wallet.charge`. The corrected
pass was re-verified against five independently documented mappings before any
table was written.
_No data was deleted in this entry._
+7
View File
@@ -127,6 +127,13 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium`
| PRICE-02 | Info | REF `app/Models/ProductVariant.php:163` vs TGT `admin-products.service.ts:206-208` | `rial_pay` (a flat rial charge per variant) appears only in the **superseded, commented-out** formula, not the live one at `:183-190`. The target returns null rather than faking it, which matches the live reference. Recorded so nobody "restores" it from the dead code. | Read both. | ✅ No action — matches. |
| PERM-01 | Medium | TGT `prisma/seed.ts`; `apps/admin-panel/src/views/user/permissionConst.ts` | `dashboard.omde` and `dashboard.tak` are seeded into the `permission` table but are **absent from the permission UI tree**, so they can be held by a row yet never seen, granted or revoked from the admin screen — and nothing reads them any more (the protected Dashboard uses `dashboard.view`). The DB holds **178** distinct rids against the UI's **174**. | `SELECT rid FROM permission GROUP BY rid` → 178, including all three dashboard rids. | ✅ **FIXED 2026-10-08** (owner approved the deletion). Migration `20261008160000_dashboard_permission_merge` gives `dashboard.view` to every type holding `dashboard` / `dashboard.omde` / `dashboard.tak`, then deletes those rows. **Also found:** the import dropped these three as unknown rids, so legacy types 102, 104, 105, 108 and 135 lost the dashboard — `people.ts` RENAMED_RIDS now maps them to `dashboard.view`. The other 45 legacy rids the target lacks are tree parents or rids no legacy code reads. |
## Found while closing Phase 1 (2026-10-12)
| ID | Severity | Where | Description | Evidence | Status |
|---|---|---|---|---|---|
| SMS-03 | Medium | TGT `apps/api/src/modules/orders/orders.service.ts` | **No order-placement SMS at all.** The reference texts four audiences when an order is placed (`ApiMobileController.php:733-766`): the admins, the customer — with a distinct text when the order was suspended over the gram ceiling — and the customer's referring sales expert. `OrdersService` did not inject `SmsService`, so a customer placing an order was told nothing. The matrix had logged only the suspended-order message, understating the gap. | Read both sides; `grep SmsService` showed six modules wired and `orders` absent. | ✅ **FIXED 2026-10-12** — `notifyOrderPlaced` + `notifySalesAgent`, sent after the transaction commits, fire-and-forget per D20, admin numbers from `ORDER_ADMIN_MOBILES`. 6 tests. The «ویرایش شد» variant is «عمداً تکرار نشد» — no target path edits a placed order. |
| PERM-02 | Medium | TGT `apps/admin-panel/src/views/user/permissionConst.ts` | Two rids are **enforced by routes but absent from the permission tree**, so an operator cannot grant them: `newsletters` (newsletter subscriber list) and `persons.list.sellers.view` (the ➕ target-only agent-detail screen). Only a role that already holds them from the seed can use those routes. Inherited from the reference — that file's own header recorded four such rids; the other two (`buy.order.list`, `persons.create.customer.tak`) now have no route on either side and are dead. | Mechanical comparison of 68 enforced rids against 176 tree keys, `01-ADMIN-MATRIX.md` §2. | **awaiting owner** — the fix is additive (add the two keys to the tree) but it changes the permission vocabulary, so it waits like PERM-01 did. |
## Delivery-mode items (D19/D20, 2026-09-22)
| ID | Severity | Where | Description | Evidence | Suggested fix |
+32 -8
View File
@@ -19,12 +19,36 @@ The "Current state" table below is the parity project's and was last updated 202
| 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 |
| **Phase** | **Phase 1 — admin parity — COMPLETE, at the gate** |
| **Branch** | `main` (the parity branches were merged; see git log 2026-10-08..10-12) |
| **Status** | All in-scope sections closed: §2 §4 §5 §6 §7 §8 §9 ✅ · §3 ⏸️ out of scope by D16. **283/283** jest tests, typecheck clean. |
| **Next step** | **GATE question asked 2026-10-12 — waiting on the owner** before Phase 2 (apps parity). |
| **Blocked on** | the gate answer. Two additive items await an owner ruling: **PERM-02** and the open rows below. |
| **Last updated** | 2026-10-12 |
### Phase 1 close-out (2026-10-12)
The two items that were still genuinely unfinished:
| Item | Result |
|---|---|
| §2 — the D1 rid matrix (stood as *(pending)* all phase) | ✅ filled: 163 rid-guarded routes, 68 distinct rids, 176 tree keys, mapped per module. 66/68 grantable from the UI; the 2 that are not are **PERM-02**. |
| Order-placement SMS | ✅ **SMS-03** — the matrix had logged one missing message; in fact all four were missing and `OrdersService` had no `SmsService`. Fixed with 6 tests. |
**What is NOT done, by decision rather than by omission:**
| | Why |
|---|---|
| §3 — product price display (PRICE-01) | ⏸️ retail storefront, out of scope by **D13/D16**. `variant-price.ts` + 11 tests kept but unused. |
| Firebase push beside each SMS (PUSH-01) | ⏸️ deferred to the PWA port. |
| «سفارش ویرایش شد» admin SMS | «عمداً تکرار نشد» (D4) — no target path edits a placed order. |
| PERM-02 (2 ungrantable rids) | additive, changes the permission vocabulary → awaits the owner, as PERM-01 did. |
| `02-APPS-MATRIX.md` | Phase 2's file; not created yet. |
⚠️ **Not verified in a browser.** Every ✅ above rests on reading both sides,
unit tests and smoke scripts. The prior port's note still stands: no admin view
has been visually checked in a browser. A full click-through is the honest
remaining risk on Phase 1.
### Security hotfix — final result
@@ -92,8 +116,8 @@ All user rulings live there (D0–D17). Highlights that change how work is done:
- [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)?"
- [x] Phase 1 — Admin parity (`01-ADMIN-MATRIX.md`) — all in-scope sections closed 2026-10-12
- [ ] **GATE** ← HERE — asked 2026-10-12: "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)