Files
tehran_taj/docs/parity/01-ADMIN-MATRIX.md
T
s.esmaeiliandClaude Opus 5 64e76a8616 parity(admin): start the Phase 1 matrix — rid mapping, price and order sections
Answers D1's 172-vs-174 question with real counts: the catalogues hold 176
(reference) and 174 (target) unique keys, and the ENTIRE difference is the
dashboard. The reference has `dashboard` + `dashboard.omde` + `dashboard.tak`;
the target replaces all three with `dashboard.view`, because the admin
Dashboard is a PROTECTED zone that merged the two reference dashboards.
176 - 3 + 1 = 174. All 173 other keys match exactly, including the same 5
keys that legitimately appear twice in the tree.

Route-guard rids are 57 vs 57, differing only by that dashboard
consolidation plus one target-only route reusing an existing catalogue rid.
All 67 reachable reference admin routes exist in the target at the same
paths; 2 target-only routes are additive.

Three findings logged:

PRICE-01 (critical) — no product price is computed or shown anywhere in the
admin panel. The reference appends price/price_with_discount to every
variant and renders it in three places in its product list; the target's
variantRow() returns neither, nothing fetches a gold rate for the admin
list, and computeVariantPrice() — ported and unit-tested earlier today — is
called from nowhere. This is fix #1.

PERM-01 (medium) — dashboard.omde and dashboard.tak are seeded into the
permission table but absent from the permission UI, so they are grantable
yet unmanageable; the DB holds 178 distinct rids against the UI's 174. The
fix deletes permission rows, so it waits for the owner's approval.

PRICE-02 (info) — rial_pay belongs only to the superseded commented-out
formula; the target correctly omits it. Recorded so it is not "restored".

Contrary to the Phase 0 expectation, the order module is in good shape: the
gram_level credit limit matches the reference exactly including the strict
'>' comparison, and debt recognition fires at the same status.

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

11 KiB
Raw Blame History

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.