From 938f351ede52a754c0fa0e14ca918edd855c7873 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 20 Jul 2026 09:21:26 +0000 Subject: [PATCH] Expand specialty RBAC with dedicated specialist roles and support access. Add cardiologist/ENT/derm/ophthalmology/ortho/podiatry/fertility roles and manage/refer/view matrices so GPs, nurses, lab, pharmacy, and reception get realistic module access beyond single-app specialists. Co-authored-by: Cursor --- app/Services/Care/CarePermissions.php | 197 +++++++++++++++-- app/Services/Care/SpecialtyModuleService.php | 33 +-- config/care.php | 7 + config/care_specialty_modules.php | 214 ++++++++++++++----- tests/Feature/CareNurseOrgAccessTest.php | 19 +- tests/Feature/CareSpecialtyAccessTest.php | 70 ++++-- tests/Feature/CareSpecialtyClinicalTest.php | 2 +- tests/Feature/CareSpecialtyModulesTest.php | 15 +- tests/Feature/CareSpecialtyShellTest.php | 10 +- 9 files changed, 451 insertions(+), 116 deletions(-) diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php index 1c357a3..4c59660 100644 --- a/app/Services/Care/CarePermissions.php +++ b/app/Services/Care/CarePermissions.php @@ -5,11 +5,13 @@ namespace App\Services\Care; use App\Models\Member; /** - * Care RBAC: role → abilities + primary specialty apps. + * Care RBAC: role → abilities + specialty manage / refer / view apps. * - * Authorization must use can() / primaryAppsFor() — never role string checks - * like isDoctor. Legacy `doctor` aliases to general_physician; `nurse` is a - * limited floor role (ED / theatre / dialysis nurses are separate). + * Authorization must use can() / primaryAppsFor() / referAppsFor() / + * viewAppsFor() — never role string checks like isDoctor. Legacy `doctor` + * aliases to general_physician for abilities; desk specialists are narrowed + * by SpecialtyModuleService keyword matching. `nurse` is a limited floor role + * (ED / theatre / dialysis nurses are separate). * * Inheritance: lab_manager ⊃ lab_technician; blood_bank_manager ⊃ blood_bank_officer. */ @@ -28,7 +30,7 @@ class CarePermissions /** * Specialty module keys each role may open (manage). Null = no specialty * matrix entry (fall back to module config allowlists + desk keywords). - * Empty list = no specialty modules. + * Empty list = no specialty manage. * * @var array|null> */ @@ -37,14 +39,20 @@ class CarePermissions 'hospital_admin' => null, 'emergency_physician' => ['emergency', 'radiology', 'pathology', 'blood_bank', 'cardiology'], 'ed_nurse' => ['emergency', 'blood_bank', 'radiology', 'pathology'], - 'general_physician' => ['emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology'], - 'doctor' => ['emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology'], // legacy → GP + 'general_physician' => [ + 'emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology', + 'ophthalmology', 'vaccination', 'child_welfare', + ], + 'doctor' => [ + 'emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology', + 'ophthalmology', 'vaccination', 'child_welfare', + ], // legacy → GP (desk specialists narrowed elsewhere) 'surgeon' => ['surgery', 'radiology', 'pathology', 'blood_bank'], 'theatre_nurse' => ['surgery', 'blood_bank'], 'radiologist' => ['radiology'], 'radiographer' => ['radiology'], - 'lab_technician' => ['pathology'], - 'lab_manager' => ['pathology'], + 'lab_technician' => ['pathology', 'blood_bank'], + 'lab_manager' => ['pathology', 'blood_bank'], 'pathologist' => ['pathology'], 'blood_bank_officer' => ['blood_bank'], 'blood_bank_manager' => ['blood_bank'], @@ -55,14 +63,59 @@ class CarePermissions 'midwife' => ['maternity'], 'pediatrician' => ['pediatrics'], 'oncologist' => ['oncology', 'infusion'], + 'cardiologist' => ['cardiology'], + 'ent_specialist' => ['ent'], + 'dermatologist' => ['dermatology'], + 'ophthalmologist' => ['ophthalmology'], + 'orthopedist' => ['orthopedics'], + 'podiatrist' => ['podiatry'], + 'fertility_specialist' => ['fertility'], 'dialysis_nurse' => ['renal'], 'ambulance_staff' => ['ambulance', 'emergency'], - 'receptionist' => [], // registration / queues — refer via queue.manage + 'receptionist' => [], // refer only — see roleReferApps 'billing_officer' => [], 'cashier' => [], 'accountant' => [], 'department_manager' => null, // department analytics — all enabled modules view - 'nurse' => [], // legacy limited — no specialty manage + 'nurse' => ['vaccination', 'child_welfare', 'infusion', 'maternity'], + ]; + + /** + * Specialty modules a role may refer into (beyond manage). Empty / missing = none + * beyond primary apps. Manage always implies refer. + * + * @var array> + */ + protected array $roleReferApps = [ + 'general_physician' => [ + 'dentistry', 'physiotherapy', 'maternity', 'psychiatry', 'orthopedics', + 'oncology', 'renal', 'surgery', 'radiology', 'pathology', 'blood_bank', + 'infusion', 'podiatry', 'fertility', 'ambulance', + ], + 'doctor' => [ + 'dentistry', 'physiotherapy', 'maternity', 'psychiatry', 'orthopedics', + 'oncology', 'renal', 'surgery', 'radiology', 'pathology', 'blood_bank', + 'infusion', 'podiatry', 'fertility', 'ambulance', + ], + 'nurse' => ['emergency', 'pediatrics', 'blood_bank'], + 'pharmacist' => ['oncology', 'infusion', 'maternity', 'emergency'], + 'receptionist' => [ + 'emergency', 'pediatrics', 'vaccination', 'child_welfare', 'dentistry', + 'maternity', 'ambulance', 'ophthalmology', 'physiotherapy', + ], + ]; + + /** + * Specialty modules a role may view without refer/manage. Empty / missing = none + * beyond primary + refer. Manage and refer always imply view. + * + * @var array> + */ + protected array $roleViewApps = [ + 'lab_technician' => ['radiology'], + 'lab_manager' => ['radiology'], + 'pathologist' => ['blood_bank'], + 'pharmacist' => ['oncology', 'infusion', 'maternity', 'emergency'], ]; /** @@ -106,6 +159,7 @@ class CarePermissions 'pathways.manage', 'emergency.queue.view', 'emergency.discharge', 'cardiology.view', 'pediatrics.view', 'ent.view', 'dermatology.view', + 'ophthalmology.view', 'queue.patient_board', 'service_queues.console', ], @@ -121,6 +175,7 @@ class CarePermissions 'pathways.manage', 'emergency.queue.view', 'emergency.discharge', 'cardiology.view', 'pediatrics.view', 'ent.view', 'dermatology.view', + 'ophthalmology.view', 'queue.patient_board', 'service_queues.console', ], @@ -162,6 +217,7 @@ class CarePermissions 'dashboard.view', 'patients.view', 'lab.view', 'lab.manage', 'pathology.view', 'pathology.result.enter', + 'blood_bank.view', 'blood_bank.issue', // cannot: pathology.result.approve, lab.catalog.manage 'service_queues.console', ], @@ -170,6 +226,7 @@ class CarePermissions 'pathology.result.approve', 'analytics.lab.view', 'lab.admin', + 'blood_bank.manage', ], 'pathologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', @@ -252,6 +309,75 @@ class CarePermissions 'infusion.view', 'infusion.manage', 'service_queues.console', ], + 'cardiologist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'lab.view', 'lab.results.view', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'cardiology.view', 'cardiology.manage', + 'service_queues.console', + ], + 'ent_specialist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'ent.view', 'ent.manage', + 'service_queues.console', + ], + 'dermatologist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'dermatology.view', 'dermatology.manage', + 'service_queues.console', + ], + 'ophthalmologist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'ophthalmology.view', 'ophthalmology.manage', + 'service_queues.console', + ], + 'orthopedist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'orthopedics.view', 'orthopedics.manage', + 'service_queues.console', + ], + 'podiatrist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', + 'podiatry.view', 'podiatry.manage', + 'service_queues.console', + ], + 'fertility_specialist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'fertility.view', 'fertility.manage', + 'service_queues.console', + ], 'dialysis_nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', @@ -301,7 +427,7 @@ class CarePermissions 'reports.finance.view', 'service_queues.console', ], - // Legacy nurse — floor vitals / assessments only; no specialty modules. + // Legacy floor nurse — vaccination / CWC / infusion / maternity manage. 'nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', @@ -317,8 +443,10 @@ class CarePermissions 'lab_technician', 'lab_manager', 'pathologist', 'blood_bank_officer', 'blood_bank_manager', 'pharmacist', 'dentist', 'psychiatrist', 'physiotherapist', - 'midwife', 'pediatrician', 'oncologist', 'dialysis_nurse', - 'ambulance_staff', 'receptionist', 'cashier', 'billing_officer', + 'midwife', 'pediatrician', 'oncologist', + 'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist', + 'orthopedist', 'podiatrist', 'fertility_specialist', + 'dialysis_nurse', 'ambulance_staff', 'receptionist', 'cashier', 'billing_officer', 'nurse', ]; @@ -327,6 +455,8 @@ class CarePermissions 'emergency_physician', 'general_physician', 'doctor', 'surgeon', 'radiologist', 'pathologist', 'dentist', 'psychiatrist', 'physiotherapist', 'midwife', 'pediatrician', 'oncologist', + 'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist', + 'orthopedist', 'podiatrist', 'fertility_specialist', 'ed_nurse', 'theatre_nurse', 'dialysis_nurse', 'ambulance_staff', 'radiographer', 'lab_technician', 'lab_manager', 'blood_bank_officer', 'blood_bank_manager', 'pharmacist', 'nurse', @@ -381,7 +511,7 @@ class CarePermissions } /** - * Primary specialty module keys for this member's role. + * Primary specialty module keys for this member's role (manage). * * @return list|null null = unrestricted specialty (admins / dept manager use other rules) */ @@ -403,6 +533,41 @@ class CarePermissions return $this->rolePrimaryApps[$role]; } + /** + * Specialty modules this role may refer into (beyond manage). + * + * @return list + */ + public function referAppsFor(?Member $member): array + { + if ($member === null || $this->isAdmin($member)) { + return []; + } + + $role = (string) $member->role; + if ($role === 'department_manager') { + return []; + } + + return array_values(array_unique($this->roleReferApps[$role] ?? [])); + } + + /** + * Specialty modules this role may view without refer/manage. + * + * @return list + */ + public function viewAppsFor(?Member $member): array + { + if ($member === null || $this->isAdmin($member)) { + return []; + } + + $role = (string) $member->role; + + return array_values(array_unique($this->roleViewApps[$role] ?? [])); + } + /** * Whether the role matrix lists this specialty module as a primary app. */ @@ -485,6 +650,8 @@ class CarePermissions 'emergency_physician', 'general_physician', 'doctor', 'surgeon', 'radiologist', 'pathologist', 'dentist', 'psychiatrist', 'pediatrician', 'oncologist', 'physiotherapist', 'midwife', + 'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist', + 'orthopedist', 'podiatrist', 'fertility_specialist', ], true); } diff --git a/app/Services/Care/SpecialtyModuleService.php b/app/Services/Care/SpecialtyModuleService.php index a89ca5e..24a30b5 100644 --- a/app/Services/Care/SpecialtyModuleService.php +++ b/app/Services/Care/SpecialtyModuleService.php @@ -358,7 +358,8 @@ class SpecialtyModuleService */ public function memberCanView(Organization $organization, ?Member $member, string $key): bool { - if ($this->memberCanManage($organization, $member, $key)) { + if ($this->memberCanManage($organization, $member, $key) + || $this->memberCanRefer($organization, $member, $key)) { return true; } @@ -380,14 +381,14 @@ class SpecialtyModuleService return $this->specialistBelongsToModule($organization, $member, $key); } - // Legacy doctor (non-desk) / GP: keep refer/view on restricted modules - // outside primary manage via config view_roles when role is doctor/GP. - if (in_array((string) $member->role, ['doctor', 'general_physician'], true) - && ! in_array($key, $primaryApps, true)) { - $definition = $this->definition($key); - if ($definition && $this->roleListed($member, $definition['view_roles'] ?? [])) { - return true; - } + if (in_array($key, $permissions->viewAppsFor($member), true)) { + return true; + } + + // Secondary: config view_roles (docs + legacy allowlists). + $definition = $this->definition($key); + if ($definition && $this->roleListed($member, $definition['view_roles'] ?? [])) { + return true; } return in_array($key, $primaryApps, true); @@ -428,12 +429,14 @@ class SpecialtyModuleService return $this->specialistBelongsToModule($organization, $member, $key); } - if (in_array((string) $member->role, ['doctor', 'general_physician'], true) - && ! in_array($key, $primaryApps, true)) { - $definition = $this->definition($key); - if ($definition && $this->roleListed($member, $definition['refer_roles'] ?? [])) { - return true; - } + if (in_array($key, $permissions->referAppsFor($member), true)) { + return true; + } + + // Secondary: config refer_roles (docs + legacy allowlists). + $definition = $this->definition($key); + if ($definition && $this->roleListed($member, $definition['refer_roles'] ?? [])) { + return true; } return in_array($key, $primaryApps, true); diff --git a/config/care.php b/config/care.php index 2d8e956..a50adfd 100644 --- a/config/care.php +++ b/config/care.php @@ -26,6 +26,13 @@ return [ 'midwife' => 'Midwife', 'pediatrician' => 'Pediatrician', 'oncologist' => 'Oncologist', + 'cardiologist' => 'Cardiologist', + 'ent_specialist' => 'ENT Specialist', + 'dermatologist' => 'Dermatologist', + 'ophthalmologist' => 'Ophthalmologist', + 'orthopedist' => 'Orthopedist', + 'podiatrist' => 'Podiatrist', + 'fertility_specialist' => 'Fertility Specialist', 'dialysis_nurse' => 'Dialysis Nurse', 'ambulance_staff' => 'Ambulance Staff', 'nurse' => 'Nurse (legacy)', diff --git a/config/care_specialty_modules.php b/config/care_specialty_modules.php index 851d5af..e98633b 100644 --- a/config/care_specialty_modules.php +++ b/config/care_specialty_modules.php @@ -4,10 +4,10 @@ * Specialty practice modules (Settings → Modules). * Pro-gated via plans.*.features specialty_modules. * - * Primary specialty visibility is driven by CarePermissions::primaryAppsFor() - * (role → module keys). Config roles / support_roles / view_roles / refer_roles - * remain for legacy doctor desk-specialist keyword matching and GP refer/view - * outside the primary-app matrix. + * Primary specialty visibility is driven by CarePermissions::primaryAppsFor() / + * referAppsFor() / viewAppsFor() (role → module keys). Config roles / + * support_roles / view_roles / refer_roles remain for legacy doctor desk-specialist + * keyword matching and as a secondary allowlist aligned with the matrix. * * @return array> */ @@ -23,7 +23,17 @@ return [ 'icon' => 'bolt', 'queue_keywords' => ['emerg', 'casualty', 'trauma', 'er', 'a&e'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'lab_technician', 'pharmacist', 'receptionist'], + 'roles' => [ + 'emergency_physician', 'ed_nurse', 'general_physician', 'doctor', + 'ambulance_staff', + ], + 'view_roles' => [ + 'emergency_physician', 'ed_nurse', 'general_physician', 'doctor', + 'ambulance_staff', 'nurse', 'pharmacist', 'receptionist', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'nurse', 'pharmacist', 'receptionist', + ], 'default_on_paid_plans' => true, ], 'blood_bank' => [ @@ -37,7 +47,18 @@ return [ 'icon' => 'droplet', 'queue_keywords' => ['blood', 'transfusion', 'crossmatch', 'donor'], 'access' => 'general', - 'roles' => ['blood_bank_manager', 'lab_technician', 'nurse', 'doctor', 'receptionist'], + 'roles' => [ + 'blood_bank_officer', 'blood_bank_manager', 'lab_technician', 'lab_manager', + 'emergency_physician', 'ed_nurse', 'surgeon', 'theatre_nurse', + ], + 'view_roles' => [ + 'blood_bank_officer', 'blood_bank_manager', 'lab_technician', 'lab_manager', + 'pathologist', 'emergency_physician', 'ed_nurse', 'surgeon', 'theatre_nurse', + 'general_physician', 'doctor', 'nurse', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'nurse', + ], 'default_on_paid_plans' => true, ], 'dentistry' => [ @@ -51,7 +72,9 @@ return [ 'icon' => 'face-smile', 'queue_keywords' => ['dent', 'dental', 'oral'], 'access' => 'limited', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['dentist'], + 'view_roles' => ['dentist', 'general_physician', 'doctor', 'receptionist'], + 'refer_roles' => ['general_physician', 'doctor', 'receptionist'], 'specialist_keywords' => ['dent', 'dental', 'oral', 'odont'], ], 'ophthalmology' => [ @@ -65,7 +88,11 @@ return [ 'icon' => 'eye', 'queue_keywords' => ['eye', 'ophthal', 'optom'], 'access' => 'limited', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['ophthalmologist', 'general_physician', 'doctor'], + 'view_roles' => [ + 'ophthalmologist', 'general_physician', 'doctor', 'receptionist', + ], + 'refer_roles' => ['general_physician', 'doctor', 'receptionist'], 'specialist_keywords' => ['eye', 'ophthal', 'optom'], ], 'physiotherapy' => [ @@ -79,7 +106,11 @@ return [ 'icon' => 'arrow-path', 'queue_keywords' => ['physio', 'rehab', 'therapy'], 'access' => 'limited', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['physiotherapist'], + 'view_roles' => [ + 'physiotherapist', 'general_physician', 'doctor', 'receptionist', + ], + 'refer_roles' => ['general_physician', 'doctor', 'receptionist'], 'specialist_keywords' => ['physio', 'rehab', 'therapy'], ], 'maternity' => [ @@ -90,10 +121,16 @@ return [ 'queue_name' => 'Maternity', 'queue_prefix' => 'MAT', 'nav_label' => 'Maternity', - 'icon' => 'heart', + 'icon' => 'home', 'queue_keywords' => ['matern', 'antenatal', 'obstetric'], 'access' => 'limited', - 'roles' => ['doctor', 'nurse', 'pharmacist', 'receptionist'], + 'roles' => ['midwife', 'nurse'], + 'view_roles' => [ + 'midwife', 'nurse', 'general_physician', 'doctor', 'pharmacist', 'receptionist', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'pharmacist', 'receptionist', + ], 'specialist_keywords' => ['matern', 'antenatal', 'obstetric', 'midwif'], ], 'radiology' => [ @@ -107,7 +144,16 @@ return [ 'icon' => 'camera', 'queue_keywords' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'lab_technician', 'receptionist'], + 'roles' => [ + 'radiologist', 'radiographer', 'emergency_physician', 'ed_nurse', 'surgeon', + ], + 'view_roles' => [ + 'radiologist', 'radiographer', 'emergency_physician', 'ed_nurse', 'surgeon', + 'lab_technician', 'lab_manager', 'general_physician', 'doctor', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', + ], 'specialist_keywords' => ['radio', 'radiolog', 'imaging'], ], 'cardiology' => [ @@ -121,10 +167,13 @@ return [ 'icon' => 'heart', 'queue_keywords' => ['cardio', 'heart', 'ecg', 'echo'], 'access' => 'restricted', - 'roles' => ['doctor'], - 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse', 'lab_technician'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'roles' => ['cardiologist', 'emergency_physician', 'general_physician', 'doctor'], + 'support_roles' => ['ed_nurse'], + 'view_roles' => [ + 'cardiologist', 'emergency_physician', 'general_physician', 'doctor', + 'ed_nurse', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['cardio', 'cardiology', 'heart', 'ecg'], ], 'psychiatry' => [ @@ -138,10 +187,12 @@ return [ 'icon' => 'chat-bubble-left-right', 'queue_keywords' => ['psych', 'mental', 'behaviour'], 'access' => 'restricted', - 'roles' => ['doctor'], - 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'roles' => ['psychiatrist'], + 'support_roles' => [], + 'view_roles' => [ + 'psychiatrist', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['psych', 'psychiatry', 'mental', 'behaviour'], ], 'pediatrics' => [ @@ -155,7 +206,13 @@ return [ 'icon' => 'user-group', 'queue_keywords' => ['pedia', 'paedia', 'child', 'infant'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['pediatrician', 'general_physician', 'doctor'], + 'view_roles' => [ + 'pediatrician', 'general_physician', 'doctor', 'nurse', 'receptionist', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'nurse', 'receptionist', + ], // Avoid bare "child" — collides with Child Welfare Clinic specialty labels. 'specialist_keywords' => ['pedia', 'paedia', 'pediatric', 'paediatric', 'infant'], ], @@ -170,10 +227,12 @@ return [ 'icon' => 'cube', 'queue_keywords' => ['ortho', 'fracture', 'bone', 'joint'], 'access' => 'restricted', - 'roles' => ['doctor'], - 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'roles' => ['orthopedist'], + 'support_roles' => [], + 'view_roles' => [ + 'orthopedist', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['ortho', 'orthopedic', 'orthopaedic', 'fracture'], ], 'ent' => [ @@ -187,10 +246,12 @@ return [ 'icon' => 'speaker-wave', 'queue_keywords' => ['ent', 'ear', 'nose', 'throat', 'otolaryng'], 'access' => 'restricted', - 'roles' => ['doctor'], - 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'roles' => ['ent_specialist', 'general_physician', 'doctor'], + 'support_roles' => [], + 'view_roles' => [ + 'ent_specialist', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['ent', 'otolaryng', 'ear', 'nose', 'throat'], ], 'oncology' => [ @@ -204,10 +265,14 @@ return [ 'icon' => 'shield-exclamation', 'queue_keywords' => ['oncol', 'cancer', 'chemo'], 'access' => 'restricted', - 'roles' => ['doctor'], + 'roles' => ['oncologist'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse', 'pharmacist'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'view_roles' => [ + 'oncologist', 'general_physician', 'doctor', 'pharmacist', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'pharmacist', + ], 'specialist_keywords' => ['oncol', 'cancer', 'chemo'], ], 'renal' => [ @@ -221,10 +286,12 @@ return [ 'icon' => 'circle-stack', 'queue_keywords' => ['renal', 'dialysis', 'nephro', 'kidney'], 'access' => 'restricted', - 'roles' => ['doctor'], - 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'roles' => ['dialysis_nurse'], + 'support_roles' => [], + 'view_roles' => [ + 'dialysis_nurse', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['renal', 'nephro', 'dialysis', 'kidney'], ], 'surgery' => [ @@ -238,10 +305,12 @@ return [ 'icon' => 'wrench-screwdriver', 'queue_keywords' => ['surg', 'theatre', 'operat', 'pre-op'], 'access' => 'restricted', - 'roles' => ['doctor'], - 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'roles' => ['surgeon', 'theatre_nurse'], + 'support_roles' => [], + 'view_roles' => [ + 'surgeon', 'theatre_nurse', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['surg', 'theatre', 'operat'], ], 'vaccination' => [ @@ -255,7 +324,11 @@ return [ 'icon' => 'shield-check', 'queue_keywords' => ['vaccin', 'immun', 'epi', 'injection'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['general_physician', 'doctor', 'nurse'], + 'view_roles' => [ + 'general_physician', 'doctor', 'nurse', 'receptionist', + ], + 'refer_roles' => ['general_physician', 'doctor', 'receptionist'], 'specialist_keywords' => ['vaccin', 'immun', 'epi'], ], 'pathology' => [ @@ -269,7 +342,18 @@ return [ 'icon' => 'document-magnifying-glass', 'queue_keywords' => ['pathol', 'histo', 'cytol', 'biopsy'], 'access' => 'general', - 'roles' => ['lab_technician', 'doctor', 'nurse', 'receptionist'], + 'roles' => [ + 'lab_technician', 'lab_manager', 'pathologist', + 'emergency_physician', 'ed_nurse', 'surgeon', + ], + 'view_roles' => [ + 'lab_technician', 'lab_manager', 'pathologist', + 'emergency_physician', 'ed_nurse', 'surgeon', + 'general_physician', 'doctor', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', + ], 'specialist_keywords' => ['pathol', 'histo', 'cytol'], ], 'infusion' => [ @@ -283,7 +367,13 @@ return [ 'icon' => 'beaker', 'queue_keywords' => ['infusion', 'iv', 'drip', 'day care'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'pharmacist', 'receptionist'], + 'roles' => ['oncologist', 'nurse'], + 'view_roles' => [ + 'oncologist', 'nurse', 'general_physician', 'doctor', 'pharmacist', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'pharmacist', + ], 'specialist_keywords' => ['infusion', 'iv therapy', 'day care'], ], 'dermatology' => [ @@ -297,10 +387,12 @@ return [ 'icon' => 'swatch', 'queue_keywords' => ['derma', 'skin', 'rash'], 'access' => 'restricted', - 'roles' => ['doctor'], + 'roles' => ['dermatologist', 'general_physician', 'doctor'], 'support_roles' => [], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'view_roles' => [ + 'dermatologist', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['derma', 'skin'], ], 'podiatry' => [ @@ -314,10 +406,12 @@ return [ 'icon' => 'finger-print', 'queue_keywords' => ['podiat', 'foot', 'diabetic foot'], 'access' => 'restricted', - 'roles' => ['doctor'], + 'roles' => ['podiatrist'], 'support_roles' => [], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'view_roles' => [ + 'podiatrist', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['podiat', 'foot'], ], 'fertility' => [ @@ -331,10 +425,12 @@ return [ 'icon' => 'sparkles', 'queue_keywords' => ['fertil', 'ivf', 'reproduct'], 'access' => 'restricted', - 'roles' => ['doctor'], + 'roles' => ['fertility_specialist'], 'support_roles' => [], - 'view_roles' => ['doctor', 'general_physician', 'nurse'], - 'refer_roles' => ['doctor', 'general_physician', 'nurse'], + 'view_roles' => [ + 'fertility_specialist', 'general_physician', 'doctor', + ], + 'refer_roles' => ['general_physician', 'doctor'], 'specialist_keywords' => ['fertil', 'ivf', 'reproduct'], ], 'child_welfare' => [ @@ -348,7 +444,11 @@ return [ 'icon' => 'gift', 'queue_keywords' => ['cwc', 'child welfare', 'well baby', 'well-child', 'welfare clinic'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['general_physician', 'doctor', 'nurse'], + 'view_roles' => [ + 'general_physician', 'doctor', 'nurse', 'receptionist', + ], + 'refer_roles' => ['general_physician', 'doctor', 'receptionist'], // Prefer multi-word / CWC stems — do not use bare "child" (steals pediatrics). 'specialist_keywords' => ['child welfare', 'cwc', 'welfare clinic', 'well baby', 'well-child'], ], @@ -364,7 +464,13 @@ return [ // Avoid ED stems (emergency, casualty, trauma, er, a&e) — those belong to emergency. 'queue_keywords' => ['ambulance', 'ems', 'paramedic', 'prehospital', 'ambulance service'], 'access' => 'general', - 'roles' => ['doctor', 'nurse', 'receptionist'], + 'roles' => ['ambulance_staff'], + 'view_roles' => [ + 'ambulance_staff', 'general_physician', 'doctor', 'receptionist', + ], + 'refer_roles' => [ + 'general_physician', 'doctor', 'receptionist', + ], 'specialist_keywords' => ['ambulance', 'ems', 'paramedic', 'prehospital'], ], ]; diff --git a/tests/Feature/CareNurseOrgAccessTest.php b/tests/Feature/CareNurseOrgAccessTest.php index b395014..96c842b 100644 --- a/tests/Feature/CareNurseOrgAccessTest.php +++ b/tests/Feature/CareNurseOrgAccessTest.php @@ -123,7 +123,7 @@ class CareNurseOrgAccessTest extends TestCase 'owner_ref' => $owner->public_id, 'organization_id' => $employer->id, 'user_ref' => strtolower($nurse->email), - 'role' => 'nurse', + 'role' => 'ed_nurse', ]); $branch = Branch::create([ @@ -195,7 +195,7 @@ class CareNurseOrgAccessTest extends TestCase $nurse = User::create([ 'public_id' => 'er-docs-nurse', - 'name' => 'Nurse', + 'name' => 'ED Nurse', 'email' => 'er-docs-nurse@example.com', ]); @@ -222,7 +222,7 @@ class CareNurseOrgAccessTest extends TestCase 'owner_ref' => $owner->public_id, 'organization_id' => $organization->id, 'user_ref' => $nurse->public_id, - 'role' => 'nurse', + 'role' => 'ed_nurse', ]); $branch = Branch::create([ @@ -299,8 +299,8 @@ class CareNurseOrgAccessTest extends TestCase $organization = Organization::create([ 'owner_ref' => $owner->public_id, - 'name' => 'Cardio Clinic', - 'slug' => 'cardio-docs-clinic', + 'name' => 'Rad Clinic', + 'slug' => 'rad-docs-clinic', 'settings' => [ 'onboarded' => true, 'plan' => 'pro', @@ -330,9 +330,9 @@ class CareNurseOrgAccessTest extends TestCase ]); $modules = app(SpecialtyModuleService::class); - $modules->activate($organization, $owner->public_id, 'cardiology'); + $modules->activate($organization, $owner->public_id, 'radiology'); - $dept = Department::query()->where('type', 'cardiology')->where('branch_id', $branch->id)->firstOrFail(); + $dept = Department::query()->where('type', 'radiology')->where('branch_id', $branch->id)->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $owner->public_id, @@ -369,11 +369,12 @@ class CareNurseOrgAccessTest extends TestCase ]); $labMember = Member::query()->where('user_ref', $lab->public_id)->firstOrFail(); - $this->assertSame('view', $modules->memberAccessLevel($organization, $labMember, 'cardiology')); + // Lab manages pathology/blood bank; radiology is view-only. + $this->assertSame('view', $modules->memberAccessLevel($organization, $labMember, 'radiology')); $this->actingAs($lab) ->get(route('care.specialty.workspace', [ - 'module' => 'cardiology', + 'module' => 'radiology', 'visit' => $visit, 'tab' => 'documents', ])) diff --git a/tests/Feature/CareSpecialtyAccessTest.php b/tests/Feature/CareSpecialtyAccessTest.php index 622bc2c..cc6e23a 100644 --- a/tests/Feature/CareSpecialtyAccessTest.php +++ b/tests/Feature/CareSpecialtyAccessTest.php @@ -95,22 +95,40 @@ class CareSpecialtyAccessTest extends TestCase public function test_nurse_and_pharmacist_manage_general_modules(): void { + foreach (['vaccination', 'child_welfare', 'maternity'] as $key) { + $this->modules->activate($this->organization->fresh(), $this->owner->public_id, $key); + } + $this->organization->refresh(); + [, $nurse] = $this->makeStaff('nurse', 'nurse1'); [, $edNurse] = $this->makeStaff('ed_nurse', 'edn1'); [, $pharmacist] = $this->makeStaff('pharmacist', 'rx1'); [, $lab] = $this->makeStaff('lab_technician', 'lab1'); + [, $receptionist] = $this->makeStaff('receptionist', 'rec1'); - // Legacy nurse is limited — no specialty modules. + // Legacy floor nurse: vaccination / CWC / infusion / maternity manage; emergency refer only. $this->assertFalse($this->modules->memberCanManage($this->organization, $nurse, 'emergency')); + $this->assertTrue($this->modules->memberCanRefer($this->organization, $nurse, 'emergency')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'vaccination')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'infusion')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'maternity')); // ED nurse matrix apps: Emergency, Blood Bank, Radiology, Pathology. $this->assertTrue($this->modules->memberCanManage($this->organization, $edNurse, 'emergency')); $this->assertTrue($this->modules->memberCanManage($this->organization, $edNurse, 'pathology')); - // Pharmacist is pharmacy-only (core app) — no specialty modules. + // Pharmacist: no specialty manage; refer/view pharmacy-adjacent modules. $this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'emergency')); $this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'infusion')); - $this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'pathology')); - $this->assertFalse($this->modules->memberCanManage($this->organization, $lab, 'blood_bank')); + $this->assertTrue($this->modules->memberCanRefer($this->organization, $pharmacist, 'infusion')); + $this->assertTrue($this->modules->memberCanView($this->organization, $pharmacist, 'emergency')); $this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'cardiology')); + // Lab: pathology + blood bank manage; radiology view. + $this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'pathology')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'blood_bank')); + $this->assertSame('view', $this->modules->memberAccessLevel($this->organization, $lab, 'radiology')); + // Receptionist: refer only (no specialty manage). + $this->assertFalse($this->modules->memberCanManage($this->organization, $receptionist, 'emergency')); + $this->assertTrue($this->modules->memberCanRefer($this->organization, $receptionist, 'emergency')); + $this->assertTrue($this->modules->memberCanRefer($this->organization, $receptionist, 'dentistry')); } public function test_gp_doctor_manages_general_but_only_views_restricted(): void @@ -129,17 +147,33 @@ class CareSpecialtyAccessTest extends TestCase $this->assertFalse($this->modules->isDeskSpecialist($this->organization, $member)); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'emergency')); - // Dentistry is not a GP primary app. + // Dentistry is not a GP primary app — refer only. $this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'dentistry')); - // Cardiology is a GP primary app (manage). + $this->assertTrue($this->modules->memberCanRefer($this->organization, $member, 'dentistry')); + // Cardiology / ophthalmology are GP primary apps (manage). $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology')); - // Psychiatry is outside GP apps — refer/view via legacy config for GP. + $this->modules->activate($this->organization->fresh(), $this->owner->public_id, 'ophthalmology'); + $this->organization->refresh(); + $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'ophthalmology')); + // Psychiatry is outside GP manage — refer/view via roleReferApps. $this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'psychiatry')); $this->assertTrue($this->modules->memberCanView($this->organization, $member, 'psychiatry')); $this->assertTrue($this->modules->memberCanRefer($this->organization, $member, 'psychiatry')); $this->assertSame('refer', $this->modules->memberAccessLevel($this->organization, $member, 'psychiatry')); } + public function test_dedicated_cardiologist_role_manages_cardiology_only(): void + { + [, $member] = $this->makeStaff('cardiologist', 'cardio-role'); + + $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology')); + $this->assertSame('manage', $this->modules->memberAccessLevel($this->organization, $member, 'cardiology')); + $this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'emergency')); + $this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'dentistry')); + $this->assertTrue(app(\App\Services\Care\CarePermissions::class)->isSingleAppSpecialist($member)); + $this->assertFalse($this->modules->shouldShowSpecialtyNav($this->organization, $member)); + } + public function test_cardiologist_manages_restricted_cardiology(): void { [$doctor, $member] = $this->makeStaff('doctor', 'cardio1'); @@ -237,7 +271,7 @@ class CareSpecialtyAccessTest extends TestCase ->assertSee('>Specialty

