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:
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Appointment\Command;
|
||||
|
||||
use App\Appointment\Repository\WeeklyScheduleRepository;
|
||||
use App\Clinic\Repository\ClinicRepository;
|
||||
use App\Doctor\Repository\DoctorAddressRepository;
|
||||
use App\Doctor\Repository\DoctorRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Moves an existing weekly schedule from the personal context into a clinic.
|
||||
*
|
||||
* The clinic_id migration marks every pre-existing schedule as personal, because
|
||||
* nothing in the data says otherwise. A schedule whose sessions actually point at
|
||||
* a clinic address needs to be moved by hand — this command does that, and refuses
|
||||
* when the sessions do not agree with the target clinic.
|
||||
*/
|
||||
#[AsCommand(name: 'app:schedule:assign-clinic', description: 'Move a doctor\'s personal weekly schedule into a clinic context')]
|
||||
class AssignScheduleClinicCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WeeklyScheduleRepository $scheduleRepo,
|
||||
private readonly DoctorRepository $doctorRepo,
|
||||
private readonly ClinicRepository $clinicRepo,
|
||||
private readonly DoctorAddressRepository $addressRepo,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('doctor-uuid', InputArgument::REQUIRED, 'Doctor uuid')
|
||||
->addArgument('clinic-uuid', InputArgument::REQUIRED, 'Target clinic uuid')
|
||||
->addOption('force', null, InputOption::VALUE_NONE, 'Move even when some sessions use an address outside the clinic');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$doctor = $this->doctorRepo->findByUuid((string) $input->getArgument('doctor-uuid'));
|
||||
$clinic = $this->clinicRepo->findByUuid((string) $input->getArgument('clinic-uuid'));
|
||||
|
||||
if ($doctor === null || $clinic === null) {
|
||||
$io->error('Doctor or clinic not found.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (!$clinic->hasDoctor($doctor)) {
|
||||
$io->error('This doctor is not a member of that clinic.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$schedule = $this->scheduleRepo->findByDoctorAndClinic($doctor, null);
|
||||
if ($schedule === null) {
|
||||
$io->warning('This doctor has no personal schedule to move.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($this->scheduleRepo->findByDoctorAndClinic($doctor, $clinic) !== null) {
|
||||
$io->error('A schedule already exists for this doctor in that clinic; merge it manually.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$foreign = $this->sessionsOutsideClinic($schedule->getDaySchedule(), $doctor, $clinic->getId());
|
||||
if ($foreign !== [] && !$input->getOption('force')) {
|
||||
$io->error(sprintf(
|
||||
'Sessions use address ids outside the clinic: %s. Re-run with --force to move anyway.',
|
||||
implode(', ', $foreign)
|
||||
));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$schedule->setClinic($clinic);
|
||||
$this->em->flush();
|
||||
|
||||
$io->success(sprintf('Schedule %s moved to clinic "%s".', $schedule->getUuid(), $clinic->getName()));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** @return int[] address ids referenced by the schedule that the clinic does not own */
|
||||
private function sessionsOutsideClinic(array $daySchedule, \App\Doctor\Entity\Doctor $doctor, int $clinicId): array
|
||||
{
|
||||
$owned = [];
|
||||
foreach ($this->addressRepo->findForContext($doctor, $clinicId) as $address) {
|
||||
$owned[(int) $address->getId()] = true;
|
||||
}
|
||||
|
||||
$foreign = [];
|
||||
foreach ($daySchedule as $day) {
|
||||
foreach (($day['sessions'] ?? []) as $session) {
|
||||
$id = (int) ($session['location_id'] ?? 0);
|
||||
if ($id > 0 && !isset($owned[$id])) {
|
||||
$foreign[$id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($foreign);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user