# 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` | |---|---|---| | Routes / endpoints | **412** (admin 249, api 70, customer 48, site 30, web 15) — corrected, see below | **179** | | 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` | | Seeders | 1, **empty** | `prisma/seed.ts` — user types, 144 rids, admins | The 412 → 179 drop is expected, not automatically a gap: the reference counts `Route::resource` expansions, ~28 dead legacy tables, a `site.php` storefront and 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. ### 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) - Route-by-route mapping of the reference's 412 backend routes (288 in scope after D13) onto the target's 179. - 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?