Extend Appointment for the clinic-facing appointments area:
- New nullable relations service_section/service_item/staff (بخش/سرویس/پرسنل)
plus deposit_required/deposit_amount_rials (بیعانه) and is_reserve.
- New statuses following_up (در حال پیگیری) and salon (سالن) with day-of
transition rules; reserve entries never occupy a slot (several reserves may
share one day), enforced in refreshActiveSlotKey.
- rescheduleTo(slotStart, slotEnd, isReserve) keeps active_slot_key consistent
for جا به جایی and reserve transfers.
- New PATCH /api/v1/appointment/{uuid}: partial update covering edit, slot
move (409 on taken slot, race backstop on the unique key), reserve toggle,
patient swap (جایگزینی) and optional status transition; optimistic lock via
version like the status endpoint.
- POST /my/appointment now accepts the workflow fields and is_reserve
(day-level entry: no past-slot rule, no atomic slot booking); GET
/my/appointments gains reserve=1 and returns the new fields per row.
Migration Version20260713195434 (+ mirrored on db_test). Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
89 lines
2.8 KiB
PHP
89 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Appointment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* Clinic-workflow additions on Appointment: following_up/salon statuses,
|
|
* service section/item + staff relations, deposit fields, reserve flag,
|
|
* and rescheduleTo keeping active_slot_key consistent.
|
|
*/
|
|
class AppointmentWorkflowFieldsTest extends ApiTestCase
|
|
{
|
|
private function makeDoctor(): Doctor
|
|
{
|
|
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر تست');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
return $doctor;
|
|
}
|
|
|
|
private function newBooking(Doctor $doctor, int $start): Appointment
|
|
{
|
|
$a = new Appointment($doctor, $this->createUser(), $start, $start + 1_800);
|
|
$this->em->persist($a);
|
|
$this->em->flush();
|
|
|
|
return $a;
|
|
}
|
|
|
|
public function testDayOfWorkflowTransitions(): void
|
|
{
|
|
$a = $this->newBooking($this->makeDoctor(), time() + 86_400);
|
|
|
|
$a->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$a->transitionTo(Appointment::STATUS_FOLLOWING_UP);
|
|
$a->transitionTo(Appointment::STATUS_SALON);
|
|
$a->transitionTo(Appointment::STATUS_COMPLETED);
|
|
$this->em->flush();
|
|
|
|
self::assertSame(Appointment::STATUS_COMPLETED, $a->getStatus());
|
|
}
|
|
|
|
public function testPendingCannotJumpToSalon(): void
|
|
{
|
|
$a = $this->newBooking($this->makeDoctor(), time() + 86_400);
|
|
|
|
$this->expectException(\LogicException::class);
|
|
$a->transitionTo(Appointment::STATUS_SALON);
|
|
}
|
|
|
|
public function testWorkflowFieldsPersistAndSerialize(): void
|
|
{
|
|
$a = $this->newBooking($this->makeDoctor(), time() + 86_400);
|
|
|
|
$a->setDepositRequired(true)
|
|
->setDepositAmountRials(5_000_000);
|
|
$a->rescheduleTo($a->getSlotStart(), $a->getSlotEnd(), true);
|
|
$this->em->flush();
|
|
$this->em->clear();
|
|
|
|
$reloaded = $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $a->getUuid()]);
|
|
$arr = $reloaded->toArray();
|
|
|
|
self::assertTrue($arr['deposit_required']);
|
|
self::assertSame(5_000_000, $arr['deposit_amount_rials']);
|
|
self::assertTrue($arr['is_reserve']);
|
|
self::assertNull($arr['service_section']);
|
|
self::assertNull($arr['staff']);
|
|
}
|
|
|
|
public function testRescheduleFreesTheOldSlot(): void
|
|
{
|
|
$doctor = $this->makeDoctor();
|
|
$start = time() + 86_400;
|
|
|
|
$first = $this->newBooking($doctor, $start);
|
|
$first->rescheduleTo($start + 3_600, $start + 5_400);
|
|
$this->em->flush();
|
|
|
|
// old slot must be free again — a new live booking on it succeeds
|
|
$second = $this->newBooking($doctor, $start);
|
|
self::assertNotNull($second->getId());
|
|
}
|
|
}
|