Files
tehran_taj/docs/parity/01-ADMIN-MATRIX.md
T
s.esmaeiliandClaude Opus 5 e3af31948e parity(docs): D16 — stop PRICE-01 as storefront, reorder Phase 1, verify SEC-02
Owner ruling: computing and displaying price / price_with_discount is retail
storefront work, so it falls under D13 and is marked out of scope. The B2B
app works in making charge only.

variant-price.ts and its 11 hand-checked tests are kept and still run, just
unused. Nothing needed reverting: computeVariantPrice was never called from
outside its own file and spec, verified by grep over apps/api/src and
apps/admin-panel/src, so no price had been wired into any screen. PRICE-01
is re-marked in ISSUES.md rather than deleted so the evidence survives if
the storefront is ever taken up.

Making-charge work stays fully in scope: percentage_pay,
makingChargeOffsetPercent, and wage display on variants and order lines.

New Phase 1 order: Persons -> products/categories/variants/order detail ->
financial documents -> users and permissions -> admin SMS -> promotions,
notifications, cities, branches, workshops -> reports -> gate. Per section:
implement the gaps, then field-by-field test that section, and show a gap
summary before fixing.

SEC-02 re-verified live rather than quoted: 9/9 assertions, plus 32/32 in
auth-smoke.sh. POST /orders is 401 without a token; a body customerId of
"customer-2" sent with a customer-dev-test token still lands the order on
customer-dev-test and is warn-logged; a foreign order id returns 404 rather
than 403 so ids stay unenumerable; the admin read rejects a customer token.
Client-supplied wagePercent/discountPercent/flatRialCharge remain as MON-08,
already scheduled as Phase 2 item #1.

Also records why the dev OTP log briefly looked broken: a stale API process
still held port 3077, so the new instance died with EADDRINUSE and the
tailed log belonged to the old process. Not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:43:11 +03:30

245 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 — ⏸️ STOPPED BY D16
> **Owner ruling, 2026-09-22.** Computing and showing `price` /
> `price_with_discount` is a **retail-storefront** concern; the B2B app works in
> **making charge (اجرت)** only. PRICE-01 is therefore ⏸️ under D13 and is NOT
> worked on. `variant-price.ts` and its 11 tests are **kept but unused**;
> nothing needed reverting because the function was never wired in.
>
> **Making charge stays fully in scope** — `percentage_pay`,
> `makingChargeOffsetPercent`, and wage display on variants and order lines.
>
> The findings below are retained as evidence for whenever the storefront is
> taken up.
### PRICE-01 — the target computes no product price at all — ⏸️ out of scope
| 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 (historical).** Every admin screen that would show a product price shows
nothing. Under D16 this is storefront behaviour, so it is not a Phase 1 gap.
**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 (only if the storefront is ever taken up):** 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. |