Service-booking mode now selects services by section like slot mode: appointment-booking-services returns service_section per item; ServiceSlotPicker groups by section (SearchableSelect), accumulates picks across sections into a removable 'section -> service' chip list. Secretaries can override a service's duration for a single appointment without changing the service default: appointment-service-slots accepts durations[uuid] and both create endpoints accept service_durations; the override drives total duration and slot_end. Online (patient) booking is unaffected — it never sends overrides. Backend + frontend tests and docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
5.3 KiB
PHP
123 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Appointment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Appointment\Entity\WeeklySchedule;
|
|
use App\ClinicService\Entity\ServiceItem;
|
|
use App\ClinicService\Entity\ServiceSection;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* حالت نوبتدهی سرویسی: پاسخ booking-services باید بخش هر سرویس را بدهد؛ اسلاتها و
|
|
* ثبت نوبت باید مدتِ override منشی را (فقط برای همان نوبت) لحاظ کنند بدون تغییر پیشفرضِ سرویس.
|
|
*/
|
|
class ServiceModeSectionDurationTest extends ApiTestCase
|
|
{
|
|
/** @return array{0:\App\Auth\Entity\User,1:Doctor,2:string} */
|
|
private function serviceDoctor(int $serviceMinutes = 30): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_DOCTOR']);
|
|
$doctor = new Doctor($owner, 'دکتر سرویس');
|
|
$this->em->persist($doctor);
|
|
|
|
$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' => '19: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 [$owner, $doctor, $date];
|
|
}
|
|
|
|
private function service(Doctor $doctor, string $name, int $minutes): ServiceItem
|
|
{
|
|
$section = new ServiceSection('doctor', $doctor->getId(), 'بخش ' . $name);
|
|
$this->em->persist($section);
|
|
$item = new ServiceItem($section, $name, 0);
|
|
$item->setDurationMinutes($minutes)->setBookable(true);
|
|
$this->em->persist($item);
|
|
$this->em->flush();
|
|
|
|
return $item;
|
|
}
|
|
|
|
public function testBookingServicesReturnsSection(): void
|
|
{
|
|
[$owner, $doctor] = $this->serviceDoctor();
|
|
$svc = $this->service($doctor, 'بوتاکس', 30);
|
|
|
|
$res = $this->authJson('GET', '/api/v1/appointment-booking-services/' . $doctor->getUuid(), $owner);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$row = $res['data']['services'][0];
|
|
self::assertSame($svc->getUuid(), $row['uuid']);
|
|
self::assertArrayHasKey('service_section', $row);
|
|
self::assertSame($svc->getSection()->getUuid(), $row['service_section']['uuid']);
|
|
self::assertSame($svc->getSection()->getName(), $row['service_section']['name']);
|
|
}
|
|
|
|
public function testServiceSlotsHonorsDurationOverride(): void
|
|
{
|
|
[$owner, $doctor, $date] = $this->serviceDoctor();
|
|
$svc = $this->service($doctor, 'فیلر', 30);
|
|
|
|
// بدون override: مدت کل = ۳۰
|
|
$base = $this->authJson('GET', sprintf(
|
|
'/api/v1/appointment-service-slots?doctor_uuid=%s&date=%s&service_item_uuids[]=%s',
|
|
$doctor->getUuid(), $date, $svc->getUuid()
|
|
), $owner);
|
|
self::assertSame(30, $base['data']['total_duration_minutes']);
|
|
|
|
// با override = ۹۰
|
|
$over = $this->authJson('GET', sprintf(
|
|
'/api/v1/appointment-service-slots?doctor_uuid=%s&date=%s&service_item_uuids[]=%s&durations[%s]=90',
|
|
$doctor->getUuid(), $date, $svc->getUuid(), $svc->getUuid()
|
|
), $owner);
|
|
self::assertSame(90, $over['data']['total_duration_minutes']);
|
|
|
|
// پیشفرضِ سرویس در DB تغییر نکرده
|
|
$this->em->clear();
|
|
$reloaded = $this->em->getRepository(ServiceItem::class)->findOneBy(['uuid' => $svc->getUuid()]);
|
|
self::assertSame(30, $reloaded->getDurationMinutes());
|
|
}
|
|
|
|
public function testCreateAppliesDurationOverrideToSlotEnd(): void
|
|
{
|
|
[$owner, $doctor] = $this->serviceDoctor();
|
|
$svc = $this->service($doctor, 'لیزر', 30);
|
|
|
|
$start = time() + 86_400 + random_int(0, 3_600) * 100;
|
|
$nc = '00' . str_pad((string) random_int(0, 99_999_999), 8, '0', STR_PAD_LEFT);
|
|
|
|
$res = $this->authJson('POST', '/api/v1/my/appointment', $owner, [
|
|
'doctor_uuid' => $doctor->getUuid(),
|
|
'slot_start' => $start,
|
|
'slot_end' => $start + 60, // نادیده گرفته میشود (بازمحاسبه)
|
|
'patient_mobile' => '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT),
|
|
'patient_name' => 'بیمار تست',
|
|
'patient_national_code' => $nc,
|
|
'service_item_uuids' => [$svc->getUuid()],
|
|
'duration_from_services' => true,
|
|
'service_durations' => [$svc->getUuid() => 75],
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$appt = $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $res['data']['uuid']]);
|
|
// slot_end = start + 75 دقیقه (override)، نه ۳۰ پیشفرض
|
|
self::assertSame($start + 75 * 60, $appt->getSlotEnd());
|
|
|
|
// پیشفرضِ سرویس دستنخورده
|
|
$reloaded = $this->em->getRepository(ServiceItem::class)->findOneBy(['uuid' => $svc->getUuid()]);
|
|
self::assertSame(30, $reloaded->getDurationMinutes());
|
|
}
|
|
}
|