parity(apps+api): report + report/detail on the internal ledger [ref: henza_pwa views/report]
Ports "گزارش" end to end (D24), the last missing app page.
In the reference this feature computes nothing: Customer\TahesabController
proxies an external accounting system (hesab.henzagold.com and a bare IP at
31.7.68.147:3030) with TLS verification disabled, a bearer token hardcoded in
the source and a URL-embedded secret, keyed on Customer.mcode. The target has
no such system and no mcode, so per the owner's option الف this is built on our
own ledger with no external connection.
API — customer/me/report{,/detail,/balances,/:id}
- one row per CONFIRMED ledger entry with the running gold and rial balance
after it, newest first as the reference displayed it
- only confirmed entries count, the same rule the cached
Customer.goldDebtGrams/fiatDebtRial follow, so the statement's closing
balance agrees with the figure shown everywhere else in the app
- fields the target has no source for (foreign currency, نام کارساخته,
جنس فلز, the external system's internal ids) are null/zero and the UI OMITS
the row rather than rendering a fabricated value (REP-01)
SEC-16, NOT ported: the reference's detail($id) took the factor code from the
URL and performed no ownership check at all — it never consults auth() — so any
signed-in customer could read any other customer's document by guessing a code.
Here the id is only a filter on top of a customerId scope from the token.
Tested in both directions.
Apps (both, template+script byte-identical, per-app styling only)
- ReportView with the reference's two tabs, its card rows and its «جزئیات»
modal; ReportDocument for one document
- routes /report and /report/detail, the latter opening the second tab
- dashboard entry (v1 tile, v2 row), shown to every signed-in customer since
the reference's hasMCode() gate has no target equivalent
Verified: build 5/5 · jest 45/45 · report 40/40 (new) ·
complete-profile 38/38 · apps smoke 43/43 · typecheck clean on all four apps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5ad1083aee
commit
3f549f1749
@@ -1301,3 +1301,40 @@ clear warning; the lower-risk option was chosen (D19), since failing the boot
|
||||
could take down a running deployment that has not set the variable yet. A
|
||||
banner as loud as the master-OTP one now fires at boot, and DELIVERY.md lists
|
||||
it as the **first** pre-release step (REL-03).
|
||||
|
||||
## D24 — «گزارش» (report + report/detail) on the internal ledger, 2026-09-23
|
||||
|
||||
**In the reference this feature computes nothing.** `Customer\TahesabController`
|
||||
(henza_server `app/Http/Controllers/Customer/TahesabController.php`) forwards
|
||||
every call to an external accounting system — `hesab.henzagold.com` and a bare
|
||||
IP at `31.7.68.147:3030` — with `Http::withoutVerifying()` (TLS verification
|
||||
off), a bearer token hardcoded in the source and a URL-embedded `sfx` secret,
|
||||
keyed on `Customer.mcode`, the customer's id in that other system. The PWA
|
||||
renders whatever comes back.
|
||||
|
||||
The target has no such system and no `mcode`. **The owner chose option الف:**
|
||||
build the reference's UI and flow over our own ledger, with no external
|
||||
connection. So `customer/me/report*` reproduces the statement's SHAPE — the
|
||||
field names `henza_pwa/src/type/accounting.ts` declares, so the ported
|
||||
components bind unchanged — from `LedgerEntry`.
|
||||
|
||||
**Honesty rule.** Every figure is computed from confirmed ledger entries.
|
||||
Where the external system carried a column the target has no equivalent for
|
||||
(foreign currency, وزن پلاستیک, نام کارساخته, جنس فلز and its internal
|
||||
bookkeeping ids), the field is `null`/zero and the UI **omits the row** rather
|
||||
than rendering a fabricated value. Recorded as REP-01.
|
||||
|
||||
**Two things the target does better, deliberately:**
|
||||
- Only **confirmed** entries count, the same rule the cached
|
||||
`Customer.goldDebtGrams` / `fiatDebtRial` follow — so the statement's closing
|
||||
balance agrees with the figure shown everywhere else in the app. Asserted by
|
||||
the test.
|
||||
- **SEC-16:** the reference's `detail($id)` took the factor code from the URL
|
||||
and performed **no ownership check whatsoever** — it never looks at `auth()`
|
||||
— so any signed-in customer could read any other customer's document by
|
||||
guessing a code. Here the id is only ever a filter on top of a `customerId`
|
||||
scope, so someone else's id returns nothing. Tested in both directions.
|
||||
|
||||
The reference gated the page on `myStore.hasMCode()`; with no mcode in the
|
||||
target the gate is simply a session, and the server scopes every row to the
|
||||
token.
|
||||
|
||||
@@ -156,3 +156,12 @@ Severity: `Critical` (money loss / security / data loss) · `High` · `Medium`
|
||||
| **SEC-15** | Low (reference bug, NOT ported) | REF `Major/CustomerController.php:110-112` | `sendSmsToAdmin()` hardcoded four real personal mobile numbers in the source. | `$admin_mobile = ['0990…','0913…','0912…','0919…'];` | **Not ported.** The target reads `PROFILE_ADMIN_MOBILES` (comma-separated, blank = notify nobody), so no personal number is committed to this repository. |
|
||||
| **APP-03** | Low (reference bug, NOT ported) | REF `completeProfile.vue:175-181` | `refresh()` called `window.setInterval(() => window.location.reload(), 1000)` — an **unterminated** timer that reloaded the whole page every second forever, not a one-off refresh. | `completeProfile.vue:178-180` | **Not ported.** The target navigates home once after the success dialog. |
|
||||
| **UPL-01** | Medium (gap, fixed) | TGT `admin-uploads.controller.ts` | The reference let the app upload documents through the same `file/create` the admin panel used, inside its `auth:api-customer` group (`routes/api.php:95`). The target's copy of that route is **admin-guarded**, which no app token can pass — so without a customer-facing upload route, complete_profile could not have worked at all. | `AdminUploadsController` has no `@CustomerAuth()` route | ✅ Added `POST customer/me/documents`, reusing `AdminUploadsService` unchanged so the size limit, MIME whitelist and stored-name scheme are identical on both paths. The uploader is recorded as `null` rather than as an admin, since that column is an AdminUser FK. |
|
||||
|
||||
## report — found while porting the page (2026-09-23)
|
||||
|
||||
| ID | Severity | Where | Description | Evidence | Suggested fix |
|
||||
|---|---|---|---|---|---|
|
||||
| **SEC-16** | High (reference bug, NOT ported) | REF `Customer/TahesabController.php::detail()` | The document-detail endpoint took the factor code straight from the URL and performed **no ownership check at all** — the method never consults `auth()`. Any signed-in customer could read any other customer's accounting document by guessing or enumerating a code. | `public function detail(Request $request, $id)` → proxies `GetHesabDetailByFactorCode?Factor_Code=$id` with no customer scope | **Not ported.** In the target the id is only a filter on top of a `customerId` scope taken from the token, so another customer's id returns nothing. Asserted in both directions by `test-report.cjs` §6. |
|
||||
| **REP-01** | Medium (documented gap) | REF external Tahesab system vs TGT `LedgerEntry` | The reference's statement carried columns the target has no source for: foreign currency (`rial_V2`/`rial_Kh2`/`tahesabP2`), `vaznPlastik`, `nO_kar` (نام کارساخته), `jens_felez` (جنس فلز) and the external system's internal ids (`gid`/`rec`/`reg`). | `henza_pwa/src/type/accounting.ts`; `AccountingDetail.vue:34-46` | The API returns `null`/zero for these and the UI **omits** the row rather than showing a fabricated value. If the business needs them, they need columns on `LedgerEntry` first. |
|
||||
| **REP-02** | Low (performance) | TGT `customer-report.service.ts` `summary()` | The running balance makes a row depend on every entry before it, so the whole confirmed ledger for the customer is folded in memory and the page sliced afterwards. Cheap at one customer's own document counts; linear in their history. | `summary()` `findMany` without `take` | If a customer ever accumulates enough entries for it to matter, move the fold to a stored running total written at entry time. |
|
||||
| **SEC-17** | Medium (reference, not applicable to target) | REF `Customer/TahesabController.php` | The reference calls the external accounting system with `Http::withoutVerifying()` (TLS certificate verification **disabled**) against a bare IP, with a bearer token and an `sfx` secret hardcoded in the source and embedded in the URL. | `->withToken("S3F1P8…")`, `sfx=wcG97tkF…` in four URLs | Not applicable — the target makes no external call. Listed so it is not reintroduced if a Tahesab integration is ever added; the credentials in that reference file should be rotated regardless. |
|
||||
|
||||
Reference in New Issue
Block a user