Fills service_total_minutes/service_buffer_minutes on future service-mode appointments booked before the columns existed. The value comes from the appointment itself (slot_end - slot_start), not from recomputing the services: an existing appointment may have been booked with a manual duration and recomputing would rewrite the past. Slot-mode, past, reserve and cancelled appointments are skipped. Dry-run by default. Idempotency comes from the query filtering on serviceTotalMinutes IS NULL rather than from a flag, so a second run has nothing to do. Task: docs/new_feture/taskes/task-00-service-mode-completion/ Slot-mode contract: unchanged (--group=slot-mode-frozen green) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
164 lines
6.5 KiB
PHP
164 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Appointment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Appointment\Entity\WeeklySchedule;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Tests\ApiTestCase;
|
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
|
use Symfony\Component\Console\Tester\CommandTester;
|
|
|
|
/**
|
|
* `app:appointment:backfill-service-duration` — نوبتهای آیندهٔ محیطهای سرویسی که
|
|
* پیش از افزودن ستونهای مدت ثبت شدهاند.
|
|
*
|
|
* مقدار از **خودِ نوبت** میآید نه از بازمحاسبهٔ سرویسها: نوبت موجود ممکن است با مدت
|
|
* دستی ثبت شده باشد و بازمحاسبه یعنی تغییرِ گذشته.
|
|
*/
|
|
class BackfillServiceDurationTest extends ApiTestCase
|
|
{
|
|
private function tester(): CommandTester
|
|
{
|
|
$application = new Application(static::$kernel);
|
|
|
|
return new CommandTester($application->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());
|
|
}
|
|
}
|