Files
clinicpro/tests/Appointment/AppointmentMultiServiceTest.php
T
hamedandClaude Opus 4.8 4642506c0f feat: support multiple services per appointment (checkbox selection)
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>
2026-07-16 10:10:47 +03:30

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());
}
}