Files
clinicpro/tests/Appointment/ClinicOwnerScheduleAccessTest.php
T
hamed f1258d206d 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.
2026-07-18 13:32:56 +03:30

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