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>