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>
173 lines
6.7 KiB
PHP
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());
|
|
}
|
|
}
|