feat(migrations): add clinic_id context to weekly_schedules, date_overrides, and holidays
- 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.
This commit is contained in:
@@ -10,9 +10,9 @@ use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* Appointment settings are reachable by the doctor, an admin, and the owner of a
|
||||
* clinic the doctor belongs to. A member doctor's own access is governed by the
|
||||
* clinic's appointment_settings permission. Edits never leak across doctors.
|
||||
* تنظیمات نوبتدهی per-context است: مطب شخصی پزشک (بدون clinic_uuid) فقط برای خود
|
||||
* پزشک و ادمین باز است، و کلینیک با clinic_uuid فقط برنامهٔ همان کلینیک را
|
||||
* میبیند/مینویسد. این دو برنامهٔ جدا هستند و روی هم اثر نمیگذارند.
|
||||
*/
|
||||
class ClinicOwnerScheduleAccessTest extends ApiTestCase
|
||||
{
|
||||
@@ -50,32 +50,43 @@ class ClinicOwnerScheduleAccessTest extends ApiTestCase
|
||||
return $address;
|
||||
}
|
||||
|
||||
private function schedulePayload(Doctor $doctor, int $locationId, string $start): array
|
||||
private function clinicAddress(Clinic $clinic): DoctorAddress
|
||||
{
|
||||
return [
|
||||
$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 testClinicOwnerCanReadAndWriteMemberDoctorSchedule(): void
|
||||
public function testClinicOwnerCanReadAndWriteMemberDoctorScheduleInClinicContext(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor('دکتر عضو');
|
||||
[$owner] = $this->makeClinicWith($doctor);
|
||||
$address = $this->addressFor($doctor);
|
||||
$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'));
|
||||
$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()}", $owner);
|
||||
$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, [
|
||||
'schedule' => [
|
||||
'clinic_uuid' => $clinic->getUuid(),
|
||||
'schedule' => [
|
||||
['day' => 'saturday', 'sessions' => [
|
||||
['active' => true, 'location_id' => $address->getId(), 'start' => '10:00', 'end' => '13:00'],
|
||||
]],
|
||||
@@ -84,16 +95,77 @@ class ClinicOwnerScheduleAccessTest extends ApiTestCase
|
||||
self::assertSame(200, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testClinicOwnerCannotTouchOutsideDoctor(): void
|
||||
public function testClinicOwnerCannotTouchDoctorPersonalSchedule(): void
|
||||
{
|
||||
$member = $this->makeDoctor('دکتر عضو');
|
||||
[$owner] = $this->makeClinicWith($member);
|
||||
$stranger = $this->makeDoctor('دکتر بیرونی');
|
||||
$address = $this->addressFor($stranger);
|
||||
$doctor = $this->makeDoctor('دکتر عضو');
|
||||
[$owner] = $this->makeClinicWith($doctor);
|
||||
$address = $this->addressFor($doctor);
|
||||
|
||||
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($stranger, $address->getId(), '09:00'));
|
||||
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($doctor, $address->getId(), '09:00'));
|
||||
|
||||
self::assertSame(403, $this->responseCode());
|
||||
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
|
||||
@@ -132,45 +204,45 @@ class ClinicOwnerScheduleAccessTest extends ApiTestCase
|
||||
|
||||
public function testMemberDoctorLosesAccessWhenPermissionRevoked(): void
|
||||
{
|
||||
$doctor = $this->makeDoctor('دکتر عضو');
|
||||
$other = $this->makeDoctor('دکتر دیگر');
|
||||
$doctor = $this->makeDoctor('دکتر عضو');
|
||||
$other = $this->makeDoctor('دکتر دیگر');
|
||||
[, $clinic] = $this->makeClinicWith($doctor, $other);
|
||||
$address = $this->addressFor($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'));
|
||||
$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] = $this->makeClinicWith($first, $second);
|
||||
$addrFirst = $this->addressFor($first);
|
||||
$addrSecond = $this->addressFor($second);
|
||||
$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, $addrFirst->getId(), '08:00'));
|
||||
$this->authJson('POST', '/api/v1/appointment-settings/weekly-schedule', $owner, $this->schedulePayload($second, $addrSecond->getId(), '16:00'));
|
||||
$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, [
|
||||
'schedule' => [
|
||||
'clinic_uuid' => $clinic->getUuid(),
|
||||
'schedule' => [
|
||||
['day' => 'saturday', 'sessions' => [
|
||||
['active' => true, 'location_id' => $addrFirst->getId(), 'start' => '11:00', 'end' => '15:00'],
|
||||
['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)->findOneBy([
|
||||
'doctor' => $this->em->getRepository(Doctor::class)->find($second->getId()),
|
||||
]);
|
||||
$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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user