The owner ruled that the reference also ran a retail storefront (B2C
single-customer shop) and that the wallet, cashback and
ConfigStatic['percent'] belong to it. These are preserved exactly as they
are: nothing deleted or disabled on either side, no parity work, listed in
the matrices with status "preserved — out of current scope" and no gap or
work row. Shared surfaces (product, variant, price, order, customer) need
the owner's approval before a change that could reach the storefront.
Classifying the endpoints for that split surfaced a counting error in the
Phase 0 inventory: routes/admin.php has 151 live explicit routes, not the
117 recorded, so the reference total is 412 and not 369 (logged DOC-01).
get-analytics-tak reads SingleCustomerOrders, so it is a storefront route
despite its neutral name (logged DOC-02).
Of the 412 reference endpoints, 109 are retail storefront, 15 are web.php
dev/ops closures, and 288 remain in scope (218 admin + 70 B2B app).
[ref: henza_server/routes/{admin,api,customer,site,web}.php]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
305 lines
17 KiB
Markdown
305 lines
17 KiB
Markdown
# 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<string, unknown>` |
|
||
| 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?
|