Files
clinicpro/tests/Appointment/AppointmentWorkflowFieldsTest.php
T
hamedandClaude Fable 5 4678739d15 feat(appointments): backend for clinic workflow (Figma نوبت‌ها) — phase A
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>
2026-07-13 23:37:32 +03:30

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());
}
}