createUser(['ROLE_DOCTOR']), 'دکتر تست'); $this->em->persist($doctor); $this->em->flush(); return $doctor; } private function newBooking(Doctor $doctor, int $start): Appointment { return $this->newAppointment($doctor, $this->createUser(), $start, $start + 1_800); } public function testTwoLiveBookingsOnSameSlotViolateUniqueKey(): void { $doctor = $this->makeDoctor(); $start = time() + 86_400; $this->em->persist($this->newBooking($doctor, $start)); $this->em->flush(); $this->em->persist($this->newBooking($doctor, $start)); $this->expectException(UniqueConstraintViolationException::class); $this->em->flush(); } public function testCancelledBookingFreesTheSlot(): void { $doctor = $this->makeDoctor(); $start = time() + 86_400; $first = $this->newBooking($doctor, $start); $this->em->persist($first); $this->em->flush(); $first->transitionTo(Appointment::STATUS_CANCELLED_BY_USER); $this->em->flush(); $second = $this->newBooking($doctor, $start); $this->em->persist($second); $this->em->flush(); $this->assertNotNull($second->getId()); } public function testBookAtomicallyRejectsAlreadyTakenSlot(): void { $doctor = $this->makeDoctor(); $start = time() + 86_400; $repo = $this->em->getRepository(Appointment::class); $first = $this->newBooking($doctor, $start); $first->markPendingWithTtl(Appointment::PAYMENT_TTL); $repo->bookAtomically($first); $this->expectException(SlotTakenException::class); $repo->bookAtomically($this->newBooking($doctor, $start)); } /** * Regression: a slot whose only booking has already been consumed (completed, * no_show, …) must still read as taken in the availability view — otherwise the * public site offers an occupied slot as free. isSlotTaken counts the broader * SLOT_BLOCKING_STATUSES, not just live pending/confirmed. */ public function testConsumedBookingStillMarksSlotTaken(): void { $doctor = $this->makeDoctor(); $start = time() + 86_400; $repo = $this->em->getRepository(Appointment::class); $appt = $this->newBooking($doctor, $start); $appt->transitionTo(Appointment::STATUS_CONFIRMED); $appt->transitionTo(Appointment::STATUS_COMPLETED); $this->em->persist($appt); $this->em->flush(); $this->assertTrue($repo->isSlotTaken($doctor, $start, $start + 1_800)); } public function testCancelledBookingLeavesSlotFreeForAvailability(): void { $doctor = $this->makeDoctor(); $start = time() + 86_400; $repo = $this->em->getRepository(Appointment::class); $appt = $this->newBooking($doctor, $start); $appt->transitionTo(Appointment::STATUS_CANCELLED_BY_USER); $this->em->persist($appt); $this->em->flush(); $this->assertFalse($repo->isSlotTaken($doctor, $start, $start + 1_800)); } }