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>
163 lines
6.6 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|