', false) ->assertSee('Emergency') ->assertSee('Cardiology') - ->assertDontSee('Dentistry'); + ->assertSee('Dentistry'); // GP refer access } public function test_dentistry_specialty_does_not_match_ent_keyword(): void @@ -382,9 +416,11 @@ class CareSpecialtyAccessTest extends TestCase ->pluck('key') ->all(); - // Pharmacist: pharmacy core only — no specialty modules. - $this->assertSame([], $enabledForPharmacist); - $this->assertFalse($this->modules->shouldShowSpecialtyNav($this->organization, $pharmacistMember)); + // Pharmacist: pharmacy-adjacent specialty refer/view (multi-app → Specialty nav). + $this->assertContains('emergency', $enabledForPharmacist); + $this->assertContains('infusion', $enabledForPharmacist); + $this->assertNotContains('cardiology', $enabledForPharmacist); + $this->assertTrue($this->modules->shouldShowSpecialtyNav($this->organization, $pharmacistMember)); $this->assertContains('emergency', $enabledForEp); $this->assertContains('radiology', $enabledForEp); @@ -394,7 +430,7 @@ class CareSpecialtyAccessTest extends TestCase $this->actingAs($pharmacistUser) ->get(route('care.dashboard')) ->assertOk() - ->assertDontSee('>Specialty

