From 64e76a86164c0fdae78f57137d82ff8f902d30ae Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Tue, 22 Sep 2026 16:17:31 +0330 Subject: [PATCH] =?UTF-8?q?parity(admin):=20start=20the=20Phase=201=20matr?= =?UTF-8?q?ix=20=E2=80=94=20rid=20mapping,=20price=20and=20order=20section?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/parity/01-ADMIN-MATRIX.md | 232 +++++++++++++++++++++++++++++++++ docs/parity/ISSUES.md | 8 ++ docs/parity/PROGRESS.md | 6 +- 3 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 docs/parity/01-ADMIN-MATRIX.md diff --git a/docs/parity/01-ADMIN-MATRIX.md b/docs/parity/01-ADMIN-MATRIX.md new file mode 100644 index 0000000..7f5819a --- /dev/null +++ b/docs/parity/01-ADMIN-MATRIX.md @@ -0,0 +1,232 @@ +# 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. | diff --git a/docs/parity/ISSUES.md b/docs/parity/ISSUES.md index ab4cf9e..f1f97a0 100644 --- a/docs/parity/ISSUES.md +++ b/docs/parity/ISSUES.md @@ -118,3 +118,11 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium` | ID | Severity | Where | Description | Evidence | Status | |---|---|---|---|---|---| | DEAD-07 | Low | TGT DB table `_goldbank_migrations` + `apps/api/src/prisma/migrate.ts` | **Re-assessed 2026-09-22 — not dead code.** It is the tracking table of the app's OWN migration runner (`src/prisma/migrate.ts:5`), which exists because the Docker deploy image is copy-only and ships no Prisma CLI, so `migrate deploy` cannot run there (`migrate.ts:7-13`). So the project has TWO ledgers on purpose: `_prisma_migrations` (CLI, used in development) and `_goldbank_migrations` (runtime runner, used in the container). `migrate diff` proposed `DROP TABLE` only because the table was not declared in the schema; the runner already refused to execute such a statement (`migrate.ts:190-196`). | Read `src/prisma/migrate.ts`. | ✅ Resolved: declared as an `@@ignore`d model, so drift is now **zero**. Table and all rows kept. | + +## Found in the Phase 1 admin matrix (2026-09-22) + +| ID | Severity | Where | Description | Evidence | Status | +|---|---|---|---|---|---| +| PRICE-01 | **Critical** | TGT `apps/api/src/modules/admin-products/admin-products.service.ts:188-208`; `apps/admin-panel/src/views/apps/product/ProductList.vue` | **No product price is computed or shown anywhere in the admin panel.** The reference appends `price` and `price_with_discount` to every variant (`ProductVariant.php:115-119`), sets the live gold rate before listing (`AdminController.php:97-98`), and renders `{{ item.price }}` in three places in its product list (`ProductList.vue:283,287,301`). The target's `variantRow()` returns neither field, nothing fetches a gold price for the admin list, and `computeVariantPrice()` — ported and unit-tested today — is called from nowhere. | `grep computeVariantPrice src/ --include=*.ts` matches only its own file and spec. | Open — **fix #1 of Phase 1**. | +| PRICE-02 | Info | REF `app/Models/ProductVariant.php:163` vs TGT `admin-products.service.ts:206-208` | `rial_pay` (a flat rial charge per variant) appears only in the **superseded, commented-out** formula, not the live one at `:183-190`. The target returns null rather than faking it, which matches the live reference. Recorded so nobody "restores" it from the dead code. | Read both. | ✅ No action — matches. | +| PERM-01 | Medium | TGT `prisma/seed.ts`; `apps/admin-panel/src/views/user/permissionConst.ts` | `dashboard.omde` and `dashboard.tak` are seeded into the `permission` table but are **absent from the permission UI tree**, so they can be held by a row yet never seen, granted or revoked from the admin screen — and nothing reads them any more (the protected Dashboard uses `dashboard.view`). The DB holds **178** distinct rids against the UI's **174**. | `SELECT rid FROM permission GROUP BY rid` → 178, including all three dashboard rids. | Open — the fix removes permission rows, which is a data deletion and needs the owner's approval first. | diff --git a/docs/parity/PROGRESS.md b/docs/parity/PROGRESS.md index 6c36c9a..d822dd9 100644 --- a/docs/parity/PROGRESS.md +++ b/docs/parity/PROGRESS.md @@ -7,10 +7,10 @@ | Field | Value | |---|---| -| **Phase** | Security hotfix — **stages 1 and 2 ✅ complete** | -| **Branch** | `parity/security-hotfix` | +| **Phase** | **Phase 1 — admin parity, matrix in progress** | +| **Branch** | `parity/phase-1-admin` (cut from `parity/security-hotfix`) | | **Status** | Both stages done and verified live: **46/46** smoke assertions, **7/7** jest unit tests, typecheck + build clean on all three projects. | -| **Next step** | **Start `01-ADMIN-MATRIX.md`** — field-by-field per D12, rid mapping matrix per D1 | +| **Next step** | Owner is reviewing the matrix summary; §3 PRICE-01 is fix #1 once the order is confirmed | | **Blocked on** | nothing | | **Last updated** | 2026-09-22 |