`➕ extra in target` · `❓ unknown` · `⏸️ preserved — out of current scope` (D13)
---
## §0 — Scope of this phase
From the 412 reference endpoints, D13 leaves **288 in scope**; Phase 1 covers the
**218 admin (B2B)** ones plus the admin panel screens that call them. The 31
storefront-management admin routes are listed `⏸️` and get no gap rows.
### Reachable screens
The reference ships 11 router files but **only two are imported**
(`henza_admin/src/router.js:6,8` → `router/admin.js` and `router/branch.js`);
the other nine are dead (Phase 0 finding, confirmed). Reachable routes:
| | Reference | Target |
|---|---|---|
| `router/admin` | 67 | 69 |
| `router/branch` | 17 | 17 |
| **Total** | **84** | **86** |
Route-level mapping is complete: **all 67 reference admin routes exist in the
target at the same paths** (`henza_admin/src/router/admin.js` ↔
`apps/admin-panel/src/router/admin.ts`). Two target-only routes:
| Target route | Component | Status |
|---|---|---|
| `/reagent/add` | `views/apps/customer/EditReagent.vue` | ➕ extra — the reference had no add-agent route (agents were added through the shared customer form); harmless |
| `/reagent/view/:id` | `views/customer/reagentList/AgentDetail.vue` | ➕ extra — a read-only agent detail screen the reference lacks |
---
## §1 — Permission (rid) mapping — D1
D1 asked for a complete "reference admin operation ↔ target rid" matrix and an
explanation of the **172 vs 174** unique-key difference.
### The real counts
The Phase 0 figure was approximate. Counted from the authoritative catalogues
| `persons.list.sellers.view` | target only, on `/reagent/view/:id` | ➕ guards the new agent-detail route with an existing catalogue rid — correct, no new permission invented |
**Verdict: ✅ the route-guard layer is at parity.** Every reference route's rid is
either identical or maps onto the protected dashboard consolidation.
---
## §2 — Reference admin operation ↔ target rid
The reference leaves all its admin routes unauthenticated
(`henza_server/routes/admin.php:12,272`); the target enforces JWT + rid on every
one. **D1 accepted this as a deliberate deviation** — the requirement was to
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/`:
**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.
| PERM-01 | Medium | `dashboard.omde` / `dashboard.tak` are seeded into `permission` but absent from the permission UI, so they are grantable yet unmanageable; DB has 178 distinct rids vs the UI's 174. |
| Formula called anywhere | on every variant, via `$appends = ['price', …]` (`ProductVariant.php:115-119`) | **never** — `grep computeVariantPrice` outside its own file and spec returns nothing | ❌ missing |
| Gold price injected | `AdminController.php:97-98` sets `ProductVariant::$goldPrice = WebServiceGold::getPriceGold18Ayar()` before listing | no equivalent | ❌ missing |
| `price` in the variant payload | appended automatically | `variantRow()` (`admin-products.service.ts:188-208`) returns **no `price`** | ❌ missing |
| Displayed in the product list | `henza_admin/src/views/apps/product/ProductList.vue:283,287,301` — `{{ item.price }}` in three places | no price column | ❌ missing |
| Extra percent source | `ConfigStatic['product_price_extra_percent']`, default 7 (`ProductVariant.php:50`) | the ported function defaults to 7 but nothing reads the config row | ❌ missing |
| 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) |
(`orders.service.ts:40-52`) — **MON-08**, fixed as Phase 2 item #1 per the
owner's ruling, since `henza_pwa` computes rather than collects them.
---
## Issues raised by §3–§4
| ID | Severity | Summary |
|---|---|---|
| PRICE-01 | **Critical** | The price formula is ported and tested but **wired to nothing**: no gold price is fetched, `variantRow()` returns no `price`/`price_with_discount`, and the product list has no price column. The reference shows a price on every variant. |
| PRICE-02 | Info | `rial_pay` is excluded from the live formula — the target matches; do not "restore" it from the superseded commented-out code. |
| 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) |