', false); + ->assertSee('>Specialty

', false); } public function test_manage_role_still_sees_mutate_actions_on_restricted_module(): void @@ -778,8 +814,18 @@ class CareSpecialtyAccessTest extends TestCase $this->assertTrue($permissions->can($lab, 'lab.manage')); $this->assertTrue($permissions->can($lab, 'pathology.result.enter')); $this->assertFalse($permissions->can($lab, 'pathology.result.approve')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'blood_bank')); $this->assertTrue($permissions->can($labMgr, 'pathology.result.approve')); $this->assertTrue($permissions->can($labMgr, 'lab.catalog.manage')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $labMgr, 'blood_bank')); + + [, $cardiologist] = $this->makeStaff('cardiologist', 'matrix-cardio'); + [, $ophthalmologist] = $this->makeStaff('ophthalmologist', 'matrix-oph'); + $this->modules->activate($this->organization->fresh(), $this->owner->public_id, 'ophthalmology'); + $this->organization->refresh(); + $this->assertTrue($this->modules->memberCanManage($this->organization, $cardiologist, 'cardiology')); + $this->assertFalse($this->modules->memberCanManage($this->organization, $cardiologist, 'emergency')); + $this->assertTrue($this->modules->memberCanManage($this->organization, $ophthalmologist, 'ophthalmology')); $this->assertTrue($permissions->can($billing, 'bills.manage')); $this->assertFalse($permissions->can($billing, 'payments.manage')); diff --git a/tests/Feature/CareSpecialtyClinicalTest.php b/tests/Feature/CareSpecialtyClinicalTest.php index f90e522..7261d23 100644 --- a/tests/Feature/CareSpecialtyClinicalTest.php +++ b/tests/Feature/CareSpecialtyClinicalTest.php @@ -54,7 +54,7 @@ class CareSpecialtyClinicalTest extends TestCase 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, - 'role' => 'nurse', + 'role' => 'hospital_admin', ]); $this->branch = Branch::create([ diff --git a/tests/Feature/CareSpecialtyModulesTest.php b/tests/Feature/CareSpecialtyModulesTest.php index a8d7249..6606ee9 100644 --- a/tests/Feature/CareSpecialtyModulesTest.php +++ b/tests/Feature/CareSpecialtyModulesTest.php @@ -216,16 +216,16 @@ class CareSpecialtyModulesTest extends TestCase Http::fake(); app(SpecialtyModuleService::class)->activate($this->organization, $this->owner->public_id, 'dentistry'); - $nurse = User::create([ - 'public_id' => 'modules-nurse', - 'name' => 'Floor Nurse', - 'email' => 'modules-nurse@example.com', + $receptionist = User::create([ + 'public_id' => 'modules-reception', + 'name' => 'Front Desk', + 'email' => 'modules-reception@example.com', ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, - 'user_ref' => $nurse->public_id, - 'role' => 'nurse', + 'user_ref' => $receptionist->public_id, + 'role' => 'receptionist', 'branch_id' => $this->branch->id, ]); @@ -234,7 +234,8 @@ class CareSpecialtyModulesTest extends TestCase ->assertOk() ->assertSee('Patient flow'); - $this->actingAs($nurse) + // Receptionist has refer access to dentistry (registration into specialty queues). + $this->actingAs($receptionist) ->get(route('care.specialty.show', 'dentistry')) ->assertOk() ->assertSee('Patient flow'); diff --git a/tests/Feature/CareSpecialtyShellTest.php b/tests/Feature/CareSpecialtyShellTest.php index 3bb5aea..ccb2130 100644 --- a/tests/Feature/CareSpecialtyShellTest.php +++ b/tests/Feature/CareSpecialtyShellTest.php @@ -55,7 +55,7 @@ class CareSpecialtyShellTest extends TestCase 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, - 'role' => 'nurse', + 'role' => 'dentist', ]); $this->branch = Branch::create([ @@ -99,6 +99,8 @@ class CareSpecialtyShellTest extends TestCase public function test_specialty_shell_overview_and_sections_render(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'emergency_physician']); + app(SpecialtyModuleService::class)->activate( $this->organization, $this->owner->public_id, @@ -153,6 +155,8 @@ class CareSpecialtyShellTest extends TestCase public function test_workspace_can_add_catalog_service_to_visit_bill(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'emergency_physician']); + app(SpecialtyModuleService::class)->activate( $this->organization, $this->owner->public_id, @@ -307,7 +311,7 @@ class CareSpecialtyShellTest extends TestCase public function test_workspace_includes_timeline_tab_and_complete_action(): void { - Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'dentist']); app(SpecialtyModuleService::class)->activate( $this->organization, @@ -393,7 +397,7 @@ class CareSpecialtyShellTest extends TestCase public function test_called_workspace_shows_call_again_until_session_starts(): void { - Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'dentist']); app(SpecialtyModuleService::class)->activate( $this->organization,