Files
clinicpro/tests/Appointment/AppointmentUpdateTest.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

136 lines
5.2 KiB
PHP

<?php
namespace App\Tests\Appointment;
use App\Appointment\Entity\Appointment;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Staff\Entity\ClinicStaff;
use App\Tests\ApiTestCase;
/**
* PATCH /api/v1/appointment/{uuid} — general update used by ویرایش,
* جا به جایی, انتقال به رزرو and جایگزینی نوبت.
*/
class AppointmentUpdateTest extends ApiTestCase
{
/** @return array{0: \App\Auth\Entity\User, 1: Doctor, 2: Appointment} */
private function booking(): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر');
$this->em->persist($doctor);
$this->em->flush();
$appointment = new Appointment($doctor, $this->createUser(), time() + 86_400, time() + 86_400 + 1_800);
$this->em->persist($appointment);
$this->em->flush();
return [$owner, $doctor, $appointment];
}
public function testUpdatesWorkflowFields(): void
{
[$owner, $doctor, $appointment] = $this->booking();
$section = new ServiceSection('doctor', $doctor->getId(), 'زیبایی');
$item = new ServiceItem($section, 'لیزر توتال');
$staff = new ClinicStaff('doctor', $doctor->getId(), 'سحر ایمانی');
$this->em->persist($section);
$this->em->persist($item);
$this->em->persist($staff);
$this->em->flush();
$res = $this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'service_section_uuid' => $section->getUuid(),
'service_item_uuid' => $item->getUuid(),
'staff_uuid' => $staff->getUuid(),
'deposit_required' => true,
'deposit_amount_rials' => 5_000_000,
'note' => 'یادداشت',
'patient_name' => 'مریم خلیلی',
]);
self::assertSame(200, $this->responseCode());
$d = $res['data']['data'];
self::assertSame('زیبایی', $d['service_section']['name']);
self::assertSame('لیزر توتال', $d['service_item']['name']);
self::assertSame('سحر ایمانی', $d['staff']['full_name']);
self::assertTrue($d['deposit_required']);
self::assertSame('مریم خلیلی', $d['patient_name']);
}
public function testRescheduleMovesSlot(): void
{
[$owner, , $appointment] = $this->booking();
$newStart = time() + 2 * 86_400;
$res = $this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'slot_start' => $newStart, 'slot_end' => $newStart + 1_800,
]);
self::assertSame(200, $this->responseCode());
self::assertSame($newStart, $res['data']['data']['slot_start']);
}
public function testRescheduleRejectsTakenSlot(): void
{
[$owner, $doctor, $appointment] = $this->booking();
$otherStart = time() + 3 * 86_400;
$this->em->persist(new Appointment($doctor, $this->createUser(), $otherStart, $otherStart + 1_800));
$this->em->flush();
$this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'slot_start' => $otherStart, 'slot_end' => $otherStart + 1_800,
]);
self::assertSame(409, $this->responseCode());
}
public function testTransferToReserveAndBack(): void
{
[$owner, , $appointment] = $this->booking();
$day = strtotime('tomorrow midnight');
$res = $this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'is_reserve' => true, 'slot_start' => $day, 'slot_end' => $day,
]);
self::assertSame(200, $this->responseCode());
self::assertTrue($res['data']['data']['is_reserve']);
$back = time() + 4 * 86_400;
$version = $res['data']['data']['version'];
$res2 = $this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'is_reserve' => false, 'slot_start' => $back, 'slot_end' => $back + 1_800, 'version' => $version,
]);
self::assertSame(200, $this->responseCode());
self::assertFalse($res2['data']['data']['is_reserve']);
}
public function testRejectsHalfSlotPair(): void
{
[$owner, , $appointment] = $this->booking();
$this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'slot_start' => time() + 86_400,
]);
self::assertSame(422, $this->responseCode());
}
public function testForbiddenForStranger(): void
{
[, , $appointment] = $this->booking();
$stranger = $this->createUser(['ROLE_DOCTOR']);
$this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $stranger, ['note' => 'x']);
self::assertSame(403, $this->responseCode());
}
public function testUnknownStaffUuidIs422(): void
{
[$owner, , $appointment] = $this->booking();
$this->authJson('PATCH', '/api/v1/appointment/' . $appointment->getUuid(), $owner, [
'staff_uuid' => 'no-such-uuid',
]);
self::assertSame(422, $this->responseCode());
}
}