From 43db75394284c8da5ea8ac971041e0e8f2fd7057 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 23 Jul 2026 18:21:53 +0330 Subject: [PATCH] fix(secretary): allow insurance pages; add grantable `subscription` resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Insurance pages redirected to the dashboard: the insurance-pricing/claims routes never listed `secretary`, so RoleRoute bounced a secretary who had insurances.view and saw the menu item. Added secretary + permission ['insurances','view'] to both routes; also gated my-financial with ['payments','view'] for consistency. «خرید اشتراک» was owner-only with no permission toggle, so it could not be granted. Added a `subscription` secretary resource (view/create) end-to-end: - entity DEFAULT_PERMISSIONS + SecretaryPermissions type + both secretary forms. - backend: SubscriptionController::my (view) and trial (create), PaymentController::initiateSubscription (create). resolveEntity in SubscriptionController was already secretary-aware. - frontend: subscription + subscription/success routes accept secretary + permission; settings navs gate «خرید اشتراک» by ['subscription','view']. Tests: subscription denied-by-default / allowed-when-granted. docs/api secretary.md updated (resource list, enforcement map, JSON example). Co-Authored-By: Claude Opus 4.8 (1M context) --- assets/admin/App.tsx | 10 +++++----- .../layout/PurchaseSubscriptionSidebar.tsx | 2 +- .../components/layout/SettingsLayout.tsx | 2 +- assets/admin/pages/MySecretariesPage.tsx | 9 +++++++++ assets/admin/pages/SecretariesPage.tsx | 8 ++++++++ assets/admin/types/index.ts | 4 ++++ docs/api/secretary.md | 6 ++++-- src/Payment/Controller/PaymentController.php | 1 + src/Secretary/Entity/DoctorSecretary.php | 1 + .../Controller/SubscriptionController.php | 3 +++ .../SecretaryResourceEnforcementTest.php | 19 +++++++++++++++++++ 11 files changed, 56 insertions(+), 9 deletions(-) diff --git a/assets/admin/App.tsx b/assets/admin/App.tsx index 0c093f5d..2c4ce24e 100644 --- a/assets/admin/App.tsx +++ b/assets/admin/App.tsx @@ -242,10 +242,10 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> - } /> + } /> {/* فاز ۲ — دکتر / کلینیک */} } /> @@ -253,9 +253,9 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> - } /> + } /> } /> } /> } /> diff --git a/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx b/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx index 92648774..fa33dc49 100644 --- a/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx +++ b/assets/admin/components/layout/PurchaseSubscriptionSidebar.tsx @@ -31,7 +31,7 @@ type NavItem = { }; const NAV_ITEMS: NavItem[] = [ - { key: 'subscription', label: 'خرید اشتراک', to: '/admin/subscription' }, + { key: 'subscription', label: 'خرید اشتراک', to: '/admin/subscription', perm: ['subscription', 'view'] }, { key: 'payment', label: 'مدیریت پرداخت', to: '/admin/my-financial', perm: ['payments', 'view'] }, { key: 'appointment', label: 'مدیریت نوبت دهی', to: '/admin/appointment-settings', roles: ['doctor'], perm: ['appointment_settings', 'view'] }, { key: 'appointment', label: 'مدیریت نوبت دهی', to: '/admin/settings/appointment-settings', roles: ['clinic'], perm: ['appointment_settings', 'view'] }, diff --git a/assets/admin/components/layout/SettingsLayout.tsx b/assets/admin/components/layout/SettingsLayout.tsx index fd986793..898c3527 100644 --- a/assets/admin/components/layout/SettingsLayout.tsx +++ b/assets/admin/components/layout/SettingsLayout.tsx @@ -24,7 +24,7 @@ export type SettingsMenuItem = { }; export const SETTINGS_MENU: SettingsMenuItem[] = [ - { key: 'subscription', label: 'خرید اشتراک', icon: CreditCardIcon, to: '/admin/subscription' }, + { key: 'subscription', label: 'خرید اشتراک', icon: CreditCardIcon, to: '/admin/subscription', perm: ['subscription', 'view'] }, { key: 'doctor', label: 'مدیریت پزشک', icon: UserIcon, to: '/admin/profile', roles: ['doctor'] }, { key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/appointment-settings', roles: ['doctor'], perm: ['appointment_settings', 'view'] }, { key: 'appointment', label: 'مدیریت نوبت دهی', icon: CalendarDaysIcon, to: '/admin/settings/appointment-settings', roles: ['clinic'], perm: ['appointment_settings', 'view'] }, diff --git a/assets/admin/pages/MySecretariesPage.tsx b/assets/admin/pages/MySecretariesPage.tsx index 9ce5d168..f03709b0 100644 --- a/assets/admin/pages/MySecretariesPage.tsx +++ b/assets/admin/pages/MySecretariesPage.tsx @@ -81,6 +81,7 @@ const EMPTY_PERMISSIONS: SecretaryPermissions = { sms: { view: false, create: false, update: false, delete: false }, appointment_settings: { view: false, update: false }, clinic_doctors: { view: false, create: false, update: false, delete: false }, + subscription: { view: false, create: false }, }; type PermSection = keyof SecretaryPermissions; @@ -229,6 +230,14 @@ const PERMISSION_SECTIONS: { { key: "delete", label: "حذف پزشک" }, ], }, + { + key: "subscription", + title: "خرید اشتراک", + items: [ + { key: "view", label: "مشاهده اشتراک" }, + { key: "create", label: "خرید/فعال‌سازی اشتراک" }, + ], + }, ]; function PermissionAccordions({ diff --git a/assets/admin/pages/SecretariesPage.tsx b/assets/admin/pages/SecretariesPage.tsx index 84898365..5378f2bb 100644 --- a/assets/admin/pages/SecretariesPage.tsx +++ b/assets/admin/pages/SecretariesPage.tsx @@ -27,6 +27,7 @@ const DEFAULT_PERMISSIONS: SecretaryPermissions = { sms: { view: false, create: false, update: false, delete: false }, appointment_settings: { view: false, update: false }, clinic_doctors: { view: false, create: false, update: false, delete: false }, + subscription: { view: false, create: false }, }; type PermSection = keyof SecretaryPermissions; @@ -154,6 +155,13 @@ const PERMISSION_LABELS: RecordsecretaryAccess->denyUnlessGranted($user, 'subscription', 'create'); $data = json_decode($request->getContent(), true) ?? []; $gatewayName = trim($data['gateway'] ?? 'mellat'); $frontendAddress = trim($data['frontend_address'] ?? ''); diff --git a/src/Secretary/Entity/DoctorSecretary.php b/src/Secretary/Entity/DoctorSecretary.php index dd7cb839..a8371be4 100644 --- a/src/Secretary/Entity/DoctorSecretary.php +++ b/src/Secretary/Entity/DoctorSecretary.php @@ -34,6 +34,7 @@ class DoctorSecretary 'sms' => ['view' => false, 'create' => false, 'update' => false, 'delete' => false], 'appointment_settings' => ['view' => false, 'update' => false], 'clinic_doctors' => ['view' => false, 'create' => false, 'update' => false, 'delete' => false], + 'subscription' => ['view' => false, 'create' => false], ], ]; diff --git a/src/Subscription/Controller/SubscriptionController.php b/src/Subscription/Controller/SubscriptionController.php index 3b21c931..e1416a22 100644 --- a/src/Subscription/Controller/SubscriptionController.php +++ b/src/Subscription/Controller/SubscriptionController.php @@ -35,6 +35,7 @@ class SubscriptionController extends BaseController private readonly ClinicRepository $clinicRepo, private readonly UserActiveContextRepository $contextRepo, private readonly EntityManagerInterface $em, + private readonly \App\Secretary\Security\SecretaryAccessChecker $secretaryAccess, ) {} // ── Public ────────────────────────────────────────────────────────────── @@ -56,6 +57,7 @@ class SubscriptionController extends BaseController #[IsGranted('IS_AUTHENTICATED_FULLY')] public function my(#[CurrentUser] User $user): JsonResponse { + $this->secretaryAccess->denyUnlessGranted($user, 'subscription', 'view'); [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); @@ -76,6 +78,7 @@ class SubscriptionController extends BaseController #[IsGranted('IS_AUTHENTICATED_FULLY')] public function trial(#[CurrentUser] User $user): JsonResponse { + $this->secretaryAccess->denyUnlessGranted($user, 'subscription', 'create'); [$entityType, $entityId] = $this->resolveEntity($user); if ($entityId === null) { return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403); diff --git a/tests/Secretary/SecretaryResourceEnforcementTest.php b/tests/Secretary/SecretaryResourceEnforcementTest.php index fb0d05ce..5a55bbc1 100644 --- a/tests/Secretary/SecretaryResourceEnforcementTest.php +++ b/tests/Secretary/SecretaryResourceEnforcementTest.php @@ -226,6 +226,25 @@ class SecretaryResourceEnforcementTest extends ApiTestCase $this->assertSame(200, $this->responseCode()); } + public function testSubscriptionDeniedByDefault(): void + { + [$secretary] = $this->makeClinicSecretary(); + $this->em->flush(); + + $this->authJson('GET', '/api/v1/subscription/my', $secretary); + $this->assertSame(403, $this->responseCode()); + } + + public function testSubscriptionAllowedWhenGranted(): void + { + [$secretary, $rel] = $this->makeClinicSecretary(); + $rel->mergePermissions(['resources' => ['subscription' => ['view' => true]]]); + $this->em->flush(); + + $this->authJson('GET', '/api/v1/subscription/my', $secretary); + $this->assertSame(200, $this->responseCode()); + } + public function testPatientDeleteSeparateFromUpdate(): void { // منشی با patients.update ولی بدون patients.delete نباید بتواند حذف کند.