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>
12 KiB
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:
- Permissions exist that no UI can manage — they can only be changed by editing the database directly.
- 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_levellimit - §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_discountis 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.tsand 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. |