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

12 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 — ⏸️ 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.