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