Adds computeVariantPrice(), a verbatim port of the live formula at
henza_server/app/Models/ProductVariant.php:183-190, with the per-customer
making-charge offset as an explicit input defaulting to 0. The three
superseded formulas commented out above it apply VAT to the whole price
and are deliberately not ported (D4).
11 unit tests assert against hand calculations derived from the PHP source
rather than from the implementation, covering: with and without the
offset, VAT on the non-metal portion only, floor-to-nearest-1000, the
extraPercent default of 7, and the equivalence between the app folding the
offset into percent_pay and the server deriving it. 18 tests pass overall.
Also adds the D0 name-split review report script (read-only) and two
investigations the owner asked for:
- SMS: the reference uses the Trez panel (smspanel.trez.ir, Sms.php:69,97)
with production credentials committed to git (SEC-12 — rotate them). All
12 message templates catalogued; env var names proposed; nothing
implemented pending approval.
- MON-08: henza_pwa computes percent_pay, discount and rial_pay rather
than asking the user (cart.vue:170-179, Mixins/product/index.ts:69-75),
so Phase 2 item #1 is to derive them server-side and ignore the client's
values. rash_weight stays read from the DB as a recorded deviation.
Records the owner's two new standing rules: migrations only through the
standard Prisma workflow, and every data deletion logged in CHANGELOG.
[ref: henza_server/app/Models/{ProductVariant,Sms}.php, henza_pwa/src/views/cart.vue]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Stage 1 narrowed GET /customers but did not close the leak: the response still
carried every customer id, and /customers/:id, /:id/orders and
/:id/ledger-entries were still public, so the full customer book was still two
requests away. The user caught this. Stage 2 deletes the id-bearing surface.
Customer JWT, separate from the admin one (T1):
- new src/modules/customer-auth/ with its own passport strategy,
CustomerJwtGuard, @CustomerAuth() and @CurrentCustomer()
- signed with JWT_CUSTOMER_SECRET, and every token carries typ:'customer'
which the strategy verifies; both checks are present deliberately
- customer state re-read per request, so deactivating takes effect at once
- mirrors the reference's own split: auth:api vs auth('api-customer')
Real OTP (D6), porting ApiMobileController::userAuth/userVerfiy (:39-64):
- new CustomerOtp table, additive migration, nothing existing touched
- parity behaviour: rand(1000,9999), active-customer filter, faTOen digit
normalisation on both number and code, rejection on mismatch
- approved improvements over the reference: bcrypt-hashed code, 2-minute
expiry, 5 attempts then lockout, single use, send throttle (60s per
number, 5/number and 20/IP per 10 min), and no number enumeration
Routes removed, replaced by token-derived equivalents:
GET customers, customers/:id, /:id/orders, /:id/ledger-entries
-> GET customer/me, customer/me/orders, customer/me/ledger-entries
No route accepts a customer id any more, so the IDOR is closed structurally
rather than by a check. The admin panel was unaffected (admin/customers-omde/*).
Both apps migrated in the same commit per the v1/v2 parity rule: token-based
session store, Bearer header, 401 -> clear session and route to login, real
two-step OTP login. Each app keeps its own markup and styling; only logic moved.
Tests: auth-smoke.sh extended with the stage-2 assertions, 46 passed 0 failed
against a live server. jest installed dev-only (user-approved) so pnpm test
works for the first time: 7 passed covering faToEn, which moved to its own
digits.ts to be testable without booting Nest. Typecheck clean on api,
user-app and user-app-v2; both apps build.
[ref: henza_server ApiMobileController.php:39-78]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Authorised by rulings D7 and T1-T3 in docs/parity/DECISIONS.md. This is the
CLAUDE.md section 3 carve-out: matching the reference here would keep a money
write path open to anonymous callers.
Closed, each behind the rid the legacy vocabulary already had:
GET accounting/customers/:id/balance -> wallet.list
POST accounting/customers/:id/reconcile -> wallet.charge
POST accounting/repayments/gold -> document.create
POST accounting/repayments/rial -> document.create
POST accounting/entries/:id/confirm -> document.list.confirm
PATCH orders/:id/status -> order.list.sts.submit
Four of the accounting routes WRITE to the ledger. The order-status route was
worse in kind: its DTO takes a client-supplied actorAdminUserId, so an
anonymous caller could move any order and pin it on any admin. It had no
caller - the admin panel posts to admin/orders/status.
GET /customers kept its public status (both apps' login still matches the
phone number in the browser) but now selects only id/mobile/isActive/
isArchived. It previously returned every active customer's full name, national
code, both debts, credit limit and the URLs of their identity-document scans.
CustomerSummary narrowed in BOTH user apps in the same commit, per the
v1/v2 parity rule.
Not changed: /uploads stays public per ruling T2. The filename check requested
alongside it passed unchanged - md5(time + name + uuid), no directory listing.
Tests: apps/api/scripts/auth-smoke.sh asserts 401 without a token, 403 with a
low-privilege token (ut 88, 5 unrelated rids) and pass-through for a super
admin on every closed route, plus that the public routes still answer 200 and
that GET /customers exposes only the four login fields. 23 passed, 0 failed
against a live server. Typecheck clean on api, user-app and user-app-v2.
[ref: henza_server/routes/admin.php:12,272; ApiMobileController.php:39-78]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 0 gate passed. All rulings recorded in docs/parity/DECISIONS.md and
applied to PROTECTED.md, PROGRESS.md and ISSUES.md. No source code changed.
Urgent investigations requested by the user, all evidence-cited:
- percent trace: the target's implementation is WRONG. `percent` is the
customer's OWN column in the reference (customer_add.php:32), a per-customer
making-charge offset used to shift catalogue filter bounds
(ApiMobileController.php:391-396,76-77). The target reads the referring
agent's wagePercent instead (admin-customers.service.ts:245) and never
writes the field at all. It is NOT part of the price formula -- that extra
percent comes from a global ConfigStatic row (ProductVariant.php:50,183).
Two unrelated `percent` keys exist and must not be confused with it.
- @Public audit: 15 routes across 7 controllers, grouped by risk. Group A is
5 ledger routes (4 writes) that contradict decision D1; group B is 3 order
routes incl. a public status change accepting a client-supplied
actorAdminUserId; group C is 8 reads, of which only the catalogue, pricing,
slider and uploads are legitimately public.
- Login leak: it is in the TARGET. GET /customers is public and returns every
active customer with national ID numbers, debts and ID-document URLs.
passwordHash is correctly excluded. Not possible in the reference.
Also logged 5 customer-module defects found in the deep check the user asked
for (CUST-01..05), including the fullName collapse the user reported.
Skill updated per D12: Step C gains rule C0 (field-by-field matrix, never
page-by-page) and Step E gains rule E0 (mandatory 4-step round-trip test).
[ref: henza_server, henza_admin]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read-only inventory of all 7 projects (3 reference + 4 target) produced by
parallel subagents, every claim cited path:line. No source code changed.
- 00-INVENTORY.md consolidated mapping: stack, backend, admin, user apps
- PROTECTED.md draft protected list (awaiting user confirmation)
- ISSUES.md 26 logged issues (security, money/stock, validation, dead code)
- CHANGELOG.md change log scaffold
- PROGRESS.md resumable state + Q1-Q5 blocking Phase 1
- _inv-*.md 7 raw per-project inventories (8111 lines)
Key finding: the reference is a gold-jewellery B2B/B2C e-commerce system, not a
gold bank. Buy/sell, deposit/withdraw, transfer, KYC levels, payment callbacks,
cron and websockets do not exist in it, so they cannot be parity gaps.
[ref: henza_server, henza_admin, henza_pwa]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>