Files
clinicpro/tests/Shared/NumericFieldNormalizerTest.php
T
hamedandClaude Opus 4.8 00cb9aaa1a 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>
2026-07-18 10:38:56 +03:30

90 lines
3.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Tests\Shared;
use App\Auth\Entity\User;
use App\Doctor\Entity\Doctor;
use App\Secretary\Entity\DoctorSecretary;
use App\Shared\Util\PersianText;
use App\Tests\ApiTestCase;
/**
* Persian/Arabic digits sent by any client must never reach the database.
* The admin SPA normalizes at the input, but nobat724_front and clinic-pro-tauri
* hit the same endpoints, so the request layer is the real guarantee.
*/
class NumericFieldNormalizerTest extends ApiTestCase
{
public function testDigitsHelperTranslatesWithoutStripping(): void
{
self::assertSame('09123456789', PersianText::digits('۰۹۱۲۳۴۵۶۷۸۹'));
self::assertSame('0912', PersianText::digits('٠٩١٢'));
self::assertSame('IR12-34', PersianText::digits('IR۱۲-۳۴'), 'letters and separators survive');
self::assertSame('', PersianText::digits(''));
}
public function testSecretaryCreatedWithPersianDigitsIsStoredLatin(): void
{
$owner = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر تست');
$this->em->persist($doctor);
$this->em->flush();
$persianMobile = '۰۹' . str_pad((string) random_int(0, 999_999_999), 9, '۰', STR_PAD_LEFT);
$latinMobile = PersianText::digits($persianMobile);
$this->authJson('POST', '/api/v1/secretary', $owner, [
'doctor_uuid' => $doctor->getUuid(),
'mobile_number' => $persianMobile,
'name' => 'منشی تست',
'national_code' => '۰۰۱۲۳۴۵۶۷۸',
]);
self::assertSame(201, $this->responseCode(), 'Persian digits must not break validation');
$this->em->clear();
$created = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $latinMobile]);
self::assertNotNull($created, 'user is stored under the latin mobile');
$rel = $this->em->getRepository(DoctorSecretary::class)->findOneBy(['secretary' => $created]);
self::assertSame('0012345678', $rel->getNationalCode());
}
public function testNestedArraysAreNormalized(): void
{
$user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
$this->authJson('PUT', '/api/v1/insurance-pricing', $user, [
'free_visit_price_rials' => '۵۰۰۰۰۰',
'insurances' => [
['insurance_id' => 1, 'patient_share_rials' => '۱۲۳۴۵'],
],
]);
// پروفایل پزشک ممکن است بیمه‌ای نداشته باشد؛ مهم این است که ارقام فارسی
// باعث خطای اعتبارسنجی یا NaN نشوند.
self::assertNotSame(500, $this->responseCode(), 'nested persian digits must not blow up');
}
public function testNonNumericKeysKeepPersianDigits(): void
{
$owner = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر تست');
$this->em->persist($doctor);
$this->em->flush();
$mobile = '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
$this->authJson('POST', '/api/v1/secretary', $owner, [
'doctor_uuid' => $doctor->getUuid(),
'mobile_number' => $mobile,
'name' => 'منشی شماره ۲',
]);
self::assertSame(201, $this->responseCode());
$this->em->clear();
$created = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
self::assertStringContainsString('۲', $created->getRealName(), 'name is not a numeric field');
}
}