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
@@ -16,6 +16,7 @@ use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Security\PatientRecordScopeResolver;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Controller\PermissionGateTrait;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
@@ -27,6 +28,17 @@ use OpenApi\Attributes as OA;
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class BillingController extends BaseController
{
use PermissionGateTrait;
/**
* صورتحساب و مطالبهٔ بیمه هر دو زیرِ «مدیریت پرداخت‌ها»ی پنل نشسته‌اند و توگل
* جداگانه‌ای ندارند، پس منبعِ مجوزشان یکی است.
*/
private function permissionResource(): string
{
return 'payments';
}
public function __construct(
private readonly InvoiceService $invoiceService,
private readonly InvoiceRepository $invoiceRepo,
@@ -108,6 +120,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/invoices', methods: ['POST'])]
public function create(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'create');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -132,6 +146,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/invoices/{uuid}', methods: ['GET'])]
public function show(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
$invoice = $this->invoiceRepo->findByUuid($uuid);
if ($invoice === null || $invoice->getEntityType() !== $entityType || $invoice->getEntityId() !== $entityId) {
@@ -144,6 +160,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/invoices/{uuid}/finalize', methods: ['POST'])]
public function finalize(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
[$entityType, $entityId] = $this->resolveEntity($user);
$invoice = $this->invoiceRepo->findByUuid($uuid);
if ($invoice === null || $invoice->getEntityType() !== $entityType || $invoice->getEntityId() !== $entityId) {
@@ -164,6 +182,8 @@ class BillingController extends BaseController
#[Route('/api/v1/my/billing/payments', methods: ['GET'])]
public function listPayments(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -190,6 +210,8 @@ class BillingController extends BaseController
#[Route('/api/v1/my/billing/payments/summary', methods: ['GET'])]
public function paymentsSummary(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -223,6 +245,8 @@ class BillingController extends BaseController
#[Route('/api/v1/my/billing/patients/{patientUuid}/invoices', methods: ['GET'])]
public function listPatientInvoices(string $patientUuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -262,6 +286,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/claims', methods: ['POST'])]
public function createClaim(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'create');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -286,6 +312,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/claims', methods: ['GET'])]
public function listClaims(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -324,6 +352,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/claims/by-patient', methods: ['GET'])]
public function claimsByPatient(Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -349,6 +379,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/claims/by-patient/{patientUuid}', methods: ['GET'])]
public function claimsForPatient(string $patientUuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
@@ -439,6 +471,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/claims/{uuid}/{action}', methods: ['POST'], requirements: ['action' => 'submit|approve|reject|pay'])]
public function transitionClaim(string $uuid, string $action, Request $request, #[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'update');
[$entityType, $entityId] = $this->resolveEntity($user);
$claim = $this->claimRepo->findByUuid($uuid);
if ($claim === null || $claim->getEntityType() !== $entityType || $claim->getEntityId() !== $entityId) {
@@ -483,6 +517,8 @@ class BillingController extends BaseController
#[Route('/api/v1/billing/reports/insurance-debt', methods: ['GET'])]
public function insuranceDebt(#[CurrentUser] User $user): JsonResponse
{
$this->denyUnlessGranted($user, 'view');
[$entityType, $entityId] = $this->resolveEntity($user);
if ($entityId === null) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);