feat: Implement permission gate for appointment and billing controllers

- Added PermissionGateTrait to manage access control for AppointmentPlanController and BillingController.
- Introduced denyUnlessGrantedForPlanning method in AppointmentPlanController to handle specific permission checks for planning appointments.
- Updated existing methods in both controllers to utilize the new permission checks.
- Refactored ResourcePermissionTrait to use PermissionGateTrait for cleaner permission management.
- Added tests to ensure proper permission enforcement across different scenarios, including cross-tenant access restrictions for staff.
This commit is contained in:
hamed
2026-08-08 10:27:13 +03:30
parent c452150a83
commit 934405c42d
14 changed files with 830 additions and 67 deletions
@@ -26,6 +26,38 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class AppointmentPlanController extends BaseController
{
use \App\Shared\Controller\PermissionGateTrait;
/**
* بخش‌بندی یک خاصیتِ `ServiceItem` است و صفحه‌اش داخل کاتالوگ خدمات، پس همان
* منبعِ `services` — نه `appointments`. دقیقاً همان استدلالِ پروتکل درمان در
* آدیت ۲۰۲۶-۰۸-۰۷.
*/
private function permissionResource(): string
{
return 'services';
}
/**
* پیش‌نمایشِ برنامهٔ یک نوبت — ورودیِ فرمِ ثبت نوبت است، نه پیکربندیِ سرویس.
*
* قرینهٔ `ResourcePermissionTrait::denyUnlessGrantedForBooking`: منشی‌ای که
* اجازهٔ ثبت نوبت دارد ولی کاتالوگ خدمات برایش بسته است، وگرنه نمی‌توانست همان
* نوبتی را که مجاز است ثبت کند.
*/
private function denyUnlessGrantedForPlanning(User $user): void
{
$allowed =
($this->secretaryAccess->canOrNonSecretary($user, 'services', 'view')
&& $this->clinicDoctorAccess->canOrNonMember($user, 'services', 'view'))
|| ($this->secretaryAccess->canOrNonSecretary($user, 'appointments', 'view')
&& $this->clinicDoctorAccess->canOrNonMember($user, 'appointments', 'view'));
if (!$allowed) {
throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
}
}
public function __construct(
private readonly SegmentTemplateRepository $templates,
private readonly ServiceItemRepository $items,
@@ -38,6 +70,8 @@ class AppointmentPlanController extends BaseController
#[Route('/api/v1/service-item/{uuid}/segments', name: 'service_segments_show', methods: ['GET'])]
public function show(#[CurrentUser] User $user, string $uuid): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
$service = $this->requireItem($user, $uuid);
return $this->success(array_map(
@@ -55,6 +89,8 @@ class AppointmentPlanController extends BaseController
#[Route('/api/v1/service-item/{uuid}/segments', name: 'service_segments_replace', methods: ['PUT'])]
public function replace(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
$data = json_decode($request->getContent(), true);
if (!is_array($data) || !is_array($data['segments'] ?? null)) {
@@ -198,6 +234,8 @@ class AppointmentPlanController extends BaseController
#[Route('/api/v1/appointment-plan/preview', name: 'appointment_plan_preview', methods: ['POST'])]
public function preview(#[CurrentUser] User $user, Request $request): JsonResponse
{
$this->denyUnlessGrantedForPlanning($user);
$data = json_decode($request->getContent(), true);
if (!is_array($data) || !is_string($data['service_uuid'] ?? null)) {