233 lines
11 KiB
Markdown
233 lines
11 KiB
Markdown
# 01 — ADMIN PARITY MATRIX (Phase 1)
|
||||
|
|
|
|||
|
|
Branch: `parity/phase-1-admin`, cut from `parity/security-hotfix` so the security
|
|||
|
|
work is preserved (owner's instruction).
|
|||
|
|
|
|||
|
|
Method: the `reference-parity-audit` skill, **rule C0 — field-by-field, never
|
|||
|
|
page-by-page** (D12). Every row cites `path:line` on both sides.
|
|||
|
|
|
|||
|
|
Status legend: `✅ match` · `❌ missing` · `⚠️ different` · `🛡️ protected` ·
|
|||
|
|
`➕ 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
|
|||
|
|
(`henza_admin/src/views/user/permissionConst.js` ↔
|
|||
|
|
`apps/admin-panel/src/views/user/permissionConst.ts`):
|
|||
|
|
|
|||
|
|
| | Reference | Target |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Tree entries | 181 | 179 |
|
|||
|
|
| **Unique keys** | **176** | **174** |
|
|||
|
|
| Duplicate keys | 5 | 5 (the same 5) |
|
|||
|
|
|
|||
|
|
The 5 duplicates are identical on both sides — `persons.list.client.single.view`,
|
|||
|
|
`product.list.view`, `order.list.view`, `order.tamin.view`, `cities.list.view` —
|
|||
|
|
each appearing twice in the tree because it is reachable from two parents. The
|
|||
|
|
target reproduces this faithfully.
|
|||
|
|
|
|||
|
|
### The entire 176 → 174 difference is the dashboard, and nothing else
|
|||
|
|
|
|||
|
|
| Key | Reference | Target | Status |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `dashboard` (parent) | present, `permissionConst.js:3` | absent | ⚠️ see below |
|
|||
|
|
| `dashboard.omde` (آمار عمده فروشی) | `permissionConst.js:7` | absent from the UI tree | ⚠️ |
|
|||
|
|
| `dashboard.tak` (آمار تک فروشی) | `permissionConst.js:11` | absent from the UI tree | ⏸️ storefront (D13) |
|
|||
|
|
| `dashboard.view` | — | `permissionConst.ts` | ➕ replaces the three above |
|
|||
|
|
|
|||
|
|
**176 − 3 + 1 = 174.** Every other one of the 173 shared keys matches exactly.
|
|||
|
|
|
|||
|
|
**Cause:** the admin **Dashboard is a PROTECTED zone** (`PROTECTED.md` §A) — the
|
|||
|
|
monorepo replaced the reference's two separate dashboards (wholesale stats and
|
|||
|
|
retail stats) with one redesigned dashboard, so it needs one permission instead
|
|||
|
|
of two.
|
|||
|
|
|
|||
|
|
**Verdict: ✅ accepted, not a gap.** Collapsing the three keys into one follows
|
|||
|
|
directly from the protected redesign. Reversing it would mean restoring the
|
|||
|
|
two-dashboard split, which is exactly what PROTECTED forbids.
|
|||
|
|
|
|||
|
|
### ⚠️ But there IS a real defect here — PERM-01
|
|||
|
|
|
|||
|
|
The **database** carries all three rids:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
SELECT rid FROM permission GROUP BY rid -> 178 distinct
|
|||
|
|
dashboard rids present: dashboard.omde, dashboard.tak, dashboard.view
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
So `dashboard.omde` and `dashboard.tak` are **grantable in data but invisible in
|
|||
|
|
the permission screen**. An operator cannot see, grant or revoke them, yet a row
|
|||
|
|
may already hold them, and nothing reads them any more. Two consequences:
|
|||
|
|
|
|||
|
|
1. Permissions exist that no UI can manage — they can only be changed by editing
|
|||
|
|
the database directly.
|
|||
|
|
2. The DB has **178** distinct rids against the UI's **174**, so the two
|
|||
|
|
disagree about what the permission set even is.
|
|||
|
|
|
|||
|
|
Logged as **PERM-01**. The fix is additive and safe: stop seeding the two dead
|
|||
|
|
rids, and clean up any rows that hold them — **not** done yet, because deleting
|
|||
|
|
permission rows is a data deletion and needs the owner's approval (and a
|
|||
|
|
CHANGELOG entry per the standing rule).
|
|||
|
|
|
|||
|
|
### Route-guard rids
|
|||
|
|
|
|||
|
|
`meta.role` on each route is the rid the guard checks
|
|||
|
|
(`henza_admin/src/router.js:193-195` ↔ the target's guard).
|
|||
|
|
|
|||
|
|
| | Reference | Target |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Route entries carrying a role | 77 | 79 |
|
|||
|
|
| **Unique rids used by routes** | **57** | **57** |
|
|||
|
|
|
|||
|
|
Differences — the same dashboard story, plus one:
|
|||
|
|
|
|||
|
|
| Rid | Where | Status |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `dashboard.tak` → `dashboard.view` | ref `router/admin.js:13` | ✅ protected-zone consequence |
|
|||
|
|
| `dashboard.omde` → `dashboard.view` | ref `router/admin.js:31` | ✅ protected-zone consequence |
|
|||
|
|
| `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/`:
|
|||
|
|
|
|||
|
|
| Reference operation (controller) | Target endpoint | rid |
|
|||
|
|
|---|---|---|
|
|||
|
|
| *(pending — filled per module as each section below is audited)* | | |
|
|||
|
|
|
|||
|
|
> Populated as §3 onward completes, so no row is written before both sides have
|
|||
|
|
> been read.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §3+ — Field-by-field sections
|
|||
|
|
|
|||
|
|
To be filled, in the fix order given in the Phase 1 summary:
|
|||
|
|
|
|||
|
|
- §3 Price formula and product/variant
|
|||
|
|
- §4 Orders and the `gram_level` limit
|
|||
|
|
- §5 Financial documents (اسناد)
|
|||
|
|
- §6 Customers (اشخاص)
|
|||
|
|
- §7 Users, permissions, cities, branches, workshops
|
|||
|
|
- §8 Promotions, SMS, notifications, newsletters, statics
|
|||
|
|
- §9 Reports
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Issues raised by this section
|
|||
|
|
|
|||
|
|
| ID | Severity | Summary |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 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. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §3 — Price formula and product pricing — **the top critical gap**
|
|||
|
|
|
|||
|
|
### PRICE-01 — the target computes no product price at all ❌ CRITICAL
|
|||
|
|
|
|||
|
|
| Dimension | Reference | Target | Status |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Formula implemented | `app/Models/ProductVariant.php:183-190` (`getPriceAttribute`) | `apps/api/src/modules/pricing/variant-price.ts` — ported today, **verbatim + 11 hand-checked tests** | ⚠️ exists but unused |
|
|||
|
|
| 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 |
|
|||
|
|
| `price_with_discount` | appended (`ProductVariant.php:118`) | absent | ❌ 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 |
|
|||
|
|
|
|||
|
|
**Impact.** Every admin screen that shows a product price shows nothing. This is
|
|||
|
|
the single largest behavioural gap in the admin panel and it is money-facing, so
|
|||
|
|
it is **fix #1**.
|
|||
|
|
|
|||
|
|
**What is already done:** the formula itself is ported and proven — the live
|
|||
|
|
version at `:183-190`, not the three superseded variants above it that apply the
|
|||
|
|
9% to the whole price (a test asserts the difference: 39,986,000 vs 42,686,000).
|
|||
|
|
|
|||
|
|
**What remains:** read `product_price_extra_percent` from settings, fetch the
|
|||
|
|
current gold price, call `computeVariantPrice`, and surface `price` /
|
|||
|
|
`price_with_discount` in `variantRow()` and the product list column.
|
|||
|
|
|
|||
|
|
### PRICE-02 — `rial_pay` (flat rial charge per variant) ⚠️
|
|||
|
|
|
|||
|
|
`ProductVariant.php:163` adds `$this->rial_pay` in the **superseded** formula
|
|||
|
|
only; the live formula at `:183-190` does not. The target notes the column is
|
|||
|
|
"returned as null rather than faked" (`admin-products.service.ts:206-208`).
|
|||
|
|
**Matches the live reference** — no action. Recorded so a later reader does not
|
|||
|
|
"restore" it from the commented-out code.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §4 — Orders, the credit limit, and debt recognition
|
|||
|
|
|
|||
|
|
Contrary to the Phase 0 expectation, this area is in **good** shape.
|
|||
|
|
|
|||
|
|
| Rule | Reference | Target | Status |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Credit-limit check | `ApiMobileController.php:568` — `($allGram + $remain) > $gramLevel` | `orders.service.ts:88-92` — `totalGoldWeightGrams.add(goldDebtGrams).gt(goldCreditLimitGrams)` | ✅ match, including the strict `>` |
|
|||
|
|
| 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 |
|
|||
|
|
|
|||
|
|
The one **money hole** here is already logged and scheduled: the server accepts
|
|||
|
|
client-supplied `wagePercent` / `discountPercent` / `flatRialCharge`
|
|||
|
|
(`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. |
|