- Introduced clinic_id to weekly_schedules, date_overrides, and holidays to differentiate between personal and clinic schedules. - Updated unique constraints and indexes to accommodate the new clinic context. feat(command): create AssignScheduleClinicCommand to move schedules - Added a command to move a doctor's personal weekly schedule into a clinic context. - Implemented checks to ensure sessions align with the target clinic. feat(context): implement EntityContext and EntityContextResolver - Created EntityContext to represent the effective working environment of a request (doctor or clinic). - Developed EntityContextResolver to determine the execution context based on user roles and active contexts. test: add ServiceModeContextTest for appointment scheduling - Implemented tests to ensure service booking respects clinic and personal contexts. - Verified that financial data is omitted in clinic contexts in InvitedDoctorDashboardScopeTest.
250 lines
11 KiB
PHP
250 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Appointment;
|
|
|
|
use App\Appointment\Entity\WeeklySchedule;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Clinic\Repository\ClinicDoctorPermissionRepository;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Doctor\Entity\DoctorAddress;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* تنظیمات نوبتدهی per-context است: مطب شخصی پزشک (بدون clinic_uuid) فقط برای خود
|
|
* پزشک و ادمین باز است، و کلینیک با clinic_uuid فقط برنامهٔ همان کلینیک را
|
|
* میبیند/مینویسد. این دو برنامهٔ جدا هستند و روی هم اثر نمیگذارند.
|
|
*/
|
|
class ClinicOwnerScheduleAccessTest extends ApiTestCase
|
|
{
|
|
private function makeDoctor(string $name): Doctor
|
|
{
|
|
$user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
|
$doctor = new Doctor($user, $name);
|
|
$doctor->setMobileNumber($user->getMobileNumber());
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
return $doctor;
|
|
}
|
|
|
|
private function makeClinicWith(Doctor ...$doctors): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
|
$clinic = new Clinic($owner);
|
|
$clinic->setName('کلینیک تست');
|
|
foreach ($doctors as $d) {
|
|
$clinic->getDoctors()->add($d);
|
|
}
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
return [$owner, $clinic];
|
|
}
|
|
|
|
private function addressFor(Doctor $doctor): DoctorAddress
|
|
{
|
|
$address = DoctorAddress::forDoctor($doctor);
|
|
$this->em->persist($address);
|
|
$this->em->flush();
|
|
|
|
return $address;
|
|
}
|
|
|
|
private function clinicAddress(Clinic $clinic): DoctorAddress
|
|
{
|
|
$address = DoctorAddress::forClinic($clinic->getId());
|
|
$this->em->persist($address);
|
|
$this->em->flush();
|
|
|
|
return $address;
|
|
}
|
|
|
|
private function schedulePayload(Doctor $doctor, int $locationId, string $start, ?Clinic $clinic = null): array
|
|
{
|
|
return array_filter([
|
|
'doctor_uuid' => $doctor->getUuid(),
|
|
'clinic_uuid' => $clinic?->getUuid(),
|
|
'schedule' => [
|
|
['day' => 'saturday', 'sessions' => [
|
|
['active' => true, 'location_id' => $locationId, 'start' => $start, 'end' => '12:00'],
|
|
]],
|
|
],
|
|
], fn($v) => $v !== null);
|
|
}
|
|
|
|
public function testClinicOwnerCanReadAndWriteMemberDoctorScheduleInClinicContext(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر عضو');
|
|
[$owner, $clinic] = $this->makeClinicWith($doctor);
|
|
$address = $this->clinicAddress($clinic);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($doctor, $address->getId(), '09:00', $clinic));
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$this->authJson('GET', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}?clinic_uuid={$clinic->getUuid()}", $owner);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$this->authJson('PATCH', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $owner, [
|
|
'clinic_uuid' => $clinic->getUuid(),
|
|
'schedule' => [
|
|
['day' => 'saturday', 'sessions' => [
|
|
['active' => true, 'location_id' => $address->getId(), 'start' => '10:00', 'end' => '13:00'],
|
|
]],
|
|
],
|
|
]);
|
|
self::assertSame(200, $this->responseCode());
|
|
}
|
|
|
|
public function testClinicOwnerCannotTouchDoctorPersonalSchedule(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر عضو');
|
|
[$owner] = $this->makeClinicWith($doctor);
|
|
$address = $this->addressFor($doctor);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($doctor, $address->getId(), '09:00'));
|
|
|
|
self::assertSame(403, $this->responseCode(), 'مطب شخصی پزشک از دسترس کلینیک خارج است');
|
|
}
|
|
|
|
public function testClinicContextRejectsPersonalAddress(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر عضو');
|
|
[$owner, $clinic] = $this->makeClinicWith($doctor);
|
|
$personal = $this->addressFor($doctor);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($doctor, $personal->getId(), '09:00', $clinic));
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testPersonalContextRejectsClinicAddress(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر عضو');
|
|
[, $clinic] = $this->makeClinicWith($doctor);
|
|
$clinicAddress = $this->clinicAddress($clinic);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $doctor->getUser(), $this->schedulePayload($doctor, $clinicAddress->getId(), '09:00'));
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testStrangerClinicUuidIsRejected(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر مستقل');
|
|
$outsider = $this->makeDoctor('دکتر دیگر');
|
|
[, $clinic] = $this->makeClinicWith($outsider);
|
|
$address = $this->addressFor($doctor);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $doctor->getUser(), $this->schedulePayload($doctor, $address->getId(), '09:00', $clinic));
|
|
|
|
self::assertSame(422, $this->responseCode(), 'پزشک عضو این کلینیک نیست');
|
|
}
|
|
|
|
public function testPersonalAndClinicSchedulesCoexistIndependently(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر دو-محیطی');
|
|
[$owner, $clinic] = $this->makeClinicWith($doctor);
|
|
$personal = $this->addressFor($doctor);
|
|
$inClinic = $this->clinicAddress($clinic);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $doctor->getUser(), $this->schedulePayload($doctor, $personal->getId(), '08:00'));
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($doctor, $inClinic->getId(), '16:00', $clinic));
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$reloadedDoctor = $this->em->getRepository(Doctor::class)->find($doctor->getId());
|
|
$schedules = $this->em->getRepository(WeeklySchedule::class)->findAllByDoctor($reloadedDoctor);
|
|
|
|
self::assertCount(2, $schedules, 'یک برنامه به ازای هر محیط');
|
|
|
|
$byContext = [];
|
|
foreach ($schedules as $schedule) {
|
|
$byContext[$schedule->getClinic() === null ? 'personal' : 'clinic'] = $schedule->getSetting()[0]['sessions'][0]['start'];
|
|
}
|
|
|
|
self::assertSame('08:00', $byContext['personal']);
|
|
self::assertSame('16:00', $byContext['clinic']);
|
|
}
|
|
|
|
public function testDoctorKeepsFullAccessToOwnSchedule(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر مستقل');
|
|
$address = $this->addressFor($doctor);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $doctor->getUser(), $this->schedulePayload($doctor, $address->getId(), '09:00'));
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$this->authJson('GET', "/api/v1/appointment-settings/weekly-schedule/{$doctor->getUuid()}", $doctor->getUser());
|
|
self::assertSame(200, $this->responseCode());
|
|
}
|
|
|
|
public function testDoctorCannotTouchAnotherDoctorSchedule(): void
|
|
{
|
|
$mine = $this->makeDoctor('دکتر یک');
|
|
$theirs = $this->makeDoctor('دکتر دو');
|
|
$address = $this->addressFor($theirs);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $mine->getUser(), $this->schedulePayload($theirs, $address->getId(), '09:00'));
|
|
|
|
self::assertSame(403, $this->responseCode());
|
|
}
|
|
|
|
public function testAdminCanWriteAnyDoctorSchedule(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر هدف');
|
|
$admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
$address = $this->addressFor($doctor);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $admin, $this->schedulePayload($doctor, $address->getId(), '09:00'));
|
|
|
|
self::assertSame(201, $this->responseCode());
|
|
}
|
|
|
|
public function testMemberDoctorLosesAccessWhenPermissionRevoked(): void
|
|
{
|
|
$doctor = $this->makeDoctor('دکتر عضو');
|
|
$other = $this->makeDoctor('دکتر دیگر');
|
|
[, $clinic] = $this->makeClinicWith($doctor, $other);
|
|
$address = $this->clinicAddress($clinic);
|
|
|
|
$perm = static::getContainer()->get(ClinicDoctorPermissionRepository::class)->getOrCreate($clinic, $doctor);
|
|
$perm->mergePermissions(['resources' => ['appointment_settings' => ['update' => false, 'view' => false]]]);
|
|
$this->em->flush();
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $doctor->getUser(), $this->schedulePayload($other, $address->getId(), '09:00', $clinic));
|
|
|
|
self::assertSame(403, $this->responseCode());
|
|
}
|
|
|
|
public function testEditingOneDoctorDoesNotAffectAnother(): void
|
|
{
|
|
$first = $this->makeDoctor('دکتر اول');
|
|
$second = $this->makeDoctor('دکتر دوم');
|
|
[$owner, $clinic] = $this->makeClinicWith($first, $second);
|
|
$address = $this->clinicAddress($clinic);
|
|
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($first, $address->getId(), '08:00', $clinic));
|
|
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($second, $address->getId(), '16:00', $clinic));
|
|
|
|
$this->authJson('PATCH', "/api/v1/appointment-settings/weekly-schedule/{$first->getUuid()}", $owner, [
|
|
'clinic_uuid' => $clinic->getUuid(),
|
|
'schedule' => [
|
|
['day' => 'saturday', 'sessions' => [
|
|
['active' => true, 'location_id' => $address->getId(), 'start' => '11:00', 'end' => '15:00'],
|
|
]],
|
|
],
|
|
]);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
$this->em->clear();
|
|
$reloaded = $this->em->getRepository(WeeklySchedule::class)->findByDoctorAndClinic(
|
|
$this->em->getRepository(Doctor::class)->find($second->getId()),
|
|
$this->em->getRepository(Clinic::class)->find($clinic->getId()),
|
|
);
|
|
|
|
self::assertSame('16:00', $reloaded->getSetting()[0]['sessions'][0]['start'], "the other doctor's schedule is untouched");
|
|
}
|
|
}
|