find('app:appointment:backfill-service-duration')); } private function doctorInMode(string $mode, int $buffer = 0): Doctor { $owner = $this->createUser(['ROLE_DOCTOR']); $doctor = new Doctor($owner, 'دکتر backfill'); $this->em->persist($doctor); $this->em->flush(); $schedule = $this->newWeeklySchedule($doctor, [ '0' => ['sessions' => [[ 'active' => true, 'start_time' => '09:00', 'end_time' => '18:00', 'duration_per_patient' => 20, 'location_id' => 1, ]]], ]); $schedule->setMeta(['booking_mode' => $mode, 'buffer_minutes' => $buffer]); $this->em->persist($schedule); $this->em->flush(); return $doctor; } private function appointment( Doctor $doctor, string $when, int $minutes, string $status = Appointment::STATUS_CONFIRMED, bool $reserve = false, ): Appointment { $patient = $this->createUser(['ROLE_USER']); $start = (int) strtotime($when); $appt = $this->newAppointment($doctor, $patient, $start, $start + $minutes * 60); if ($status !== Appointment::STATUS_PENDING) { $appt->transitionTo($status); } if ($reserve) { $appt->rescheduleTo($start, $start, true); } $this->em->persist($appt); $this->em->flush(); return $appt; } // ── ✅ موفق ────────────────────────────────────────────────────────────── public function testForceFillsDurationFromTheAppointmentItself(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SERVICE, buffer: 10); // مدت دستیِ ۵۰ دقیقه — نباید با مدت سرویس‌ها بازمحاسبه شود. $appt = $this->appointment($doctor, '+6 days 11:00', 50); $this->tester()->execute(['--force' => true]); $this->em->refresh($appt); self::assertSame(50, $appt->getServiceTotalMinutes(), 'مدت از slot_end - slot_start'); self::assertSame(10, $appt->getServiceBufferMinutes(), 'بافر از meta برنامه'); } public function testDryRunWritesNothing(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SERVICE, buffer: 5); $appt = $this->appointment($doctor, '+6 days 12:00', 30); $tester = $this->tester(); $tester->execute([]); $this->em->refresh($appt); self::assertNull($appt->getServiceTotalMinutes(), 'dry-run پیش‌فرض است'); self::assertStringContainsString('--force', $tester->getDisplay()); } public function testRunningTwiceChangesNothingTheSecondTime(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SERVICE, buffer: 5); $appt = $this->appointment($doctor, '+6 days 13:00', 40); $first = $this->tester(); $first->execute(['--force' => true]); $this->em->refresh($appt); self::assertStringContainsString($appt->getUuid(), $first->getDisplay(), 'بار اول باید کاندید باشد'); $second = $this->tester(); $second->execute(['--force' => true]); $this->em->refresh($appt); self::assertSame(40, $appt->getServiceTotalMinutes(), 'مقدار عوض نشد'); // کوئری با `serviceTotalMinutes IS NULL` فیلتر می‌کند، پس ردیفِ پرشده دیگر // کاندید نیست و در خروجی اجرای دوم نمی‌آید. self::assertStringNotContainsString($appt->getUuid(), $second->getDisplay(), 'idempotent'); } // ── ⚠️ مرزی ────────────────────────────────────────────────────────────── public function testSlotModeAppointmentIsSkipped(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SLOT); $appt = $this->appointment($doctor, '+6 days 14:00', 20); $this->tester()->execute(['--force' => true]); $this->em->refresh($appt); self::assertNull($appt->getServiceTotalMinutes(), 'حالت اسلاتی نباید ستون سرویسی بگیرد'); } public function testPastAppointmentIsSkipped(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SERVICE); $appt = $this->appointment($doctor, '-3 days 10:00', 30); $this->tester()->execute(['--force' => true]); $this->em->refresh($appt); self::assertNull($appt->getServiceTotalMinutes(), 'گذشته بازنویسی نمی‌شود'); } public function testReserveEntryIsSkipped(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SERVICE); $appt = $this->appointment($doctor, '+6 days 00:00', 0, reserve: true); $this->tester()->execute(['--force' => true]); $this->em->refresh($appt); self::assertNull($appt->getServiceTotalMinutes(), 'رزرو مدتی برای استنتاج ندارد'); } public function testCancelledAppointmentIsSkipped(): void { $doctor = $this->doctorInMode(WeeklySchedule::MODE_SERVICE); $appt = $this->appointment($doctor, '+6 days 15:00', 30, Appointment::STATUS_CANCELLED_BY_DOCTOR); $this->tester()->execute(['--force' => true]); $this->em->refresh($appt); self::assertNull($appt->getServiceTotalMinutes()); } }