createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر جابه‌جایی'); $this->em->persist($doctor); $date = date('Y-m-d', strtotime('+2 days')); $dayKey = (string) (((int) date('w', strtotime($date)) + 1) % 7); $schedule = $this->newWeeklySchedule($doctor, [ $dayKey => ['sessions' => [[ 'active' => true, 'start_time' => '15:00', 'end_time' => '17:00', 'duration_per_patient' => 20, 'location_id' => 1, ]]], ]); $schedule->setMeta(['booking_mode' => WeeklySchedule::MODE_SERVICE, 'buffer_minutes' => 0]); $this->em->persist($schedule); $this->em->flush(); return [$doctor, $date, (int) strtotime($date . ' 15:00')]; } private function confirmedAppointment(Doctor $doctor, int $start, int $minutes): Appointment { $patient = $this->createUser(['ROLE_USER']); $appt = $this->newAppointment($doctor, $patient, $start, $start + $minutes * 60); $appt->transitionTo(Appointment::STATUS_CONFIRMED); $this->em->persist($appt); $this->em->flush(); return $appt; } private function calc(): SlotCalculatorService { return static::getContainer()->get(SlotCalculatorService::class); } // ── ✅ موفق ────────────────────────────────────────────────────────────── public function testOwnIntervalReappearsWhenExcluded(): void { [$doctor, $date, $start] = $this->serviceDoctor(); $appt = $this->confirmedAppointment($doctor, $start, 30); $without = array_column($this->calc()->getServiceStartTimes($doctor, $date, 30), 'start_time'); self::assertNotContains('15:00', $without, 'بدون exclude، نوبت خودش را اشغال می‌بیند'); $with = array_column( $this->calc()->getServiceStartTimes($doctor, $date, 30, null, false, $appt->getId()), 'start_time', ); self::assertContains('15:00', $with, 'با exclude، زمان فعلیِ خود نوبت در فهرست می‌آید'); } public function testExcludingAllowsALongerDurationAtTheSameStart(): void { [$doctor, $date, $start] = $this->serviceDoctor(); $appt = $this->confirmedAppointment($doctor, $start, 30); // همان ساعت ۱۵:۰۰ ولی سرویس بلندتر (۶۰ دقیقه) — سناریوی «سرویس را عوض کردم». $times = array_column( $this->calc()->getServiceStartTimes($doctor, $date, 60, null, false, $appt->getId()), 'start_time', ); self::assertContains('15:00', $times); } // ── ❌ خطا / مسیر نادرست ───────────────────────────────────────────────── public function testOtherAppointmentsStayBlockingWhenOneIsExcluded(): void { [$doctor, $date, $start] = $this->serviceDoctor(); $mine = $this->confirmedAppointment($doctor, $start, 30); $others = $this->confirmedAppointment($doctor, $start + 30 * 60, 30); // ۱۵:۳۰ $times = array_column( $this->calc()->getServiceStartTimes($doctor, $date, 30, null, false, $mine->getId()), 'start_time', ); self::assertContains('15:00', $times, 'نوبت خودم آزاد شد'); self::assertNotContains('15:30', $times, 'نوبت دیگری همچنان اشغال است'); self::assertNotNull($others->getId()); } // ── ⚠️ مرزی ────────────────────────────────────────────────────────────── public function testNullExcludeIdBehavesExactlyLikeBefore(): void { [$doctor, $date, $start] = $this->serviceDoctor(); $this->confirmedAppointment($doctor, $start, 30); $implicit = $this->calc()->getServiceStartTimes($doctor, $date, 30); $explicit = $this->calc()->getServiceStartTimes($doctor, $date, 30, null, false, null); self::assertSame($implicit, $explicit, 'پیش‌فرض null نباید رفتار موجود را عوض کند'); } public function testRepositoryExcludeIdFiltersOnlyThatRow(): void { [$doctor, , $start] = $this->serviceDoctor(); $appt = $this->confirmedAppointment($doctor, $start, 30); $repo = static::getContainer()->get(AppointmentRepository::class); $from = $start - 3600; $to = $start + 7200; self::assertNotEmpty($repo->findBusyIntervals($doctor, $from, $to)); self::assertSame([], $repo->findBusyIntervals($doctor, $from, $to, $appt->getId())); } public function testExcludingAReserveEntryChangesNothing(): void { [$doctor, $date, $start] = $this->serviceDoctor(); // نوبت رزرو هیچ بازه‌ای اشغال نمی‌کند، پس exclude کردنش هم بی‌اثر است. $patient = $this->createUser(['ROLE_USER']); $dayMid = (int) strtotime($date . ' 00:00:00'); $reserve = $this->newAppointment($doctor, $patient, $dayMid, $dayMid); $reserve->rescheduleTo($dayMid, $dayMid, true); $this->em->persist($reserve); $this->em->flush(); $before = $this->calc()->getServiceStartTimes($doctor, $date, 30); $after = $this->calc()->getServiceStartTimes($doctor, $date, 30, null, false, $reserve->getId()); self::assertSame($before, $after); self::assertSame($start, (int) strtotime($date . ' 15:00')); } }