fix(booking): reserve conversion produced a zero-length midnight appointment
TransferReserveModal built the live appointment from appointment_time/end_time, which on a reserve entry are both 00:00 because slot_start == slot_end. Moving a reserve back to the appointment list silently created a zero-length appointment at midnight. With the new duration validation it would now fail loudly instead. Converting back now asks for a real time: the service picker in service mode, two required time inputs in slot mode. The appointment -> reserve direction is untouched. GET /my/appointments has its own array-hydration serializer rather than Appointment::toArray(), so it exposed none of the service fields the panel needs. Added service_items (separate query, no row multiplication and no N+1), clinic_uuid and the duration pair. This was also a hidden prerequisite of the public-site task, whose checklist listed it as "verify first". The reserve table now lists every service instead of only the first. Not done, deliberately: the DataTable migration the task asked for. Its stated reason — inline tokens breaking dark mode — does not hold; this table's th/td already use CSS variables and dark mode works. Rewriting a working table for no real gain is unjustified risk. Task: docs/new_feture/taskes/task-00-service-mode-completion/ Slot-mode contract: unchanged (--group=slot-mode-frozen green) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?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/my/appointments` سریالایزر خودش را دارد (array hydration)، نه
|
||||
* `Appointment::toArray()`. پس فیلدهای سرویسی باید صریحاً همانجا اضافه شوند —
|
||||
* وگرنه پنل ادمین و پنل کاربر سایت دادهای برای نمایش ندارند.
|
||||
*/
|
||||
class MyAppointmentsServiceFieldsTest extends ApiTestCase
|
||||
{
|
||||
/** @return array{0:\App\Auth\Entity\User,1:Doctor,2:ServiceSection} */
|
||||
private function serviceDoctor(): 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' => 10]);
|
||||
$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, 120000);
|
||||
$item->setDurationMinutes($minutes)->setBookable(true);
|
||||
$this->em->persist($item);
|
||||
$this->em->flush();
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function appointment(Doctor $doctor, array $items, bool $reserve = false): Appointment
|
||||
{
|
||||
$patient = $this->createUser(['ROLE_USER']);
|
||||
$start = (int) strtotime('+4 days 10:00');
|
||||
$appt = $this->newAppointment($doctor, $patient, $reserve ? $start : $start, $reserve ? $start : $start + 1800);
|
||||
if ($reserve) {
|
||||
$appt->rescheduleTo($start, $start, true);
|
||||
}
|
||||
if ($items !== []) {
|
||||
$appt->replaceServiceItems($items);
|
||||
$appt->setServiceDuration(35, 10);
|
||||
}
|
||||
$this->em->persist($appt);
|
||||
$this->em->flush();
|
||||
|
||||
return $appt;
|
||||
}
|
||||
|
||||
private function rowFor(\App\Auth\Entity\User $actor, string $uuid, bool $reserve): ?array
|
||||
{
|
||||
$res = $this->authJson('GET', '/api/v1/my/appointments?limit=50' . ($reserve ? '&reserve=1' : ''), $actor);
|
||||
foreach ($res['data'] ?? [] as $row) {
|
||||
if ($row['uuid'] === $uuid) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── ✅ موفق ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testAllServiceItemsAreListedNotJustTheFirst(): void
|
||||
{
|
||||
[$owner, $doctor, $section] = $this->serviceDoctor();
|
||||
$face = $this->service($section, 'لیزر صورت', 20);
|
||||
$bikini = $this->service($section, 'لیزر بیکینی', 15);
|
||||
$appt = $this->appointment($doctor, [$face, $bikini]);
|
||||
|
||||
$row = $this->rowFor($owner, $appt->getUuid(), false);
|
||||
|
||||
self::assertNotNull($row);
|
||||
self::assertCount(2, $row['service_items'], 'فهرست کامل سرویسها باید بیاید');
|
||||
self::assertSame(
|
||||
['لیزر صورت', 'لیزر بیکینی'],
|
||||
array_column($row['service_items'], 'name'),
|
||||
);
|
||||
self::assertSame('لیزر صورت', $row['service_item']['name'], 'سرویس تکی همان اولی میماند');
|
||||
}
|
||||
|
||||
public function testDurationAndBufferAreExposed(): void
|
||||
{
|
||||
[$owner, $doctor, $section] = $this->serviceDoctor();
|
||||
$item = $this->service($section, 'لیزر', 35);
|
||||
$appt = $this->appointment($doctor, [$item]);
|
||||
|
||||
$row = $this->rowFor($owner, $appt->getUuid(), false);
|
||||
|
||||
self::assertSame(35, $row['service_total_minutes']);
|
||||
self::assertSame(10, $row['service_buffer_minutes']);
|
||||
}
|
||||
|
||||
public function testClinicUuidIsExposedForBookingModeDetection(): void
|
||||
{
|
||||
[$owner, $doctor, $section] = $this->serviceDoctor();
|
||||
$item = $this->service($section, 'لیزر', 30);
|
||||
$appt = $this->appointment($doctor, [$item]);
|
||||
|
||||
$row = $this->rowFor($owner, $appt->getUuid(), false);
|
||||
|
||||
self::assertArrayHasKey('clinic_uuid', $row);
|
||||
self::assertNull($row['clinic_uuid'], 'مطب شخصی → null، و کلاینت باید بتواند تفکیک کند');
|
||||
}
|
||||
|
||||
public function testReserveListCarriesServicesAndDurationToo(): void
|
||||
{
|
||||
[$owner, $doctor, $section] = $this->serviceDoctor();
|
||||
$item = $this->service($section, 'لیزر', 45);
|
||||
$appt = $this->appointment($doctor, [$item], reserve: true);
|
||||
|
||||
$row = $this->rowFor($owner, $appt->getUuid(), true);
|
||||
|
||||
self::assertNotNull($row, 'نوبت رزرو باید در فهرست reserve=1 باشد');
|
||||
self::assertTrue($row['is_reserve']);
|
||||
self::assertCount(1, $row['service_items']);
|
||||
self::assertSame(35, $row['service_total_minutes'], 'مدت برای تبدیل بعدی لازم است');
|
||||
}
|
||||
|
||||
// ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testAppointmentWithoutServicesReturnsAnEmptyListNotNull(): void
|
||||
{
|
||||
[$owner, $doctor] = $this->serviceDoctor();
|
||||
$appt = $this->appointment($doctor, []);
|
||||
|
||||
$row = $this->rowFor($owner, $appt->getUuid(), false);
|
||||
|
||||
self::assertSame([], $row['service_items'], 'آرایهٔ خالی، نه null — کلاینت روی length میخواند');
|
||||
self::assertNull($row['service_total_minutes']);
|
||||
self::assertNull($row['service_buffer_minutes']);
|
||||
}
|
||||
|
||||
public function testPaginationIsNotBrokenByTheServiceItemsJoin(): void
|
||||
{
|
||||
[$owner, $doctor, $section] = $this->serviceDoctor();
|
||||
$a = $this->service($section, 'س۱', 10);
|
||||
$b = $this->service($section, 'س۲', 10);
|
||||
$c = $this->service($section, 'س۳', 10);
|
||||
// سه سرویس روی یک نوبت: اگر collection را JOIN میکردیم، این یک نوبت سه ردیف
|
||||
// میشد و صفحهٔ اول یک آیتم کمتر میداشت.
|
||||
$appt = $this->appointment($doctor, [$a, $b, $c]);
|
||||
|
||||
$res = $this->authJson('GET', '/api/v1/my/appointments?limit=50', $owner);
|
||||
$matching = array_values(array_filter($res['data'], fn($r) => $r['uuid'] === $appt->getUuid()));
|
||||
|
||||
self::assertCount(1, $matching, 'نوبت باید دقیقاً یک ردیف باشد');
|
||||
self::assertCount(3, $matching[0]['service_items']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user