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