5 Commits
Author SHA1 Message Date
s.esmaeiliandClaude Opus 5.5 2d74f53d0a feat(admin): D16 (b)(c) — Excel order-item report, document-confirm SMS, %name in bulk SMS
Finished from a parallel session that expired (owner: «بساز»).

- RPT-01: GET admin/report/order-items — legacy report/order_item's
  32-column Excel (order items, one row per unit, plus financial
  documents), RTL, Jalali dates. The «گزارش تجمیعی» screen gets
  «دریافت فایل اکسل». Review fixes:
  - weight: computedWeightGrams is already one unit's weight; legacy
    divided it by qty again, under-reporting «بدهکار» on multi-unit
    lines. Not repeated.
  - «درصد»: the final percent (wage − discount), as legacy.
  - dates: the end date covers its whole day; a bad date is a 400.
- SMS-01: confirming or rejecting a financial document texts the customer,
  as legacy CashController::toggleConfiremCash.
- SMS-02: bulk SMS substitutes %name per recipient and is actually sent
  through the configured gateway (D26 relay), no longer only logged.
- Docs: 01-ADMIN-MATRIX and D16-BC-GAP-SUMMARY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:34:45 +03:30
s.esmaeiliandClaude Opus 5.5 be5ca829d2 feat(import): henza → gold-bank data import tool
apps/api/scripts/legacy-import/ reads the legacy MySQL (read only) and
merges into the target Postgres. Every rule is from
docs/DATA-MIGRATION-MAP.md, and each step cites its section. It covers
cities, roles and permissions, branches, admin users, sales experts,
workshops, customers, brands, categories, products, variants, product
images, orders, the ledger, favourites, custom orders and a
reconciliation report.

Key rules:
- Debt is rebuilt from legacy's own formula (updateRemain): delivered
  orders minus confirmed type 1-4 documents. Type 9 «سفارش» documents
  mirror orders and are skipped, or every order's debt would count twice.
  A balance the records cannot explain is carried as one ADJUSTMENT entry.
  In the 2026-10-04 dump that is only customer #125 (owner ruling).
- Merge, not import: rows already in the target are matched by mobile or
  product code, and only their empty fields are filled. Never written:
  accounting* columns, ProductVariant.imageId, Accounting* tables. No
  ProductImage is ever deleted.
- Roles map by title, not number. Legacy 86 is «حسابدار» but target 86
  is «کارشناس فروش». An expert sharing a mobile with an admin never
  demotes the admin.
- Soft-deleted variants and products that live order lines still point
  at (473 of 610 lines) come over inactive, so order history stays whole.
- Owner rulings 2026-10-07: birthdays filtered (541 of 915), typed city
  kept plus exact-match cityId, the 7 legacy rejections get the sentinel
  reason, OTP-only accounts get OTP_ONLY_PASSWORD_HASH.
- Tehran wall-clock timestamps are converted with ICU rules, including
  Iran's pre-2022 DST. Jalali receipt dates are converted with the
  jalaali-js arithmetic (truncating division; verified on 8 known dates).

Safety: --target is required and never read from .env. Nothing is
written without --yes, and a target not named *import*/*staging* also
needs --confirm-db. The id map lives in its own schema, legacy_import,
so a re-run updates instead of duplicating. Writes bypass the API
services, so no SMS is sent and nothing is queued to accounting.

Tested against goldbank_import_test, a clone of the dev DB, in three
runs:
- 1,503/1,503 created customers match legacy gold and rial balances
  (±0.0001 g / ±1 rial)
- accounting data is byte-identical before and after
- 0 imported variants have an imageId
- runs 2 and 3 created zero duplicate rows
- 6,196 image files copied, 3 missing in the legacy storage

New devDependency: mysql2 ^3.24.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:35:18 +03:30
s.esmaeiliandClaude Opus 5 4efe543d62 parity(api,apps): security hotfix stage 2 - real customer sessions, close IDOR
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>
2026-09-22 01:20:00 +03:30
s.esmaeili 267fc7b7d1 feat: complete admin panel and backend admin api migration 2026-09-13 21:02:25 +03:30
s.esmaeili 0ed6bed028 feat: initial commit 2026-09-09 06:46:27 +03:30