Appointments could only reference a single service (ManyToOne). Add an appointment_service_items join table (ManyToMany) so an appointment can carry several services; the first stays the primary service_item for backward compatibility, and toArray now also returns service_items[]. Both create endpoints (my/appointment, admin/appointment) accept service_item_uuids[] and attach all of them. A new duration_from_services flag gates the slot_end recompute: service-booking mode sends it true (slot_end = start + Σ durations); slot mode omits it so the manual end time is preserved. The admin endpoint previously ignored services entirely. Frontend: in slot mode the single service dropdown becomes a checkbox list filtered by the selected section (multi-select); service mode sends the duration flag. Migration + backend/entity tests + docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
4.4 KiB
PHP
116 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Appointment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\ClinicService\Entity\ServiceItem;
|
|
use App\ClinicService\Entity\ServiceSection;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* چند سرویس روی یک نوبت: در حالت اسلاتی سرویسها فقط پیوست میشوند و ساعت پایانِ
|
|
* دستی حفظ میماند؛ در حالت سرویسی (duration_from_services) slot_end از مجموع مدت
|
|
* سرویسها بازمحاسبه میشود.
|
|
*/
|
|
class AppointmentMultiServiceTest extends ApiTestCase
|
|
{
|
|
private function doctor(): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_DOCTOR']);
|
|
$doctor = new Doctor($owner, 'دکتر تست');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
return [$owner, $doctor];
|
|
}
|
|
|
|
private function serviceItem(Doctor $doctor, string $name, int $minutes): ServiceItem
|
|
{
|
|
$section = new ServiceSection('doctor', $doctor->getId(), 'بخش ' . $name);
|
|
$this->em->persist($section);
|
|
$item = new ServiceItem($section, $name, 500_000);
|
|
$item->setDurationMinutes($minutes)->setBookable(true);
|
|
$this->em->persist($item);
|
|
$this->em->flush();
|
|
|
|
return $item;
|
|
}
|
|
|
|
private function nationalCode(): string
|
|
{
|
|
return '00' . str_pad((string) random_int(0, 99_999_999), 8, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private function body(Doctor $doctor, int $start, int $end, array $extra): array
|
|
{
|
|
return array_merge([
|
|
'doctor_uuid' => $doctor->getUuid(),
|
|
'slot_start' => $start,
|
|
'slot_end' => $end,
|
|
'patient_mobile' => '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT),
|
|
'patient_name' => 'بیمار تست',
|
|
'patient_national_code' => $this->nationalCode(),
|
|
], $extra);
|
|
}
|
|
|
|
private function reload(string $uuid): Appointment
|
|
{
|
|
$this->em->clear();
|
|
return $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $uuid]);
|
|
}
|
|
|
|
public function testSlotModeAttachesMultipleServicesAndKeepsManualEnd(): void
|
|
{
|
|
[$owner, $doctor] = $this->doctor();
|
|
$a = $this->serviceItem($doctor, 'تزریق ژل', 30);
|
|
$b = $this->serviceItem($doctor, 'کندلا', 20);
|
|
|
|
$start = time() + 86_400 + random_int(0, 3_600) * 100;
|
|
$end = $start + 3_600; // ساعت پایانِ دستی: ۶۰ دقیقه
|
|
|
|
$res = $this->authJson('POST', '/api/v1/my/appointment', $owner, $this->body($doctor, $start, $end, [
|
|
'service_item_uuids' => [$a->getUuid(), $b->getUuid()],
|
|
]));
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$appt = $this->reload($res['data']['uuid']);
|
|
self::assertCount(2, $appt->getServiceItems());
|
|
// ساعت پایان دستنخورده (بدون بازمحاسبه از مدت سرویسها)
|
|
self::assertSame($end, $appt->getSlotEnd());
|
|
// سرویسِ اصلی = اولین سرویس
|
|
self::assertSame($a->getUuid(), $appt->getServiceItem()?->getUuid());
|
|
}
|
|
|
|
public function testServiceModeRecomputesEndFromDurations(): void
|
|
{
|
|
[$owner, $doctor] = $this->doctor();
|
|
$a = $this->serviceItem($doctor, 'تزریق ژل', 30);
|
|
$b = $this->serviceItem($doctor, 'کندلا', 20);
|
|
|
|
$start = time() + 86_400 + random_int(0, 3_600) * 100;
|
|
|
|
$res = $this->authJson('POST', '/api/v1/my/appointment', $owner, $this->body($doctor, $start, $start + 60, [
|
|
'service_item_uuids' => [$a->getUuid(), $b->getUuid()],
|
|
'duration_from_services' => true,
|
|
]));
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$appt = $this->reload($res['data']['uuid']);
|
|
self::assertCount(2, $appt->getServiceItems());
|
|
// مجموع مدت ۳۰+۲۰=۵۰ دقیقه → slot_end بازمحاسبهشده
|
|
self::assertSame($start + 50 * 60, $appt->getSlotEnd());
|
|
}
|
|
|
|
public function testUnknownServiceUuidIsRejected(): void
|
|
{
|
|
[$owner, $doctor] = $this->doctor();
|
|
$start = time() + 86_400 + random_int(0, 3_600) * 100;
|
|
|
|
$this->authJson('POST', '/api/v1/my/appointment', $owner, $this->body($doctor, $start, $start + 1_800, [
|
|
'service_item_uuids' => ['no-such-uuid'],
|
|
]));
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
}
|