First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
17 KiB
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:
$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-190exactly, including thefloor(/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:12commented). 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_pwaand 18 inhenza_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, outsideapps/): onlypackage.json+ an outdatedvite.config.ts, ignored by pnpm-workspace. Delete, or leave?