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:
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Auth\Entity\UserActiveContext;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Patient\Entity\RecordNumberPattern;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* پروندههای ساختهشده پیش از فعالشدن الگو، اولین بار که باز میشوند شماره میگیرند
|
||||
* (`GET /api/v1/patient/{uuid}`) و از آن به بعد همان شماره را نگه میدارند.
|
||||
*/
|
||||
class RecordNumberBackfillTest extends ApiTestCase
|
||||
{
|
||||
/** @return array{0: \App\Auth\Entity\User, 1: Clinic, 2: PatientRecord} */
|
||||
private function scenario(bool $patternEnabled): array
|
||||
{
|
||||
$owner = $this->createUser(['ROLE_CLINIC']);
|
||||
$clinic = new Clinic($owner);
|
||||
$this->em->persist($clinic);
|
||||
$this->em->persist(new UserActiveContext($owner, $clinic->getUuid(), 'clinic'));
|
||||
$this->em->flush();
|
||||
|
||||
$row = new RecordNumberPattern('clinic', $clinic->getId());
|
||||
$row->setPattern('OLD-{SEQ:3}')->setResetPolicy(RecordNumberPattern::RESET_NONE)->setEnabled($patternEnabled);
|
||||
$this->em->persist($row);
|
||||
|
||||
// پروندهٔ قدیمی: بدون شماره، دقیقاً مثل ردیفهای امروزِ دیتابیس.
|
||||
$patient = $this->createUser(['ROLE_USER']);
|
||||
$patient->setRealName('بیمار قدیمی');
|
||||
$record = new PatientRecord('clinic', $clinic->getId(), $patient, 'clinic', $clinic->getId());
|
||||
$this->em->persist($record);
|
||||
$this->em->flush();
|
||||
|
||||
self::assertNull($record->getRecordNumber());
|
||||
|
||||
return [$owner, $clinic, $record];
|
||||
}
|
||||
|
||||
/** ✅ موفق + ⚠️ مرزی: بار اول شماره میگیرد، بار دوم همان شماره برمیگردد. */
|
||||
public function testFirstOpenAssignsANumberAndTheSecondKeepsIt(): void
|
||||
{
|
||||
[$owner, , $record] = $this->scenario(true);
|
||||
$uri = '/api/v1/patient/' . $record->getUuid();
|
||||
|
||||
$first = $this->authJson('GET', $uri, $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('OLD-001', $first['data']['record_number']);
|
||||
|
||||
$second = $this->authJson('GET', $uri, $owner);
|
||||
self::assertSame('OLD-001', $second['data']['record_number']);
|
||||
}
|
||||
|
||||
/** ⚠️ مرزی: بدون الگوی فعال، `GET` هیچ چیزی نمینویسد. */
|
||||
public function testDisabledPatternLeavesTheRecordUntouched(): void
|
||||
{
|
||||
[$owner, , $record] = $this->scenario(false);
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid(), $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertNull($res['data']['record_number']);
|
||||
}
|
||||
|
||||
/** ⚠️ مرزی: پروندهٔ شمارهدار دوباره شماره نمیگیرد و شمارنده مصرف نمیشود. */
|
||||
public function testAlreadyNumberedRecordDoesNotConsumeTheCounter(): void
|
||||
{
|
||||
[$owner, $clinic, $record] = $this->scenario(true);
|
||||
$record->setRecordNumber('MANUAL-7');
|
||||
$this->em->flush();
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid(), $owner);
|
||||
self::assertSame('MANUAL-7', $res['data']['record_number']);
|
||||
|
||||
$pattern = $this->em->getRepository(RecordNumberPattern::class)->findForPair('clinic', $clinic->getId());
|
||||
$this->em->refresh($pattern);
|
||||
self::assertSame(0, $pattern->getCounter());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Patient;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Patient\Entity\RecordNumberPattern;
|
||||
use App\Patient\Service\RecordNumberGenerator;
|
||||
use App\Representation\Service\JalaliDateService;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* تولید شمارهٔ پرونده: رندر توکنها، اعتبارسنجی الگو، و شمارندهٔ تراکنشی.
|
||||
*/
|
||||
class RecordNumberGeneratorTest extends ApiTestCase
|
||||
{
|
||||
/**
|
||||
* سرویسهای دامنه در کانتینرِ تست public نیستند؛ ساختن مستقیم با همان
|
||||
* وابستگیهای تزریقی، هم قرارداد را حفظ میکند هم تست را از کانتینر جدا میکند.
|
||||
*/
|
||||
private function generator(): RecordNumberGenerator
|
||||
{
|
||||
return new RecordNumberGenerator($this->patternRepo(), $this->em, new JalaliDateService());
|
||||
}
|
||||
|
||||
private function patternRepo(): \App\Patient\Repository\RecordNumberPatternRepository
|
||||
{
|
||||
return $this->em->getRepository(RecordNumberPattern::class);
|
||||
}
|
||||
|
||||
/** @return array{0:int,1:int} [سال، ماه] شمسی امروز — تست به تاریخ اجرا وابسته نماند */
|
||||
private function todayJalali(): array
|
||||
{
|
||||
$now = new \DateTimeImmutable('now', new \DateTimeZone(JalaliDateService::TIMEZONE));
|
||||
|
||||
[$jy, $jm] = (new JalaliDateService())->gregorianToJalali(
|
||||
(int) $now->format('Y'),
|
||||
(int) $now->format('m'),
|
||||
(int) $now->format('d'),
|
||||
);
|
||||
|
||||
return [$jy, $jm];
|
||||
}
|
||||
|
||||
private function pattern(string $pattern, string $reset, bool $enabled = true): Doctor
|
||||
{
|
||||
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر تست');
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
$row = new RecordNumberPattern('doctor', $doctor->getId());
|
||||
$row->setPattern($pattern)->setResetPolicy($reset)->setEnabled($enabled);
|
||||
$this->em->persist($row);
|
||||
$this->em->flush();
|
||||
|
||||
return $doctor;
|
||||
}
|
||||
|
||||
public function testRendersEveryToken(): void
|
||||
{
|
||||
[$jy, $jm] = $this->todayJalali();
|
||||
$g = $this->generator();
|
||||
|
||||
self::assertSame((string) $jy, $g->render('{YYYY}', 1));
|
||||
self::assertSame(substr((string) $jy, -2), $g->render('{YY}', 1));
|
||||
self::assertSame(str_pad((string) $jm, 2, '0', STR_PAD_LEFT), $g->render('{MM}', 1));
|
||||
self::assertSame('7', $g->render('{SEQ}', 7));
|
||||
self::assertSame('0007', $g->render('{SEQ:4}', 7));
|
||||
self::assertSame('MD-' . substr((string) $jy, -2) . '-0012', $g->render('MD-{YY}-{SEQ:4}', 12));
|
||||
}
|
||||
|
||||
/** ⚠️ مرزی: شمارندهٔ بلندتر از پدینگ نباید بریده شود. */
|
||||
public function testCounterOverflowKeepsAllDigits(): void
|
||||
{
|
||||
self::assertSame('1000', $this->generator()->render('{SEQ:3}', 1000));
|
||||
}
|
||||
|
||||
public function testPreviewDoesNotConsumeTheCounter(): void
|
||||
{
|
||||
$doctor = $this->pattern('P-{SEQ:3}', RecordNumberPattern::RESET_NONE);
|
||||
$g = $this->generator();
|
||||
|
||||
self::assertSame('P-001', $g->preview('P-{SEQ:3}', 0));
|
||||
// شمارنده هنوز صفر است، پس اولین شمارهٔ واقعی همان ۱ میماند.
|
||||
self::assertSame('P-001', $g->next('doctor', $doctor->getId()));
|
||||
}
|
||||
|
||||
/** ✅ موفق: شمارهها پشت سر هم و بدون تکرار میآیند. */
|
||||
public function testNextIncrementsSequentially(): void
|
||||
{
|
||||
$doctor = $this->pattern('P-{SEQ:3}', RecordNumberPattern::RESET_NONE);
|
||||
$g = $this->generator();
|
||||
|
||||
self::assertSame('P-001', $g->next('doctor', $doctor->getId()));
|
||||
self::assertSame('P-002', $g->next('doctor', $doctor->getId()));
|
||||
self::assertSame('P-003', $g->next('doctor', $doctor->getId()));
|
||||
}
|
||||
|
||||
/** ⚠️ مرزی: بدون الگو یا با الگوی خاموش، رفتار امروز حفظ میشود. */
|
||||
public function testReturnsNullWhenPatternMissingOrDisabled(): void
|
||||
{
|
||||
$g = $this->generator();
|
||||
|
||||
$noPattern = new Doctor($this->createUser(['ROLE_DOCTOR']), 'بدون الگو');
|
||||
$this->em->persist($noPattern);
|
||||
$this->em->flush();
|
||||
self::assertNull($g->next('doctor', $noPattern->getId()));
|
||||
|
||||
$disabled = $this->pattern('P-{SEQ}', RecordNumberPattern::RESET_NONE, false);
|
||||
self::assertNull($g->next('doctor', $disabled->getId()));
|
||||
}
|
||||
|
||||
/** ⚠️ مرزی: ورود به دورهٔ جدید شمارنده را صفر میکند. */
|
||||
public function testCounterResetsWhenPeriodChanges(): void
|
||||
{
|
||||
$doctor = $this->pattern('{YY}-{SEQ:3}', RecordNumberPattern::RESET_YEARLY);
|
||||
$g = $this->generator();
|
||||
[$jy] = $this->todayJalali();
|
||||
$yy = substr((string) $jy, -2);
|
||||
|
||||
self::assertSame("$yy-001", $g->next('doctor', $doctor->getId()));
|
||||
self::assertSame("$yy-002", $g->next('doctor', $doctor->getId()));
|
||||
|
||||
// شبیهسازی سال قبل: مهرِ دوره را عقب میبریم، شمارنده باید صفر شود.
|
||||
$row = $this->patternRepo()->findForPair('doctor', $doctor->getId());
|
||||
$row->resetCounter((string) ($jy - 1));
|
||||
$row->incrementCounter();
|
||||
$row->incrementCounter();
|
||||
$this->em->flush();
|
||||
|
||||
self::assertSame("$yy-001", $g->next('doctor', $doctor->getId()));
|
||||
}
|
||||
|
||||
/** ❌ خطا: الگوهای نامعتبر. */
|
||||
public function testValidatePatternRejectsBadInput(): void
|
||||
{
|
||||
$g = $this->generator();
|
||||
|
||||
self::assertNotEmpty($g->validatePattern('', RecordNumberPattern::RESET_NONE));
|
||||
self::assertNotEmpty($g->validatePattern('MD-{FOO}-{SEQ}', RecordNumberPattern::RESET_NONE));
|
||||
self::assertNotEmpty($g->validatePattern('MD-0001', RecordNumberPattern::RESET_NONE));
|
||||
self::assertNotEmpty($g->validatePattern(str_repeat('x', 61) . '{SEQ}', RecordNumberPattern::RESET_NONE));
|
||||
// {MM} بدون ریست ماهانه و {YY} بدون ریست، ترتیب را بیمعنا میکنند.
|
||||
self::assertNotEmpty($g->validatePattern('{MM}-{SEQ}', RecordNumberPattern::RESET_YEARLY));
|
||||
self::assertNotEmpty($g->validatePattern('{YY}-{SEQ}', RecordNumberPattern::RESET_NONE));
|
||||
|
||||
self::assertSame([], $g->validatePattern('MD-{YY}-{SEQ:4}', RecordNumberPattern::RESET_YEARLY));
|
||||
self::assertSame([], $g->validatePattern('P-{SEQ:5}', RecordNumberPattern::RESET_NONE));
|
||||
self::assertSame([], $g->validatePattern('{YY}{MM}-{SEQ:3}', RecordNumberPattern::RESET_MONTHLY));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?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\Secretary\Entity\DoctorSecretary;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* GET/PUT /api/v1/patient-record-number-settings — الگوی شمارهٔ پروندهٔ محیط جاری.
|
||||
* خواندن برای هر کسی که بیماران را میبیند؛ نوشتن فقط برای صاحب محیط.
|
||||
*/
|
||||
class RecordNumberSettingsApiTest extends ApiTestCase
|
||||
{
|
||||
private const URI = '/api/v1/patient-record-number-settings';
|
||||
|
||||
/** @return array{0: User, 1: Clinic, 2: Doctor} مالک کلینیک + کلینیک + پزشکِ عضو */
|
||||
private function clinic(): 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();
|
||||
|
||||
return [$owner, $clinic, $doctor];
|
||||
}
|
||||
|
||||
public function testOwnerReadsDefaultsBeforeAnythingIsSaved(): void
|
||||
{
|
||||
[$owner] = $this->clinic();
|
||||
|
||||
$res = $this->authJson('GET', self::URI, $owner);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertFalse($res['data']['enabled']);
|
||||
self::assertSame('none', $res['data']['reset_policy']);
|
||||
self::assertSame(0, $res['data']['counter']);
|
||||
self::assertTrue($res['data']['can_edit']);
|
||||
}
|
||||
|
||||
/** ✅ موفق: ذخیرهٔ الگو و پیشنمایش شمارهٔ بعدی. */
|
||||
public function testOwnerSavesPatternAndGetsPreview(): void
|
||||
{
|
||||
[$owner] = $this->clinic();
|
||||
|
||||
$res = $this->authJson('PUT', self::URI, $owner, [
|
||||
'enabled' => true,
|
||||
'pattern' => 'MD-{SEQ:4}',
|
||||
'reset_policy' => 'none',
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertTrue($res['data']['enabled']);
|
||||
self::assertSame('MD-{SEQ:4}', $res['data']['pattern']);
|
||||
self::assertSame('MD-0001', $res['data']['next_preview']);
|
||||
|
||||
// GET بعدی همان مقدارِ ذخیرهشده را میدهد (نه پیشفرض).
|
||||
$read = $this->authJson('GET', self::URI, $owner);
|
||||
self::assertSame('MD-{SEQ:4}', $read['data']['pattern']);
|
||||
}
|
||||
|
||||
/** ❌ خطا: منشی نه مینویسد، نه `can_edit` میگیرد. */
|
||||
public function testSecretaryCannotWriteButCanRead(): void
|
||||
{
|
||||
[, $clinic, $doctor] = $this->clinic();
|
||||
|
||||
$secretary = $this->createUser(['ROLE_SECRETARY']);
|
||||
$this->em->persist(new DoctorSecretary($doctor, $secretary, $clinic));
|
||||
$this->em->persist(new UserActiveContext($secretary, $clinic->getUuid(), 'clinic'));
|
||||
$this->em->flush();
|
||||
|
||||
$read = $this->authJson('GET', self::URI, $secretary);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertFalse($read['data']['can_edit']);
|
||||
|
||||
$this->authJson('PUT', self::URI, $secretary, [
|
||||
'enabled' => true, 'pattern' => 'X-{SEQ}', 'reset_policy' => 'none',
|
||||
]);
|
||||
self::assertSame(403, $this->responseCode());
|
||||
}
|
||||
|
||||
/** ❌ خطا: پزشکِ مهمانِ کلینیک هم صاحب آن محیط نیست. */
|
||||
public function testGuestDoctorCannotChangeTheClinicPattern(): void
|
||||
{
|
||||
[, $clinic, $doctor] = $this->clinic();
|
||||
$this->em->persist(new UserActiveContext($doctor->getUser(), $clinic->getUuid(), 'clinic'));
|
||||
$this->em->flush();
|
||||
|
||||
$this->authJson('PUT', self::URI, $doctor->getUser(), [
|
||||
'enabled' => true, 'pattern' => 'X-{SEQ}', 'reset_policy' => 'none',
|
||||
]);
|
||||
self::assertSame(403, $this->responseCode());
|
||||
}
|
||||
|
||||
/** ❌ خطا: الگوهای نامعتبر همگی ۴۲۲ با فیلد درست. */
|
||||
public function testInvalidPatternsAreRejected(): void
|
||||
{
|
||||
[$owner] = $this->clinic();
|
||||
|
||||
foreach ([
|
||||
['MD-{FOO}-{SEQ}', 'none'], // توکن ناشناخته
|
||||
['MD-0001', 'none'], // بدون {SEQ}
|
||||
['', 'none'], // خالی
|
||||
['{YY}-{SEQ}', 'none'], // سال بدون ریست
|
||||
['{MM}-{SEQ}', 'yearly'], // ماه بدون ریست ماهانه
|
||||
] as [$pattern, $reset]) {
|
||||
$res = $this->authJson('PUT', self::URI, $owner, [
|
||||
'enabled' => true, 'pattern' => $pattern, 'reset_policy' => $reset,
|
||||
]);
|
||||
self::assertSame(422, $this->responseCode(), "pattern: $pattern");
|
||||
self::assertSame('pattern', $res['errors'][0]['field'] ?? null);
|
||||
}
|
||||
|
||||
// سیاست ریستِ ناشناخته هم رد میشود، ولی فیلدش reset_policy است.
|
||||
$res = $this->authJson('PUT', self::URI, $owner, [
|
||||
'enabled' => true, 'pattern' => 'X-{SEQ}', 'reset_policy' => 'weekly',
|
||||
]);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('reset_policy', $res['errors'][0]['field'] ?? null);
|
||||
}
|
||||
|
||||
/** ⚠️ مرزی: تغییر الگو شمارنده را صفر نمیکند — شمارههای صادرشده وجود دارند. */
|
||||
public function testChangingThePatternKeepsTheCounter(): void
|
||||
{
|
||||
[$owner, $clinic] = $this->clinic();
|
||||
|
||||
$this->authJson('PUT', self::URI, $owner, [
|
||||
'enabled' => true, 'pattern' => 'A-{SEQ:3}', 'reset_policy' => 'none',
|
||||
]);
|
||||
|
||||
$repo = $this->em->getRepository(\App\Patient\Entity\RecordNumberPattern::class);
|
||||
$row = $repo->findForPair('clinic', $clinic->getId());
|
||||
$row->incrementCounter();
|
||||
$row->incrementCounter();
|
||||
$this->em->flush();
|
||||
|
||||
$res = $this->authJson('PUT', self::URI, $owner, [
|
||||
'enabled' => true, 'pattern' => 'B-{SEQ:3}', 'reset_policy' => 'none',
|
||||
]);
|
||||
|
||||
self::assertSame(2, $res['data']['counter']);
|
||||
self::assertSame('B-003', $res['data']['next_preview']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user