Files
s.esmaeiliandClaude Opus 5 c71574b1d2 parity(docs): record D13 — retail storefront preserved but out of scope
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>
2026-09-22 07:23:48 +03:30

305 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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?