docs/api/appointment.md gains the service-reschedule endpoint, the service-mode section under PATCH, exclude_appointment_uuid and clinic_uuid on appointment-service-slots, and the my/appointments additions. All JSON bodies are real output captured from the running endpoints, not hand-written. New docs/architecture/booking-modes.md holds the endpoint/mode matrix, the duration contract with a worked example (35 + 10 buffer means a 45-minute step, so 11:00 is not offered even though it looks free), the reserve-entry rules, and a placeholder for the resource mode task 06 will add. Also fixes a pre-existing flaky test that blocked a green suite: NumericFieldNormalizerTest used a fixed national_code against db_test, which is never reset, so depending on execution order the endpoint rejected it as a duplicate. The test already looped for a unique mobile but not for the national code. Out of this task's scope, fixed and declared so the definition of done is actually green rather than apparently green. phpstan was measured against the pre-task commit rather than asserted: 14 errors in 9 files before, the same 14 in the same 9 files now. Task 00 complete: 1026 tests green across three consecutive runs, 604 frontend tests green, slot-mode contract frozen and verified. Task: docs/new_feture/taskes/task-00-service-mode-completion/ Slot-mode contract: unchanged (--group=slot-mode-frozen green) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
4.6 KiB
PHP
108 lines
4.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
|
||
{
|
||
private function toPersianDigits(string $latin): string
|
||
{
|
||
return str_replace(range('0', '9'), ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'], $latin);
|
||
}
|
||
|
||
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();
|
||
|
||
// db_test پاک نمیشود؛ شماره باید تازه باشد وگرنه endpoint با «تکراری» رد میکند.
|
||
do {
|
||
$persianMobile = '۰۹' . str_pad((string) random_int(0, 999_999_999), 9, '۰', STR_PAD_LEFT);
|
||
$latinMobile = PersianText::digits($persianMobile);
|
||
} while ($this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $latinMobile]) !== null);
|
||
|
||
// کد ملی هم همینطور. ثابتبودنش این تست را flaky میکرد: در اجرای کامل سوئیت،
|
||
// بسته به ترتیب اجرا، ردیفِ اجرای قبلی باعث ۴۲۲ «تکراری» میشد و در اجرای تنها
|
||
// سبز بود.
|
||
$persianNationalCode = str_pad(
|
||
$this->toPersianDigits((string) random_int(0, 9_999_999_999)),
|
||
10,
|
||
'۰',
|
||
STR_PAD_LEFT,
|
||
);
|
||
|
||
$this->authJson('POST', '/api/v1/secretary', $owner, [
|
||
'doctor_uuid' => $doctor->getUuid(),
|
||
'mobile_number' => $persianMobile,
|
||
'name' => 'منشی تست',
|
||
'national_code' => $persianNationalCode,
|
||
]);
|
||
|
||
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(PersianText::digits($persianNationalCode), $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');
|
||
}
|
||
}
|