Files
clinicpro/tests/Appointment/PublicSiteAppointmentContractTest.php
hamedandClaude Opus 5 043713275c test(booking): freeze the public-site appointment contract; close task 00b
Adds PublicSiteAppointmentContractTest over GET /api/v1/appointments/user — the
endpoint the public site's user panel actually calls. Task 00's note claimed this
prerequisite was met by extending my/appointments, which is the admin panel's
endpoint; appointments/user returns Appointment::toArray(), which the same task
extended separately. The outcome was right, the reasoning in the note was not.
This test pins it so neither can drift silently: breaking these fields produces no
build error in either repo.

Documents why appointment-service-slots cannot be grouped into shifts by the
client, and records task 00b's checklist including the two items deliberately not
done (colour rewrite, reschedule button) with the evidence for each.

Task: docs/new_feture/taskes/task-00b-nobat724-service-mode/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:59:31 +03:30

163 lines
6.6 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;
/**
* قرارداد `GET /api/v1/appointments/user` — همان endpointی که **پنل کاربرِ سایت عمومی**
* (`nobat724_front/services/response.js` → `getMyAppointments`) مصرف می‌کند.
*
* ⚠️ با `GET /api/v1/my/appointments` اشتباه گرفته نشود: آن یکی سریالایزر جدا دارد و
* پنل ادمین از آن استفاده می‌کند. این یکی `Appointment::toArray()` را برمی‌گرداند.
*
* سایت روی این فیلدها حساب می‌کند و شکستنشان در build هیچ‌کدام از دو ریپو خطا نمی‌دهد؛
* پس تنها تور ایمنی همین تست است.
*/
class PublicSiteAppointmentContractTest extends ApiTestCase
{
/** @return array{0:\App\Auth\Entity\User,1:Doctor,2:ServiceSection} */
private function serviceDoctor(int $buffer = 10): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر قرارداد سایت');
$this->em->persist($doctor);
$this->em->flush();
$schedule = $this->newWeeklySchedule($doctor, [
'0' => ['sessions' => [[
'active' => true, 'start_time' => '09:00', 'end_time' => '18:00',
'duration_per_patient' => 20, 'location_id' => 1,
]]],
]);
$schedule->setMeta(['booking_mode' => WeeklySchedule::MODE_SERVICE, 'buffer_minutes' => $buffer]);
$this->em->persist($schedule);
$section = new ServiceSection('doctor', $doctor->getId(), 'زیبایی');
$this->em->persist($section);
$this->em->flush();
return [$owner, $doctor, $section];
}
private function service(ServiceSection $section, string $name, int $minutes): ServiceItem
{
$item = new ServiceItem($section, $name, 1200000);
$item->setDurationMinutes($minutes)->setBookable(true);
$this->em->persist($item);
$this->em->flush();
return $item;
}
/** @return array{0:\App\Auth\Entity\User,1:Appointment} */
private function bookedFor(Doctor $doctor, array $items, bool $reserve = false): array
{
$patient = $this->createUser(['ROLE_USER']);
$start = (int) strtotime('+3 days 10:00');
$appt = $this->newAppointment($doctor, $patient, $start, $start + 35 * 60);
$appt->transitionTo(Appointment::STATUS_CONFIRMED);
if ($reserve) {
$appt->rescheduleTo($start, $start, true);
}
if ($items !== []) {
$appt->replaceServiceItems($items);
$appt->setServiceDuration(35, 10);
}
$this->em->persist($appt);
$this->em->flush();
return [$patient, $appt];
}
/** پاسخ این endpoint دو لایه است: `data.data[]`. */
private function rows(\App\Auth\Entity\User $patient): array
{
$res = $this->authJson('GET', '/api/v1/appointments/user', $patient);
self::assertSame(200, $this->responseCode());
return $res['data']['data'];
}
// ── ✅ موفق ──────────────────────────────────────────────────────────────
public function testServiceFieldsThePublicPanelNeedsArePresent(): void
{
[, $doctor, $section] = $this->serviceDoctor();
$face = $this->service($section, 'لیزر صورت', 20);
$bikini = $this->service($section, 'لیزر بیکینی', 15);
[$patient] = $this->bookedFor($doctor, [$face, $bikini]);
$row = $this->rows($patient)[0];
self::assertArrayHasKey('service_items', $row);
self::assertCount(2, $row['service_items'], 'فهرست کامل، نه فقط سرویس اول');
self::assertSame(['لیزر صورت', 'لیزر بیکینی'], array_column($row['service_items'], 'name'));
self::assertSame(35, $row['service_total_minutes']);
self::assertSame(10, $row['service_buffer_minutes']);
self::assertArrayHasKey('clinic_uuid', $row);
self::assertNull($row['clinic_uuid'], 'مطب شخصی → null');
}
public function testReserveEntryKeepsItsServicesAndDuration(): void
{
[, $doctor, $section] = $this->serviceDoctor();
$item = $this->service($section, 'لیزر', 45);
[$patient] = $this->bookedFor($doctor, [$item], reserve: true);
$row = $this->rows($patient)[0];
self::assertTrue($row['is_reserve']);
self::assertCount(1, $row['service_items']);
self::assertSame(35, $row['service_total_minutes'], 'مدت برای تبدیل بعدی می‌ماند');
}
// ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
public function testSlotModeAppointmentLeavesTheServiceFieldsNull(): void
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر اسلاتی');
$this->em->persist($doctor);
$this->em->flush();
[$patient] = $this->bookedFor($doctor, []);
$row = $this->rows($patient)[0];
// پنل سایت روی این با شرط && می‌خواند؛ null باید null بماند نه صفر.
self::assertNull($row['service_total_minutes']);
self::assertNull($row['service_buffer_minutes']);
self::assertSame([], $row['service_items']);
}
public function testAppointmentWithoutServicesReturnsAnEmptyArrayNotNull(): void
{
[, $doctor] = $this->serviceDoctor();
[$patient] = $this->bookedFor($doctor, []);
$row = $this->rows($patient)[0];
self::assertIsArray($row['service_items'], 'سایت روی length می‌خواند؛ null کرش می‌دهد');
self::assertSame([], $row['service_items']);
}
public function testEachServiceItemCarriesTheKeysTheSiteReads(): void
{
[, $doctor, $section] = $this->serviceDoctor();
$item = $this->service($section, 'لیزر', 30);
[$patient] = $this->bookedFor($doctor, [$item]);
$first = $this->rows($patient)[0]['service_items'][0];
foreach (['uuid', 'name', 'price_rials'] as $key) {
self::assertArrayHasKey($key, $first);
}
}
}