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 |