- Introduced a new booking mode in WeeklySchedule to support service-based appointments. - Updated SlotCalculatorService to calculate available start times based on selected service durations and buffer times. - Enhanced AppointmentController to handle service items during booking, calculating slot_end on the server side. - Implemented validation to ensure at least one bookable service exists for doctors in service mode. - Added new API endpoint to retrieve available appointment slots based on selected services. - Updated MyAppointmentsController to accept service items during appointment creation. - Modified ServiceItem entity to include a bookable flag, allowing services to be marked for scheduling. - Created migration to add bookable column to service_items table. - Added tests for service-based slot calculations and validation logic.
106 lines
4.4 KiB
PHP
106 lines
4.4 KiB
PHP
<?php
|
||
|
||
namespace App\Tests\Appointment;
|
||
|
||
use App\Appointment\Entity\Appointment;
|
||
use App\Appointment\Entity\WeeklySchedule;
|
||
use App\Appointment\Service\SlotCalculatorService;
|
||
use App\Doctor\Entity\Doctor;
|
||
use App\Tests\ApiTestCase;
|
||
|
||
/**
|
||
* حالت نوبتدهی سرویسی: getServiceStartTimes باید فضای خالیِ داخل شیفت را با توجه
|
||
* به مدت سرویس (+ بافر) بچیند و بازههای اشغالشده را رد کند. همچنین متای mode/buffer.
|
||
*/
|
||
class ServiceBasedSlotsTest extends ApiTestCase
|
||
{
|
||
private function makeDoctorWithServiceSchedule(int $buffer = 5): array
|
||
{
|
||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||
$doctor = new Doctor($owner, 'دکتر سرویس');
|
||
$this->em->persist($doctor);
|
||
|
||
// فردا در بازهٔ booking-window قرار دارد و روز گذشته نیست.
|
||
$date = date('Y-m-d', strtotime('tomorrow'));
|
||
$dayKey = (string)(((int) date('w', strtotime($date)) + 1) % 7);
|
||
|
||
$schedule = new WeeklySchedule($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' => $buffer]);
|
||
$this->em->persist($schedule);
|
||
$this->em->flush();
|
||
|
||
return [$doctor, $date];
|
||
}
|
||
|
||
public function testGapPackingWithBuffer(): void
|
||
{
|
||
[$doctor, $date] = $this->makeDoctorWithServiceSchedule(5);
|
||
|
||
$calc = static::getContainer()->get(SlotCalculatorService::class);
|
||
$slots = $calc->getServiceStartTimes($doctor, $date, 30);
|
||
|
||
// پنجره 15:00–17:00، سرویس 30 + بافر 5 → گام 35 دقیقه: 15:00, 15:35, 16:10
|
||
$times = array_column($slots, 'start_time');
|
||
$this->assertSame(['15:00', '15:35', '16:10'], $times);
|
||
|
||
// زمان پایانِ ذخیرهشده بدون بافر است.
|
||
$this->assertSame(strtotime($date . ' 15:00') + 30 * 60, $slots[0]['end']);
|
||
}
|
||
|
||
public function testBookedIntervalIsSkipped(): void
|
||
{
|
||
[$doctor, $date] = $this->makeDoctorWithServiceSchedule(5);
|
||
|
||
$patient = $this->createUser(['ROLE_USER']);
|
||
$start = strtotime($date . ' 15:00');
|
||
$appt = new Appointment($doctor, $patient, $start, $start + 30 * 60);
|
||
$appt->transitionTo(Appointment::STATUS_CONFIRMED);
|
||
$this->em->persist($appt);
|
||
$this->em->flush();
|
||
|
||
$calc = static::getContainer()->get(SlotCalculatorService::class);
|
||
$times = array_column($calc->getServiceStartTimes($doctor, $date, 30), 'start_time');
|
||
|
||
// 15:00 اشغال است → از 15:30 شروع میشود.
|
||
$this->assertNotContains('15:00', $times);
|
||
$this->assertContains('15:30', $times);
|
||
}
|
||
|
||
public function testNoRoomReturnsEmpty(): void
|
||
{
|
||
[$doctor, $date] = $this->makeDoctorWithServiceSchedule(0);
|
||
|
||
// سرویس 200 دقیقه در پنجرهٔ 120 دقیقهای جا نمیشود.
|
||
$calc = static::getContainer()->get(SlotCalculatorService::class);
|
||
$this->assertSame([], $calc->getServiceStartTimes($doctor, $date, 200));
|
||
}
|
||
|
||
public function testMetaDefaultsAndWhitelist(): void
|
||
{
|
||
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||
$doctor = new Doctor($owner, 'دکتر متا');
|
||
$schedule = new WeeklySchedule($doctor, []);
|
||
|
||
// پیشفرض = اسلاتی
|
||
$this->assertSame(WeeklySchedule::MODE_SLOT, $schedule->getMeta()['booking_mode']);
|
||
$this->assertSame(0, $schedule->getMeta()['buffer_minutes']);
|
||
|
||
$schedule->setMeta(['booking_mode' => 'service', 'buffer_minutes' => 7]);
|
||
$this->assertSame('service', $schedule->getMeta()['booking_mode']);
|
||
$this->assertSame(7, $schedule->getMeta()['buffer_minutes']);
|
||
|
||
// مقدار نامعتبر mode نادیده گرفته میشود (whitelist)، بافر منفی → صفر.
|
||
$schedule->setMeta(['booking_mode' => 'bogus', 'buffer_minutes' => -3]);
|
||
$this->assertSame('service', $schedule->getMeta()['booking_mode']);
|
||
$this->assertSame(0, $schedule->getMeta()['buffer_minutes']);
|
||
}
|
||
}
|