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>
90 lines
3.6 KiB
PHP
90 lines
3.6 KiB
PHP
<?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');
|
||
}
|
||
}
|