feat(admin): normalize Persian/Arabic digits in every numeric field

Users typing on a Persian keyboard produced two distinct failures. Fields with
type="number" silently returned an empty string — the browser rejects Persian
digits, so the value was lost and saved as empty or zero. Text fields passed the
Persian characters straight through to the database, where a mobile stored as
۰۹۱۲… never matches 09… again. The secretary form hit the second case with no
validation at all.

Frontend:
- Adds digitsOnly() and the national-code schemas to lib/utils, plus lib/forms
  with numericField()/latinDigitsField() wrappers for React Hook Form fields.
- Converts every type="number" input to type="text" inputMode="numeric" with
  digit normalization; none remain. Fields that legitimately carry non-digits
  (sheba, landline) only get the digits translated, keeping IR and separators.
- Points the patient national-code and mobile schemas at the shared normalizing
  schemas, which accept Persian input instead of rejecting it.
- Drops two duplicate local digit converters in favour of the shared helper.

Backend:
- Adds NumericFieldNormalizerSubscriber, translating digits in whitelisted
  numeric keys of JSON request bodies under /api/v1/ before controllers run, so
  nobat724_front and clinic-pro-tauri are covered too. Translation only — no
  characters are stripped, non-string values and other keys are untouched.

Three component tests asserted on role="spinbutton" and numeric input values;
both are properties of type="number", so they were updated to match the new
text inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-18 10:38:56 +03:30
co-authored by Claude Opus 4.8
parent c103c393f3
commit 00cb9aaa1a
42 changed files with 789 additions and 125 deletions
+33
View File
@@ -39,6 +39,39 @@ Authorization: Bearer <JWT_TOKEN>
---
## Persian digit normalization (global)
Persian (`۰-۹`) and Arabic (`٠-٩`) digits sent in numeric request fields are translated to Latin **server-side, before the controller runs**`src/Shared/EventSubscriber/NumericFieldNormalizerSubscriber.php`. Every client benefits: the React admin panel, `nobat724_front`, and `clinic-pro-tauri`.
Applies to `POST` / `PUT` / `PATCH` requests under `/api/v1/` with a JSON body, recursively through nested arrays.
**Normalized keys:**
```
mobile, mobile_number, telephone, phone, notification_mobile,
national_code, postal_code,
card_number, account_number, sheba, shaba, iban,
price_rials, amount_rials, amount, free_visit_price_rials,
insurance_price_rials, patient_share_rials, visit_price_rials,
duration_minutes, duration, commission_percent, coverage,
coverage_percent, franchise, ceiling, tax_percent,
base_insurance_discount_percent, supplementary_discount_percent
```
Only **digits** are translated — no characters are stripped, so `IR` in a sheba and `-` in a landline survive. Non-string values (`int`, `bool`, `null`) and keys outside the list are untouched, so a name like `منشی شماره ۲` keeps its Persian digit.
```jsonc
// request
{ "mobile_number": "۰۹۱۲۳۴۵۶۷۸۹", "national_code": "۰۰۱۲۳۴۵۶۷۸", "name": "منشی شماره ۲" }
// what the controller sees
{ "mobile_number": "09123456789", "national_code": "0012345678", "name": "منشی شماره ۲" }
```
> Adding a new numeric field to any endpoint? Add its key to `NUMERIC_KEYS` in the subscriber, otherwise Persian digits reach the database.
---
## Modules
| File | Domain | Endpoints |
+2 -2
View File
@@ -79,9 +79,9 @@ Create a secretary for a doctor.
| --------------- | ------------- | -------- | -------------------------------------------- |
| `doctor_uuid` | string (UUID) | ✅\* | Single doctor to assign (legacy/doctor flow) |
| `doctor_uuids` | string[] (UUID) | ✅\* | **Clinic only** — assign one secretary to several clinic doctors at once. When present (non-empty) and caller is `ROLE_CLINIC`, this multi-doctor path is used instead of `doctor_uuid` |
| `mobile_number` | string | ✅ | Secretary's login mobile |
| `mobile_number` | string | ✅ | Secretary's login mobile. Persian/Arabic digits are accepted and normalized server-side — see [README → Persian digit normalization](README.md#persian-digit-normalization-global) |
| `name` | string | ❌ | Full name (نام + نام خانوادگی) → `user_name` |
| `national_code` | string | ❌ | کد ملی منشی (nullable) |
| `national_code` | string | ❌ | کد ملی منشی (nullable). Persian/Arabic digits accepted and normalized |
| `address` | string | ❌ | آدرس منشی (nullable) |
| `password` | string | ❌ | Initial password (auto-generated if omitted) |
| `permissions` | object | ❌ | Permission set (see structure below) |