feat(appointment): let a platform admin change a doctor's locked booking mode
The booking mode locks after the first save because existing appointments were computed under that mode's rules. A lock with no key, though, traps a practice that picked the wrong mode on day one, so ROLE_ADMIN can now open it. The first attempt is still refused when the doctor has active appointments in the next year, and says how many; the admin repeats the request with force_mode_change to confirm they know what happens to those. The flag does nothing for anyone else. GET now returns booking_mode_changeable so the panel enables the toggle from the server's answer rather than guessing from the role. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Appointment;
|
||||
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\ClinicService\Entity\ServiceSection;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* قفلِ نوع نوبتدهی برای همه هست، کلیدش فقط دست ادمین پلتفرم.
|
||||
*
|
||||
* قفل بیدلیل نیست: نوبتهای ثبتشده با قواعد همان نوع محاسبه شدهاند. ولی قفلِ
|
||||
* بیکلید، محیطی را که اشتباه انتخاب کرده تا ابد گیر میاندازد، پس ادمین میتواند
|
||||
* بازش کند — با یک تأیید صریح وقتی نوبت فعالی در آینده هست.
|
||||
*/
|
||||
class AdminBookingModeChangeTest extends ApiTestCase
|
||||
{
|
||||
/** نوبتدهی سرویسی بدون سرویسِ قابل رزرو ذخیره نمیشود؛ این همان سرویس است. */
|
||||
private function bookableServiceFor(Doctor $doctor): void
|
||||
{
|
||||
$section = new ServiceSection('doctor', (int) $doctor->getId(), 'بخش تست');
|
||||
$this->em->persist($section);
|
||||
|
||||
$item = new ServiceItem($section, 'ویزیت', 500_000);
|
||||
$item->setBookable(true);
|
||||
$item->setDurationMinutes(20);
|
||||
$this->em->persist($item);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/** @return array{0: Doctor, 1: DoctorAddress} */
|
||||
private function makeDoctorWithSlotSchedule(): array
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($user, 'دکتر تست');
|
||||
$doctor->setMobileNumber($user->getMobileNumber());
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
$address = DoctorAddress::forDoctor($doctor);
|
||||
$this->em->persist($address);
|
||||
$this->em->flush();
|
||||
|
||||
$this->bookableServiceFor($doctor);
|
||||
|
||||
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $user, [
|
||||
'doctor_uuid' => $doctor->getUuid(),
|
||||
'schedule' => [['day' => 'saturday', 'sessions' => [
|
||||
['active' => true, 'location_id' => $address->getId(), 'start' => '09:00', 'end' => '12:00'],
|
||||
]]],
|
||||
'meta' => ['booking_mode' => 'slot'],
|
||||
]);
|
||||
self::assertSame(201, $this->responseCode());
|
||||
|
||||
return [$doctor, $address];
|
||||
}
|
||||
|
||||
private function switchToService(Doctor $doctor, \App\Auth\Entity\User $actor, bool $force = false): array
|
||||
{
|
||||
return $this->authJson('PATCH', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $actor, [
|
||||
'meta' => ['booking_mode' => 'service'],
|
||||
'force_mode_change' => $force,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testDoctorStillCannotChangeTheirOwnLockedMode(): void
|
||||
{
|
||||
[$doctor] = $this->makeDoctorWithSlotSchedule();
|
||||
|
||||
$this->switchToService($doctor, $doctor->getUser());
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testAdminChangesModeWhenNothingIsBooked(): void
|
||||
{
|
||||
[$doctor] = $this->makeDoctorWithSlotSchedule();
|
||||
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
||||
|
||||
$body = $this->switchToService($doctor, $admin);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('service', $body['data']['data']['meta']['booking_mode']);
|
||||
}
|
||||
|
||||
public function testUpcomingAppointmentsBlockTheFirstAttemptAndAreReported(): void
|
||||
{
|
||||
[$doctor] = $this->makeDoctorWithSlotSchedule();
|
||||
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
||||
|
||||
$start = time() + 3 * 86400;
|
||||
$this->em->persist($this->newAppointment($doctor, $this->createUser(), $start, $start + 1800));
|
||||
$this->em->flush();
|
||||
|
||||
$body = $this->switchToService($doctor, $admin);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertStringContainsString('نوبت فعال', $body['errors'][0]['message'] ?? '');
|
||||
}
|
||||
|
||||
public function testAdminCanForceThroughUpcomingAppointments(): void
|
||||
{
|
||||
[$doctor] = $this->makeDoctorWithSlotSchedule();
|
||||
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
||||
|
||||
$start = time() + 3 * 86400;
|
||||
$this->em->persist($this->newAppointment($doctor, $this->createUser(), $start, $start + 1800));
|
||||
$this->em->flush();
|
||||
|
||||
$body = $this->switchToService($doctor, $admin, force: true);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('service', $body['data']['data']['meta']['booking_mode']);
|
||||
}
|
||||
|
||||
public function testResponseTellsThePanelWhoHoldsTheKey(): void
|
||||
{
|
||||
[$doctor] = $this->makeDoctorWithSlotSchedule();
|
||||
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
||||
|
||||
$asDoctor = $this->authJson('GET', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $doctor->getUser());
|
||||
self::assertTrue($asDoctor['data']['data']['booking_mode_locked']);
|
||||
self::assertFalse($asDoctor['data']['data']['booking_mode_changeable']);
|
||||
|
||||
$asAdmin = $this->authJson('GET', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $admin);
|
||||
self::assertTrue($asAdmin['data']['data']['booking_mode_changeable']);
|
||||
}
|
||||
|
||||
public function testForceFlagFromANonAdminChangesNothing(): void
|
||||
{
|
||||
[$doctor] = $this->makeDoctorWithSlotSchedule();
|
||||
|
||||
$this->switchToService($doctor, $doctor->getUser(), force: true);
|
||||
|
||||
self::assertSame(422, $this->responseCode(), 'پرچم تأیید برای غیر ادمین بیاثر است');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user