2026-09-21 18:09:46 +03:30
# 00 — INVENTORY & MAPPING (Phase 0)
Read-only inventory of all four reference/target pairs, plus the mapping tables
that drive Phase 1 and Phase 2.
**Method:** seven parallel read-only subagents, one per project, each citing
`path:line` . Their raw output lives in the `_inv-*.md` files next to this one;
this document is the consolidated map. Claims marked ✔ below were additionally
re-verified by the main session by reading the cited lines directly.
| Detail inventory | Project | Lines |
|---|---|---|
| `_inv-ref-server.md` | `henza_server` (reference backend) | 1313 |
| `_inv-tgt-api.md` | `apps/api` (target backend) | 1179 |
| `_inv-ref-admin.md` | `henza_admin` (reference admin) | 1705 |
| `_inv-tgt-admin.md` | `apps/admin-panel` (target admin) | 1261 |
| `_inv-ref-pwa.md` | `henza_pwa` (reference user app) | 1178 |
| `_inv-tgt-userapp.md` | `apps/user-app` (target v1) | 612 |
| `_inv-tgt-userappv2.md` | `apps/user-app-v2` (target v2) | 700 |
---
## 0. The single most important finding — the domain is NOT gold banking
The kickoff brief asks to audit "gold price, gram/weight, fees, commissions,
spread, wallet/balance, buy/sell orders, deposit/withdraw, transfers, KYC levels,
daily limits, payment callbacks, live price sockets". **Most of that does not
exist in the reference**, so it cannot be a parity gap.
`henza_server` + `henza_pwa` are a **gold-jewellery B2B wholesale + B2C
e-commerce** system (Henza Gold): a catalogue of physical SKUs priced from a gold
rate, a cart, orders, and a gram-denominated customer ledger.
Verified absences in the reference user app (`_inv-ref-pwa.md` §6.2):
| Brief's flow | Reality in `henza_pwa` |
|---|---|
| buy gold | ABSENT — nearest is add-to-cart for a physical jewellery SKU |
| sell gold | ABSENT |
| wallet | ABSENT — a read-only balance strip on the dashboard |
| deposit | ABSENT — nearest is submitting a *findoc* of type چک/حواله/آبشده |
| withdraw | ABSENT |
| transfer | ABSENT — findoc type 2 (`حواله` ) only records a transfer note |
| KYC levels | PARTIAL — two flat flags, no levels |
| addresses | ABSENT — one free-text field on the user |
| tickets / support | ABSENT — a static WhatsApp FAB (`index.html:59-61` ) |
| daily/min/max limits | only one exists: `gram_level` (`ApiMobileController.php:528-758` ) |
| live price socket | ABSENT — `BROADCAST_DRIVER=null` , zero `ShouldBroadcast` events ✔ |
| payment callback | route `site/transaction/verify` is **not registered** anywhere |
| cron | `Console/Kernel::schedule()` is **empty** |
**Consequence for the plan:** the parity matrices will cover what the reference
actually has — catalogue, pricing formula, cart/orders, ledger ("اسناد"), customer
management, ACL — and will not manufacture rows for gold-banking features. Where
the *target* has something bank-like, it is an intentional addition, not a gap.
---
## 1. Stack mapping
| Layer | Reference | Target | Note |
|---|---|---|---|
| Backend | Laravel/PHP, 46 controllers, 57 models | NestJS 10 + Prisma 5, 25 modules, 29 models | total rewrite; port behaviour only |
| Admin | Vue 2 + Vuesax/Vuexy template, Vuex | Vue 3.5 + ant-design-vue 4.2.6, Pinia | total rewrite |
| User app | Vue 3 + Vite + vant + ant-design-vue 3 | Vue 3 + Vite, two design versions | closest pair |
| Money types | PHP floats throughout | `Prisma.Decimal` , **zero `Float` in schema** ✔ | target is *stricter* — keep it |
---
## 2. Backend mapping — `henza_server` ⇄ `apps/api`
### 2.1 Scale
| Metric | `henza_server` | `apps/api` |
|---|---|---|
2026-09-22 07:23:48 +03:30
| Routes / endpoints | **412** (admin 249, api 70, customer 48, site 30, web 15) — corrected, see below | **179** |
2026-09-21 18:09:46 +03:30
| Controllers | 46 | 25 |
| Models | 57 files (34 migrated tables + ~28 legacy Django-era tables) | 29 Prisma models |
| Enums | PHP static arrays | 14 Prisma enums |
| DTO / validation | **zero FormRequests** , all inline, several endpoints unvalidated | 8 DTO classes; most admin bodies untyped `Record<string, unknown>` |
| Seeders | 1, **empty** | `prisma/seed.ts` — user types, 144 rids, admins |
2026-09-22 07:23:48 +03:30
The 412 → 179 drop is expected, not automatically a gap: the reference counts
2026-09-21 18:09:46 +03:30
`Route::resource` expansions, ~28 dead legacy tables, a `site.php` storefront and
2026-09-22 07:23:48 +03:30
a `web.php` that the monorepo replaces with its own apps. **Which of the in-scope
routes are genuinely unmapped is Phase 1 / Phase 2 matrix work**, per route group.
** ⚠️ Count correction (2026-09-22).** The **369** reported throughout Phase 0 is
**wrong** ; the real total is **412** . Recounting the
live routes (excluding the 6 commented-out ones, expanding `Route::resource` × 7
and `Route::match(['get','post'])` × 2) gives **412** . The error was in
`admin.php` : §3.6 of `_inv-ref-server.md` recorded 117 explicit routes where the
file actually has **151** , so admin is **249** , not 215. Verified with
`grep -E "^\s*Route::(get|post|…)" | grep -v "^\s*//"` per file.
| File | Explicit | `match` × 2 | `resource` × 7 | Total |
|---|---|---|---|---|
| `admin.php` | 151 | 0 | 14 → 98 | **249** |
| `api.php` | 61 | 1 → 2 | 1 → 7 | **70** |
| `customer.php` | 18 | 1 → 2 | 4 → 28 | **48** |
| `site.php` | 30 | 0 | 0 | **30** |
| `web.php` | 13 | 1 → 2 | 0 | **15** |
| **Total** | | | | **412** |
**Scope split (D13, 2026-09-22).** The retail storefront is preserved but out of
scope, so the 412 divide as follows:
| Group | Endpoints | Scope |
|---|---|---|
| Admin panel — B2B | 218 | ✅ Phase 1 |
| B2B mobile app (`api.php` ) | 70 | ✅ Phase 2 |
| Retail storefront — customer API (`customer.php` ) | 48 | ⏸️ preserved, out of scope |
| Retail storefront — public site API (`site.php` ) | 30 | ⏸️ preserved, out of scope |
| Retail storefront — admin management (wallet 6, `single` 15, `comment` 9, `get-analytics-tak` 1) | 31 | ⏸️ preserved, out of scope |
| `web.php` dev/ops closures, price cron, redirects | 15 | ⏸️ out of scope (dead/dev) |
| **Total** | **412** | **288 in scope · 124 out** |
**109 of the 412 are retail storefront** ; 15 more are `web.php` dev/ops. Full
classification with evidence: `DECISIONS.md` § D13.
2026-09-21 18:09:46 +03:30
### 2.2 Core business logic — where each rule lives
| Rule | Reference | Target |
|---|---|---|
| **Gold price formula** ✔ | `app/Models/ProductVariant.php:183-190` | `apps/api/src/modules/...` (Phase 1 to map) |
| Price source | `app/Models/WebServiceGold.php` — `getPriceGold18Ayar()` divides by 10 (rial→toman) | `pricing` module |
| Order pipeline | `app/Http/Controllers/Controller.php` — `createSingleCustomerOrders` , `afterCreateOrder` , cashback at sts 6 | `orders.service.ts` , `admin-orders.service.ts` |
| Wholesale order + the only limit | `ApiMobileController.php:528-758` (`gram_level` ) | `orders.service.ts` |
| Retail checkout | `Customer/OrderController.php:68-168` | `orders.service.ts` |
| Ledger / wallet | `packages/rooyesh/wallet/` — hash chain, pepper `"yhoenszeaf"` | `accounting/ledger.service.ts` (dual-currency, `Prisma.Decimal` ) |
| Cashback | `Adm/AdminController.php:985-1013` (`chargeWalletAfterOrder` , in grams) | `admin-customers.service.ts` |
| Order statuses | **two divergent enums** : `Order.php:66` (0– 6) and `SingleCustomerOrders.php:67` (1– 7) | `OrderStatus` + `PreOrderStatus` |
| SMS / OTP | `app/Models/Sms.php` | **absent** — DB persistence only |
#### The gold price formula, verified ✔
`henza_server/app/Models/ProductVariant.php:183-190` :
```php
$price = goldPrice + ( goldPrice * (( percentage_pay + product_price_extra_percent ) / 100 ));
$price += $price * 0.07 ; // 7% margin
$temp = $price - goldPrice ; // non-metal portion only
$temp += $temp * 0.09 ; // 9% VAT on that portion only
$price = goldPrice + $temp ;
$price *= grams ;
return floor ( $price / 1000 ) * 1000 ; // round DOWN to the nearest 1000
```
Three superseded variants sit commented out above it — they apply VAT to the
whole price, which is **wrong** and must not be ported. `getPriceWithDiscountAttribute`
(`:255-280` ) is the same formula with `0.07 - discount_percent/100` and a trailing
`price -= price * (discount_price/100)` .
This formula, its ordering, and the `floor(/1000)*1000` are **row #1 of the Phase 1
matrix** — it is the money rule the whole catalogue depends on.
### 2.3 Auth & permissions
| Aspect | Reference | Target |
|---|---|---|
| Admin route protection | **NONE** ✔ — `auth:api` commented at `routes/admin.php:12` and `:272` , so all ~215 admin routes are public | global `JwtAuthGuard` + `PermissionsGuard` ; 15 routes opt out with `@Public()` ✔ |
| Permission model | `Permission(rid, ut)` rows returned at login (`AdminController:74` ), enforced **client-side only** — no Gates/Policies/Spatie | `acl.constants.ts` — 144 rids + `expandWithParents()` , enforced **server-side** |
| Permission catalogue source | not derivable from the server; lives in the admin at `views/user/permissionConst.js` (**172 declared keys**) | `acl.constants.ts` (144 rids) + `permissionConst.ts` (**179 keys, 174 unique**) |
| Parent expansion | `store/users/index.js:3-21` `checkParent()` — `order.list.sts.remove` implies `order.list.sts` , `order.list` , `order` | `expandWithParents()` — same semantics |
| Token refresh | machinery exists but is **unreachable** (`main.js:22-23` commented) | none |
**The target is deliberately stricter than the reference on auth.** That is a
prior, documented decision (`docs/CHANGELOG-PORT-HISTORY.md` ), not a parity gap —
and reverting it would reopen a critical hole. Logged as `SEC-01` /`SEC-02` .
**This needs your confirmation (see Q1).**
---
## 3. Admin mapping — `henza_admin` ⇄ `apps/admin-panel`
| Metric | `henza_admin` | `apps/admin-panel` |
|---|---|---|
| `.vue` under `views/` | **541** — but only ~69 reachable from the live router; the rest are Vuexy template demos | **91** `.vue` + 19 `.ts` table models = 110 files |
| Routes | admin build + branch build (two route trees) | 80 admin-mode + 15 branch-mode records |
| Permission keys | 172 in `permissionConst.js` | 179 (174 unique) in `permissionConst.ts` |
| Rids enforced by `meta.role` | ~58 | ~58 |
| Button-level `$hasPermission` | catalogued in `_inv-ref-admin.md` §5.3 | 73 distinct rids via `hasPermission()` |
| HTTP | `src/axios.js` , **no response interceptor** , no working refresh | `src/api/client.ts` + `endpoints.ts` legacy→REST map (~100 exact + 34 regex), 401 → clear token + redirect |
| Table system | `src/components/x-table/` (4 forked copies) | one `XTable` , POST `{search, filters, page, limit, order, select, titles, report_title}` → `{rows, query}` |
| Digit/price utils | **none exist** (`_inv-ref-admin.md` Appendix B #17 ) | none, except inside `DateInput.vue` |
**Route-tree note:** `src/router/{apps,category,customer,dashboard,full,manage,order,product,temp}.js`
exist in the reference but are **not imported** — only `admin.js` and `branch.js`
are (`src/router.js:6,8` ). Nine dead route files. They must not be mistaken for
missing pages in Phase 1.
### 3.1 Mapping status
A route-by-route and action-by-action mapping is **Phase 1's deliverable**
(`01-ADMIN-MATRIX.md` ). What Phase 0 establishes is the denominator: ~69 live
reference views against 91 target views, with the reference's 172-key permission
tree against the target's 174-unique-key tree.
---
## 4. User-app mapping — `henza_pwa` ⇄ `user-app` + `user-app-v2`
### 4.1 Routes
| Reference route | name | `user-app` / `user-app-v2` | Status |
|---|---|---|---|
| `/home` | home | `/home` → `HomeView.vue` | ✅ |
| `/collection/:id?` | collection | same | ✅ |
| `/search/:id?` | search | `/search` — **no `:id?` param** | ⚠️ different |
| `/product/:id` | singleProduct | `components/product/SingleProduct.vue` | ✅ |
| `/newest` | newestProduct | same | ✅ |
| `/user` | userPage::hasSubRoute | `DashboardView.vue` | ✅ |
| `/orders` | ordersList | same | ✅ |
| `/transactions` | transactionsList | same | ✅ |
| `/profile` | profileData | same | ✅ |
| `/favorites` | favoritesList | same | ✅ |
| `/cart` | cart | same | ✅ |
| `/login` | login | `LoginRegisterView.vue` | ✅ |
| ** `/report` ** | report | — | ❌ missing |
| ** `/report/detail` ** | reportDetail | — | ❌ missing |
| ** `/ChildOrder` ** | ChildOrder (sales-rep child orders) | — | ❌ missing |
| ** `/customerChoice` ** | customerChoice (impersonation) | — | ❌ missing |
| ** `/complete_profile` ** | completeProfile (document upload / KYC) | — | ❌ missing |
| — | — | ** `/notifications` ** | ➕ extra in target |
Verified by reading `henza_pwa/src/router/index.ts:98-215` against
`apps/user-app/src/router/index.ts:18-45` ✔. The two target routers are
**byte-identical** (`diff` returns nothing) ✔ — good for the v1/v2 parity rule.
### 4.2 Guard differences ✔
| Behaviour | `henza_pwa/src/router/index.ts` | target |
|---|---|---|
| unauth hitting a gated route | `showNotify('ابتدا وارد شوید')` then redirect to ** `home` ** (`:250-254` ) | redirect to ** `/login` ** with `?redirect=` , no toast |
| gated route list | ordersList, transactionsList, profileData, favoritesList, **completeProfile** | `meta.requiresAuth` on 4 (no completeProfile — the route doesn't exist) |
| logged-in hitting `/login` | → home (`:256` ) | → home ✅ |
| `!isLoaded && to.name !== 'home'` | → home (`:258` ) | **absent** |
| `sts_verify_step_two ∈ {1,2}` blocks completeProfile | `:260` | **absent** |
| route analytics | `useCustomAnalytics()` → `POST /log/create/{key}` , `GET /log/update/{id}` (`customAnalytics.ts:25,32` ) | **absent entirely** |
### 4.3 The v1 ⇄ v2 rule
`CLAUDE.md` §2: the only allowed difference is visual design. Five **logic**
differences found, all with v2 behind v1 — these are parity gaps *inside* the
target, independent of the reference:
| # | Gap in `user-app-v2` | Evidence |
|---|---|---|
| 1 | **No cart stock ceiling** — no `capQty` /`maxQty` ; a user can order more units than exist | v1 `stores/cartStore.ts:62,65` , `components/product/CartItem.vue:82-83` ; zero hits in v2 ✔ |
| 2 | No `getHomeSlider()` / `GET /home/slider` admin banner | `diff` of the two `api/client.ts` ✔ |
| 3 | No per-variant images — v1 has `ProductImageSummary{id,url,sortOrder,role}` + `imageId` /`image` ; v2's `images` is `Array<{url}>` | same diff ✔ |
| 4 | No orderable-first variant defaulting | v1 `composables/useProduct.ts:26` |
| 5 | No variant-chip preview cap / per-chip stock badges | v1 `components/product/SingleProduct.vue:388-392` |
Logged as `MON-01` and `V2-01..05` in `ISSUES.md` ; they become Phase 2 matrix rows.
### 4.4 Live price — reference vs target ✔
Neither side has a socket. But the reference has a **two-path bug** worth knowing
before porting: `baseStore.getPriceList()` (`store.ts:107-156` ) and
`Dashboard.getListPrice()` (`Dashboard.vue:632-660` ) both do a one-shot
`GET /price` ; the visible grid comes from the second, while the manual refresh
button re-runs the first — **so the refresh button does not update the list the
user is looking at**. Also `prices.date` /`prices.time` assignments are commented
out (`store.ts:145-147` ), so the dashboard's date/time header is permanently blank.
Target: one-shot `GET /pricing/latest?karatPerMille=750` on mount + pull-to-refresh,
in `HomeView` and `DashboardView` only.
---
## 5. What Phase 0 did NOT determine (goes to Phase 1 / Phase 2)
2026-09-22 07:23:48 +03:30
- Route-by-route mapping of the reference's 412 backend routes (288 in scope after D13) onto the target's 179.
2026-09-21 18:09:46 +03:30
- Field-by-field comparison of request/response shapes per endpoint.
- Per-view form/validation/table comparison for the ~69 live admin views.
- Whether the target's gold-price implementation matches `ProductVariant.php:183-190`
exactly, including the `floor(/1000)*1000` . **This is Phase 1 row #1.**
- Whether the 174 target permission keys are a superset/subset of the reference's 172.
---
## 6. Open questions — need your answer before Phase 1
See the gate message; recorded here so they survive a context reset.
- **Q1 — Auth on admin routes.** The reference leaves all ~215 admin routes
unauthenticated (`routes/admin.php:12` commented). The target enforces JWT +
per-rid permissions. Strict parity would mean *removing* auth. Confirm the
target's stricter behaviour stands as an accepted deviation.
- **Q2 — Scope of the "gold banking" checklist.** Confirm we audit the domain the
reference actually implements (§0) rather than manufacturing rows for
buy/sell/deposit/withdraw/KYC-levels/sockets that exist nowhere.
- **Q3 — The 5 missing user-app routes** (`/report` , `/report/detail` ,
`/ChildOrder` , `/customerChoice` , `/complete_profile` ). These are real reference
features absent from both target apps. Building them means ~5 screens × 2 designs
plus backend. In scope for Phase 2, or deferred?
- **Q4 — Reference bugs.** 20 defects found in `henza_pwa` and 18 in `henza_admin`
(listed in the `_inv-*` appendices). `CLAUDE.md` §3 says replicate + log. Confirm
we do **not** replicate the data-destroying / clearly-broken ones (e.g. the
refresh button that updates an invisible list), and log them instead.
- **Q5 — Stray folder** `gold-banking-monorepo/user-app-v2/` (repo root, outside
`apps/` ): only `package.json` + an outdated `vite.config.ts` , ignored by
pnpm-workspace. Delete, or leave?