feat: add service-based booking mode to appointment scheduling
- 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.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user