createUser(['ROLE_DOCTOR']), 'دکتر تقویم'); $this->em->persist($doctor); $clinic = new Clinic($this->createUser(['ROLE_CLINIC'])); $clinic->setName('کلینیک تقویم ' . uniqid()); $clinic->getDoctors()->add($doctor); $this->em->persist($clinic); $this->em->flush(); $section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر'); $this->em->persist($section); $category = new CatalogCategory('clinic', (int) $clinic->getId(), 'دست'); $this->em->persist($category); $service = new ServiceItem($section, 'لیزر تقویم', 3_000_000); $service->setCatalogCategory($category); $this->em->persist($service); $this->em->flush(); $protocol = new TreatmentProtocol($service); $this->em->persist($protocol); $protocol->replaceSteps([ new TreatmentProtocolStep($protocol, 1, 0), new TreatmentProtocolStep($protocol, 2, 15), new TreatmentProtocolStep($protocol, 3, 30), new TreatmentProtocolStep($protocol, 4, 30), ]); $this->em->flush(); $patient = $this->createUser(['ROLE_USER']); $record = new PatientRecord('clinic', (int) $clinic->getId(), $patient, 'clinic', (int) $clinic->getId()); $this->em->persist($record); $this->em->flush(); $case = new TreatmentCase('clinic', (int) $clinic->getId(), $record, $service, $protocol); $this->em->persist($case); foreach ([1, 2, 3, 4] as $n) { $case->addSession(new TreatmentSession($case, $n)); } $this->em->flush(); return $case; } /** @return TreatmentSession[] */ private function ordered(TreatmentCase $case): array { $s = $case->getSessions()->toArray(); usort($s, static fn (TreatmentSession $a, TreatmentSession $b): int => $a->getSessionNumber() <=> $b->getSessionNumber()); return array_values($s); } /** * حالت موفق: جلسهٔ تمام‌شده لنگر است و بقیه از روی آن زنجیره می‌شوند. */ public function testFinishedSessionAnchorsTheRest(): void { $case = $this->scenario(); $sessions = $this->ordered($case); $finishedAt = 1_800_000_000; $sessions[0]->start(); $sessions[0]->finish(); // `finish()` زمان جاری می‌گذارد؛ برای تست لنگر قطعی لازم است. $sessions[1]->setDueAt($finishedAt + 15 * self::DAY); $this->em->flush(); $plan = $this->projector()->project($case); self::assertCount(4, $plan); self::assertFalse($plan[0]['is_estimate'], 'جلسهٔ انجام‌شده تخمین نیست'); self::assertFalse($plan[1]['is_estimate'], 'جلسه‌ای که due_at دارد تخمین نیست'); // جلسات ۳ و ۴ لنگری ندارند → تخمین، با فاصلهٔ گام خودشان از جلسهٔ قبل self::assertTrue($plan[2]['is_estimate']); self::assertTrue($plan[3]['is_estimate']); self::assertSame($plan[1]['planned_at'] + 30 * self::DAY, $plan[2]['planned_at']); self::assertSame($plan[2]['planned_at'] + 30 * self::DAY, $plan[3]['planned_at']); } /** * حالت مرزی: دوره‌ای که هیچ جلسهٔ قطعی ندارد — لنگر خودِ بازشدنِ پرونده است، * وگرنه کارت‌ها بی‌تاریخ می‌مانند. */ public function testACaseWithNoAnchorFallsBackToOpenedAt(): void { $case = $this->scenario(); $plan = $this->projector()->project($case); self::assertCount(4, $plan); foreach ($plan as $row) { self::assertTrue($row['is_estimate']); self::assertNotNull($row['planned_at']); } // گام اول فاصلهٔ صفر دارد، پس روی خودِ openedAt می‌نشیند. self::assertSame($case->getOpenedAt(), $plan[0]['planned_at']); self::assertSame($case->getOpenedAt() + 15 * self::DAY, $plan[1]['planned_at']); } /** هیچ‌چیز ذخیره نمی‌شود: `due_at` جلسات بی‌لنگر بعد از projection هنوز null است. */ public function testProjectionNeverWritesDueAt(): void { $case = $this->scenario(); $sessions = $this->ordered($case); $this->projector()->project($case); $this->em->flush(); $this->em->clear(); $reloaded = $this->em->getRepository(TreatmentSession::class)->find($sessions[3]->getId()); self::assertNotNull($reloaded); self::assertNull($reloaded->getDueAt(), 'projection نباید سررسید بنویسد'); } }