Files
clinicpro/src/Appointment/Command/AssignScheduleClinicCommand.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

113 lines
4.4 KiB
PHP

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