feat(patient): implement record number pattern management

- Add RecordNumberSettingsController for managing patient record number patterns.
- Create RecordNumberPattern entity to represent the pattern configuration.
- Implement RecordNumberPatternRepository for database interactions.
- Develop RecordNumberGenerator service for generating and validating record numbers.
- Add tests for record number generation, backfilling, and API interactions.
- Ensure proper access control for viewing and updating patterns based on user roles.
This commit is contained in:
hamed
2026-08-04 12:30:17 +03:30
parent 9b8eef9598
commit 85a27812c7
24 changed files with 2009 additions and 19 deletions
+139
View File
@@ -0,0 +1,139 @@
<?php
namespace App\Tests\Patient;
use App\Auth\Entity\User;
use App\Auth\Entity\UserActiveContext;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Patient\Entity\RecordNumberPattern;
use App\Secretary\Entity\DoctorSecretary;
use App\Tests\ApiTestCase;
/**
* شمارهٔ پرونده هنگام ساختِ پرونده از الگو ساخته می‌شود — و ورودِ دستی فقط از
* صاحب مجموعه پذیرفته می‌شود.
*/
class RecordNumberOnCreateTest extends ApiTestCase
{
/** @return array{0: User, 1: Clinic, 2: Doctor} */
private function clinicWithPattern(?string $pattern = 'MD-{SEQ:4}', bool $enabled = true): array
{
$owner = $this->createUser(['ROLE_CLINIC']);
$clinic = new Clinic($owner);
$this->em->persist($clinic);
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر عضو');
$this->em->persist($doctor);
$clinic->getDoctors()->add($doctor);
$this->em->persist(new UserActiveContext($owner, $clinic->getUuid(), 'clinic'));
$this->em->flush();
if ($pattern !== null) {
$row = new RecordNumberPattern('clinic', $clinic->getId());
$row->setPattern($pattern)->setResetPolicy(RecordNumberPattern::RESET_NONE)->setEnabled($enabled);
$this->em->persist($row);
$this->em->flush();
}
// فیچرِ `patient_records` از پلنِ «free» می‌آید که ApiTestCase تضمینش می‌کند.
return [$owner, $clinic, $doctor];
}
private function createPatientPayload(string $suffix): array
{
return [
'mobile' => '0912' . str_pad((string) random_int(0, 9_999_999), 7, '0', STR_PAD_LEFT),
'name' => 'بیمار ' . $suffix,
'national_code' => (string) random_int(1_000_000_000, 9_999_999_999),
];
}
/** ✅ موفق: بدون فرستادن شماره، سرور از الگو می‌سازد و دنباله جلو می‌رود. */
public function testCreateGeneratesSequentialNumbers(): void
{
[$owner] = $this->clinicWithPattern();
$first = $this->authJson('POST', '/api/v1/patient', $owner, $this->createPatientPayload('الف'));
self::assertSame(201, $this->responseCode());
self::assertSame('MD-0001', $first['data']['record_number']);
$second = $this->authJson('POST', '/api/v1/patient', $owner, $this->createPatientPayload('ب'));
self::assertSame('MD-0002', $second['data']['record_number']);
}
/** ⚠️ مرزی: بدون الگوی فعال، رفتار امروز حفظ می‌شود (شمارهٔ دستی، برای همه). */
public function testWithoutPatternTheManualNumberStillWins(): void
{
[$owner] = $this->clinicWithPattern(null);
$res = $this->authJson('POST', '/api/v1/patient', $owner, [
...$this->createPatientPayload('ج'),
'record_number' => 'کاغذی-77',
]);
self::assertSame(201, $this->responseCode());
self::assertSame('کاغذی-77', $res['data']['record_number']);
}
/** ✅ موفق: صاحب مجموعه می‌تواند خارج از دنباله شماره بگذارد. */
public function testOwnerMayOverrideTheGeneratedNumber(): void
{
[$owner] = $this->clinicWithPattern();
$res = $this->authJson('POST', '/api/v1/patient', $owner, [
...$this->createPatientPayload('د'),
'record_number' => 'ARCHIVE-1',
]);
self::assertSame(201, $this->responseCode());
self::assertSame('ARCHIVE-1', $res['data']['record_number']);
}
/** ❌ خطا: منشی حق ورود دستی ندارد — نه در ساخت، نه در ویرایش. */
public function testSecretaryCannotSetTheNumberManually(): void
{
[, $clinic, $doctor] = $this->clinicWithPattern();
$secretary = $this->createUser(['ROLE_SECRETARY']);
$rel = new DoctorSecretary($doctor, $secretary, $clinic);
$rel->mergePermissions(['resources' => ['patients' => ['view' => true, 'create' => true, 'update' => true]]]);
$this->em->persist($rel);
$this->em->persist(new UserActiveContext($secretary, $clinic->getUuid(), 'clinic'));
$this->em->flush();
$this->authJson('POST', '/api/v1/patient', $secretary, [
...$this->createPatientPayload('ه'),
'record_number' => 'HAND-9',
]);
self::assertSame(403, $this->responseCode());
// بدون شمارهٔ دستی همان منشی می‌تواند پرونده بسازد و شماره از الگو می‌آید.
$ok = $this->authJson('POST', '/api/v1/patient', $secretary, $this->createPatientPayload('و'));
self::assertSame(201, $this->responseCode());
self::assertSame('MD-0001', $ok['data']['record_number']);
}
/** ⚠️ مرزی: پروندهٔ خودکارِ نوبت هم در همان دنباله می‌نشیند. */
public function testAutoCreatedRecordFromAppointmentGetsANumber(): void
{
[, $clinic, $doctor] = $this->clinicWithPattern();
$patient = $this->createUser(['ROLE_USER']);
$patient->setRealName('بیمار نوبت');
$this->em->flush();
$appointment = new \App\Appointment\Entity\Appointment($doctor, $patient, time() + 3_600, time() + 5_400);
$appointment->setClinic($clinic);
$appointment->assignTenant(\App\Shared\Context\EntityContext::forBooking($doctor, $clinic));
$this->em->persist($appointment);
$this->em->flush();
$service = static::getContainer()->get(\App\Patient\Service\PatientService::class);
$session = $service->autoCreateOnAppointmentConfirm($appointment);
self::assertNotNull($session);
self::assertSame('MD-0001', $session->getRecord()->getRecordNumber());
}
}