Files
clinicpro/tests/Appointment/AppointmentServiceFieldsTest.php
T
hamedandClaude Opus 5 6c2e075eea feat(booking): persist service duration and allow full service replacement
Appointment gains:
  - replaceServiceItems(): full replacement that unconditionally syncs the
    legacy single serviceItem column. addServiceItem() only fills it when null,
    which would leave stale service names in the four consumers that read
    service_item (admin lists, public site, desktop app).
  - currentServiceUuids(): input-order uuids, falling back to the single column
    for appointments created before multi-service support.
  - service_total_minutes / service_buffer_minutes (both nullable, NULL in slot
    mode). slot_end - slot_start carries the number but cannot say whether it
    was intentional, and a reserve entry has slot_start == slot_end so its
    duration had nowhere to live.

Existing columns untouched: slot_start, slot_end, active_slot_key, is_reserve
verified unchanged via SHOW COLUMNS.

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>
2026-07-30 12:47:48 +03:30

173 lines
6.7 KiB
PHP

<?php
namespace App\Tests\Appointment;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Tests\ApiTestCase;
/**
* فیلدهای سرویسیِ نوبت: جایگزینی کامل سرویس‌ها، هم‌گامی ستون تکیِ `serviceItem`، و
* مدت/بافرِ ثبت‌شده در لحظهٔ ثبت.
*/
class AppointmentServiceFieldsTest extends ApiTestCase
{
private function doctorWithServices(int $count): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر فیلدهای سرویسی');
$this->em->persist($doctor);
// بخش به شناسهٔ پزشک نیاز دارد، پس پزشک باید پیش از ساختش flush شود.
$this->em->flush();
$section = new ServiceSection('doctor', $doctor->getId(), 'بخش تست');
$this->em->persist($section);
$items = [];
for ($i = 1; $i <= $count; $i++) {
$item = new ServiceItem($section, 'سرویس ' . $i, 0);
$item->setDurationMinutes(10 * $i)->setBookable(true);
$this->em->persist($item);
$items[] = $item;
}
$this->em->flush();
return [$doctor, $items];
}
private function appointmentFor(Doctor $doctor): \App\Appointment\Entity\Appointment
{
$patient = $this->createUser(['ROLE_USER']);
$start = strtotime('+5 days 10:00');
return $this->newAppointment($doctor, $patient, $start, $start + 1800);
}
// ── ✅ موفق ──────────────────────────────────────────────────────────────
public function testReplaceServiceItemsSyncsTheLegacySingleColumn(): void
{
[$doctor, $items] = $this->doctorWithServices(3);
$appt = $this->appointmentFor($doctor);
$appt->replaceServiceItems([$items[0], $items[1]]);
self::assertCount(2, $appt->getServiceItems());
self::assertSame($items[0], $appt->getServiceItem(), 'سرویس تکی = اولین عضو');
// جایگزینی کامل: اعضای قبلی می‌روند و تکی هم به‌روز می‌شود.
$appt->replaceServiceItems([$items[2]]);
self::assertCount(1, $appt->getServiceItems());
self::assertSame($items[2], $appt->getServiceItem(), 'تکی باید با عضو جدید هم‌گام شود');
}
public function testCurrentServiceUuidsKeepsInputOrder(): void
{
[$doctor, $items] = $this->doctorWithServices(3);
$appt = $this->appointmentFor($doctor);
$appt->replaceServiceItems([$items[2], $items[0], $items[1]]);
self::assertSame(
[$items[2]->getUuid(), $items[0]->getUuid(), $items[1]->getUuid()],
$appt->currentServiceUuids(),
);
}
public function testSetServiceDurationIsExposedInToArray(): void
{
[$doctor] = $this->doctorWithServices(1);
$appt = $this->appointmentFor($doctor);
$appt->setServiceDuration(45, 10);
self::assertSame(45, $appt->getServiceTotalMinutes());
self::assertSame(10, $appt->getServiceBufferMinutes());
$array = $appt->toArray();
self::assertSame(45, $array['service_total_minutes']);
self::assertSame(10, $array['service_buffer_minutes']);
}
// ── ❌ خطا / مسیر نادرست ─────────────────────────────────────────────────
public function testReplaceWithEmptyListClearsTheLegacySingleColumn(): void
{
[$doctor, $items] = $this->doctorWithServices(2);
$appt = $this->appointmentFor($doctor);
$appt->replaceServiceItems([$items[0]]);
$appt->replaceServiceItems([]);
self::assertCount(0, $appt->getServiceItems());
self::assertNull($appt->getServiceItem(), 'سرویس تکی نباید یادگارِ انتخاب قبلی بماند');
self::assertSame([], $appt->currentServiceUuids());
}
// ── ⚠️ مرزی ──────────────────────────────────────────────────────────────
public function testSlotModeAppointmentKeepsBothColumnsNull(): void
{
[$doctor] = $this->doctorWithServices(1);
$appt = $this->appointmentFor($doctor);
self::assertNull($appt->getServiceTotalMinutes());
self::assertNull($appt->getServiceBufferMinutes());
self::assertNull($appt->toArray()['service_total_minutes']);
}
public function testClearingDurationAlsoClearsBuffer(): void
{
[$doctor] = $this->doctorWithServices(1);
$appt = $this->appointmentFor($doctor);
$appt->setServiceDuration(45, 10);
// بافرِ بی‌مدت معنا ندارد؛ حتی اگر مقدار بافر پاس داده شود باید NULL شود.
$appt->setServiceDuration(null, 10);
self::assertNull($appt->getServiceTotalMinutes());
self::assertNull($appt->getServiceBufferMinutes());
}
public function testDuplicateItemsInReplaceAreDeduplicated(): void
{
[$doctor, $items] = $this->doctorWithServices(1);
$appt = $this->appointmentFor($doctor);
$appt->replaceServiceItems([$items[0], $items[0]]);
self::assertCount(1, $appt->getServiceItems());
}
public function testLegacyAppointmentWithOnlySingleItemStillReportsItsUuid(): void
{
[$doctor, $items] = $this->doctorWithServices(1);
$appt = $this->appointmentFor($doctor);
// نوبت پیش از چند-سرویسی‌شدن: فقط ستون تکی پر است.
$appt->setServiceItem($items[0]);
self::assertCount(0, $appt->getServiceItems());
self::assertSame([$items[0]->getUuid()], $appt->currentServiceUuids());
}
public function testReplacePersistsThroughFlush(): void
{
[$doctor, $items] = $this->doctorWithServices(2);
$appt = $this->appointmentFor($doctor);
$appt->replaceServiceItems([$items[0], $items[1]])->setServiceDuration(30, 5);
$this->em->persist($appt);
$this->em->flush();
$this->em->clear();
$reloaded = static::getContainer()
->get(\App\Appointment\Repository\AppointmentRepository::class)
->findByUuid($appt->getUuid());
self::assertNotNull($reloaded);
self::assertCount(2, $reloaded->getServiceItems());
self::assertSame(30, $reloaded->getServiceTotalMinutes());
self::assertSame(5, $reloaded->getServiceBufferMinutes());
}
}