Files
clinicpro/tests/Patient/RecordNumberGeneratorTest.php
T
hamed 85a27812c7 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.
2026-08-04 12:30:17 +03:30

151 lines
6.5 KiB
PHP

<?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));
}
}