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:
hamed
2026-07-18 13:32:56 +03:30
parent 2553b45990
commit f1258d206d
28 changed files with 2126 additions and 276 deletions
@@ -15,17 +15,17 @@ use App\ClinicService\Repository\ServiceSectionRepository;
use App\ClinicService\Repository\TariffRepository;
use App\ClinicService\Service\ServiceItemAuditService;
use App\ClinicService\Service\TariffService;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Inventory\Repository\InventoryItemRepository;
use App\Inventory\Repository\InventoryPackageRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Context\EntityContextResolver;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
use App\Staff\Repository\ClinicStaffRepository;
use App\Subscription\Service\SubscriptionService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -40,8 +40,6 @@ class ClinicServiceController extends BaseController
private readonly ServiceItemRepository $itemRepo,
private readonly ClinicStaffRepository $staffRepo,
private readonly SubscriptionService $subscriptionService,
private readonly DoctorRepository $doctorRepo,
private readonly ClinicRepository $clinicRepo,
private readonly TariffRepository $tariffRepo,
private readonly TariffService $tariffService,
private readonly InventoryPackageRepository $packageRepo,
@@ -49,6 +47,8 @@ class ClinicServiceController extends BaseController
private readonly ServiceItemAuditService $auditService,
private readonly ServiceItemAuditLogRepository $auditLogRepo,
private readonly EntityManagerInterface $em,
private readonly EntityContextResolver $contextResolver,
private readonly RequestStack $requestStack,
) {}
/**
@@ -489,19 +489,38 @@ class ClinicServiceController extends BaseController
return null;
}
/**
* صاحب سرویس‌های این درخواست. clinic_uuid درخواست (query یا body) مقدم است، بعد
* محیط فعال کاربر، و در آخر نقش — منطق کامل در EntityContextResolver.
*
* @return array{0: string, 1: ?int}
*/
private function resolveEntity(User $user): array
{
if ($user->hasRole('ROLE_DOCTOR')) {
$doctor = $this->doctorRepo->findByUser($user);
return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null];
return $this->contextResolver->resolve($user, $this->requestedClinicUuid())->toEntityPair();
}
private function requestedClinicUuid(): ?string
{
$request = $this->requestStack->getCurrentRequest();
if ($request === null) {
return null;
}
if ($user->hasRole('ROLE_CLINIC')) {
$clinic = $this->clinicRepo->findByUser($user);
return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
$fromQuery = $request->query->get('clinic_uuid');
if (is_string($fromQuery) && $fromQuery !== '') {
return $fromQuery;
}
return ['unknown', null];
if (!in_array($request->getMethod(), ['POST', 'PATCH', 'PUT'], true)) {
return null;
}
$body = json_decode($request->getContent(), true);
return is_array($body) && is_string($body['clinic_uuid'] ?? null) && $body['clinic_uuid'] !== ''
? $body['clinic_uuid']
: null;
}
private function assertServicesGate(string $entityType, ?int $entityId): void