withoutMiddleware(EnsurePlatformSession::class); $this->owner = User::create([ 'public_id' => 'access-owner', 'name' => 'Owner', 'email' => 'access-owner@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Access Clinic', 'slug' => 'access-clinic', 'settings' => [ 'onboarded' => true, 'facility_type' => 'clinic', 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), 'queue_integration_enabled' => true, ], ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, 'role' => 'hospital_admin', ]); $this->branch = Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'Main', 'is_active' => true, ]); $this->modules = app(SpecialtyModuleService::class); $this->modules->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id); foreach (['cardiology', 'infusion', 'pathology', 'dentistry', 'psychiatry', 'surgery', 'radiology'] as $key) { $this->modules->activate($this->organization->fresh(), $this->owner->public_id, $key); } $this->organization->refresh(); } protected function makeStaff(string $role, string $suffix): array { $user = User::create([ 'public_id' => 'access-'.$suffix, 'name' => ucfirst($role).' '.$suffix, 'email' => $suffix.'@example.com', ]); $member = Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $user->public_id, 'role' => $role, 'branch_id' => $this->branch->id, ]); return [$user, $member]; } 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 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: 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->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 { [$doctor, $member] = $this->makeStaff('general_physician', 'gp1'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr GP', 'specialty' => 'General Practice', 'is_active' => true, ]); $this->assertFalse($this->modules->isDeskSpecialist($this->organization, $member)); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'emergency')); // Dentistry is not a GP primary app — refer only. $this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'dentistry')); $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')); $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'); $dept = Department::query()->where('type', 'cardiology')->firstOrFail(); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'department_id' => $dept->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr Cardio', 'specialty' => 'Cardiology', 'is_active' => true, ]); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology')); $this->assertSame('manage', $this->modules->memberAccessLevel($this->organization, $member, 'cardiology')); // Desk specialists do not keep GP-wide general/limited manage. $this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'emergency')); $this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'dentistry')); } public function test_dentistry_specialist_only_sees_matching_modules(): void { [$doctor, $member] = $this->makeStaff('doctor', 'dentist1'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr Dentist', 'specialty' => 'Dentistry', 'is_active' => true, ]); $this->assertTrue($this->modules->isDeskSpecialist($this->organization, $member)); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'dentistry')); $this->assertSame('manage', $this->modules->memberAccessLevel($this->organization, $member, 'dentistry')); foreach (['emergency', 'blood_bank', 'cardiology', 'infusion', 'pathology'] as $key) { $this->assertSame( 'none', $this->modules->memberAccessLevel($this->organization, $member, $key), "Dentistry specialist should not see [{$key}]", ); $this->assertFalse($this->modules->memberCanManage($this->organization, $member, $key)); $this->assertFalse($this->modules->memberCanView($this->organization, $member, $key)); $this->assertFalse($this->modules->memberCanRefer($this->organization, $member, $key)); } $enabledKeys = collect($this->modules->enabledModulesForMember($this->organization, $member)) ->pluck('key') ->all(); $this->assertContains('dentistry', $enabledKeys); $this->assertNotContains('emergency', $enabledKeys); $this->assertNotContains('blood_bank', $enabledKeys); $this->assertNotContains('cardiology', $enabledKeys); // Single-desk specialists: no Specialty sidebar group (dashboard cards remain). $this->assertFalse($this->modules->shouldShowSpecialtyNav($this->organization, $member)); $this->actingAs($doctor) ->get(route('care.dashboard')) ->assertOk() ->assertSee('Dentistry') ->assertSee('Specialty modules') ->assertDontSee('>Specialty

', false) ->assertDontSee('Emergency') ->assertDontSee('Cardiology') ->assertDontSee('Blood Bank'); } public function test_gp_keeps_specialty_sidebar_nav(): void { [$doctor, $member] = $this->makeStaff('doctor', 'gp-nav'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr GP Nav', 'specialty' => 'General Practice', 'is_active' => true, ]); $this->assertFalse($this->modules->isDeskSpecialist($this->organization, $member)); $this->assertTrue($this->modules->shouldShowSpecialtyNav($this->organization, $member)); $this->actingAs($doctor) ->get(route('care.dashboard')) ->assertOk() ->assertSee('>Specialty

', false) ->assertSee('Emergency') ->assertSee('Cardiology') ->assertSee('Dentistry'); // GP refer access } public function test_dentistry_specialty_does_not_match_ent_keyword(): void { [$doctor, $member] = $this->makeStaff('doctor', 'dentist-ent-trap'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr Dentist', 'specialty' => 'Dentistry', 'is_active' => true, ]); $this->assertTrue($this->modules->specialistBelongsToModule($this->organization, $member, 'dentistry')); $this->assertFalse($this->modules->specialistBelongsToModule($this->organization, $member, 'ent')); $this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'ent')); } public function test_gp_can_refer_into_restricted_specialty_queue(): void { [$doctor, $member] = $this->makeStaff('doctor', 'gp2'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr GP2', 'specialty' => 'General Practice', 'is_active' => true, ]); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-REF-1', 'first_name' => 'Ama', 'last_name' => 'Refer', 'gender' => 'female', 'date_of_birth' => '1990-01-01', ]); $this->actingAs($doctor) ->post(route('care.specialty.refer', 'cardiology'), [ 'patient_id' => $patient->id, 'branch_id' => $this->branch->id, 'reason' => 'Chest pain review', ]) ->assertRedirect(route('care.specialty.show', 'cardiology')); $this->assertDatabaseHas('care_appointments', [ 'patient_id' => $patient->id, 'reason' => 'Chest pain review', 'status' => Appointment::STATUS_WAITING, ]); } public function test_gp_cannot_save_clinical_on_restricted_module(): void { [$doctor, $member] = $this->makeStaff('general_physician', 'gp3'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr GP3', 'specialty' => 'General Practice', 'is_active' => true, ]); $dept = Department::query()->where('type', 'psychiatry')->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-PSY-1', 'first_name' => 'Kofi', 'last_name' => 'Mind', 'gender' => 'male', 'date_of_birth' => '1980-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_IN_CONSULTATION, 'scheduled_at' => now(), 'checked_in_at' => now(), 'started_at' => now(), ]); $this->actingAs($doctor) ->get(route('care.specialty.workspace', ['module' => 'psychiatry', 'visit' => $visit, 'tab' => 'exam'])) ->assertOk() ->assertSee('view + refer') ->assertDontSee('Save record') ->assertDontSee('Call next') ->assertDontSee('Add to invoice') ->assertDontSee('Upload document') ->assertSee('Refer to specialty queue') ->assertDontSee( 'action="'.route('care.specialty.consultation.start', ['module' => 'psychiatry', 'visit' => $visit], false).'"', false, ); $this->actingAs($doctor) ->post(route('care.specialty.clinical.save', ['module' => 'psychiatry', 'visit' => $visit]), [ 'tab' => 'exam', 'payload' => [ 'chief_complaint' => 'Should not save', ], ]) ->assertForbidden(); } public function test_sidebar_and_dashboard_exclude_modules_with_no_access(): void { [$pharmacistUser, $pharmacistMember] = $this->makeStaff('pharmacist', 'rx-nav'); [, $epMember] = $this->makeStaff('emergency_physician', 'ep-nav'); $enabledForPharmacist = collect($this->modules->enabledModulesForMember($this->organization, $pharmacistMember)) ->pluck('key') ->all(); $enabledForEp = collect($this->modules->enabledModulesForMember($this->organization, $epMember)) ->pluck('key') ->all(); // 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); $this->assertNotContains('dentistry', $enabledForEp); $this->assertTrue($this->modules->shouldShowSpecialtyNav($this->organization, $epMember)); $this->actingAs($pharmacistUser) ->get(route('care.dashboard')) ->assertOk() ->assertSee('>Specialty

', false); } public function test_manage_role_still_sees_mutate_actions_on_restricted_module(): void { [$doctor, $member] = $this->makeStaff('doctor', 'cardio-ui'); $dept = Department::query()->where('type', 'cardiology')->firstOrFail(); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'department_id' => $dept->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr Cardio UI', 'specialty' => 'Cardiology', 'is_active' => true, ]); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-CARD-UI', 'first_name' => 'Abena', 'last_name' => 'Heart', 'gender' => 'female', 'date_of_birth' => '1985-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_WAITING, 'scheduled_at' => now(), 'checked_in_at' => now(), ]); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology')); $this->actingAs($doctor) ->get(route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit])) ->assertOk() ->assertDontSee('view + refer') ->assertSee('Start'); } public function test_admin_keeps_full_manage_access(): void { $admin = Member::query()->where('user_ref', $this->owner->public_id)->firstOrFail(); $this->assertTrue($this->modules->memberCanManage($this->organization, $admin, 'cardiology')); $this->assertTrue($this->modules->memberCanManage($this->organization, $admin, 'emergency')); } /** * @return array{0: \App\Models\User, 1: Member, 2: \App\Models\Visit} */ protected function specialtyVisitForRole(string $role, string $module, string $suffix): array { if ($module === 'dentistry') { return $this->dentistryVisitForRole($role, $suffix); } if ($module === 'emergency') { return $this->emergencyVisitForRole($role, $suffix); } [$user, $member] = $this->makeStaff($role, $suffix); $definition = $this->modules->definition($module); $deptType = (string) ($definition['department_type'] ?? $module); $dept = Department::query()->where('type', $deptType)->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-'.strtoupper(substr($module, 0, 3)).'-'.$suffix, 'first_name' => 'Floor', 'last_name' => ucfirst($module), 'gender' => 'female', 'date_of_birth' => '1990-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), 'specialty_stage' => 'waiting', ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_WAITING, 'scheduled_at' => now(), 'checked_in_at' => now(), 'waiting_at' => now(), ]); return [$user, $member, $visit]; } /** * @return array{0: \App\Models\User, 1: Member, 2: \App\Models\Visit} */ protected function dentistryVisitForRole(string $role, string $suffix): array { [$user, $member] = $this->makeStaff($role, $suffix); if (in_array($role, ['doctor', 'dentist'], true)) { Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $user->public_id, 'name' => 'Dr '.$suffix, 'specialty' => 'Dentistry', 'is_active' => true, ]); } $dept = Department::query()->where('type', 'dental')->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-DEN-'.$suffix, 'first_name' => 'Efua', 'last_name' => 'Boateng', 'gender' => 'female', 'date_of_birth' => '1992-06-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), 'specialty_stage' => 'waiting', ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_WAITING, 'scheduled_at' => now(), 'checked_in_at' => now(), 'waiting_at' => now(), ]); return [$user, $member, $visit]; } /** * @return array{0: \App\Models\User, 1: Member, 2: \App\Models\Visit} */ protected function emergencyVisitForRole(string $role, string $suffix): array { [$user, $member] = $this->makeStaff($role, $suffix); $dept = Department::query()->where('type', 'emergency')->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-ER-'.$suffix, 'first_name' => 'Kojo', 'last_name' => 'Floor', 'gender' => 'male', 'date_of_birth' => '1988-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), 'specialty_stage' => 'arrival', ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_WAITING, 'scheduled_at' => now(), 'checked_in_at' => now(), 'waiting_at' => now(), ]); return [$user, $member, $visit]; } /** * Roles that can manage a specialty module but lack consultations.manage must not see * Start / stage / chart-mutate CTAs. Gating is ability-based — not nurse-hardcoded. * * @return list */ public static function floorRolesWithoutConsultManage(): array { return [ ['ed_nurse', 'emergency', 'floor-edn-er'], ['theatre_nurse', 'surgery', 'floor-tn-sur'], ['lab_technician', 'pathology', 'floor-lab-path'], ['radiographer', 'radiology', 'floor-rad-tech'], ]; } #[DataProvider('floorRolesWithoutConsultManage')] public function test_floor_roles_without_consult_do_not_see_mutate_ctas( string $role, string $module, string $suffix, ): void { [$user, $member, $visit] = $this->specialtyVisitForRole($role, $module, $suffix); $permissions = app(\App\Services\Care\CarePermissions::class); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, $module)); $this->assertFalse($permissions->can($member, 'consultations.manage')); // Prove gating is not nurse-only: same CTA rules for every floor role without consult. $this->assertNotSame('hospital_admin', $member->role); $html = $this->actingAs($user) ->get(route('care.specialty.workspace', ['module' => $module, 'visit' => $visit, 'tab' => 'overview'])) ->assertOk() ->assertSee('consultation access') ->assertDontSee('Seat at chair') ->assertDontSee('Start triage') ->assertSee('Patient chart') ->getContent(); // Nurses keep specialty workspaces but lose the patient-flow board link. if ($permissions->canAccessPatientQueue($member)) { $this->assertStringContainsString('Back to queue', $html); $this->assertStringNotContainsString('Specialty home', $html); } else { $this->assertStringNotContainsString('Back to queue', $html); $this->assertStringContainsString('Specialty home', $html); } $this->assertStringNotContainsString( route('care.specialty.consultation.start', ['module' => $module, 'visit' => $visit], absolute: false), $html, ); if ($module === 'dentistry') { $this->assertStringNotContainsString( route('care.specialty.dentistry.stage', $visit, absolute: false), $html, ); $this->assertStringContainsString('Waiting', $html); } elseif ($module === 'emergency') { $this->assertStringNotContainsString( route('care.specialty.emergency.stage', $visit, absolute: false), $html, ); } if ($permissions->can($member, 'appointments.manage')) { $this->assertStringContainsString('Register / book', $html); } else { $this->assertStringNotContainsString('Register / book', $html); } } public function test_nurse_dentistry_mutate_posts_are_forbidden(): void { [$nurseUser, , $visit] = $this->dentistryVisitForRole('nurse', 'den-nurse-post'); $this->actingAs($nurseUser) ->post(route('care.specialty.dentistry.stage', $visit), ['stage' => 'chair']) ->assertForbidden(); $this->actingAs($nurseUser) ->post(route('care.specialty.consultation.start', ['module' => 'dentistry', 'visit' => $visit])) ->assertForbidden(); $this->actingAs($nurseUser) ->post(route('care.specialty.dentistry.tooth', $visit), [ 'tooth_code' => '16', 'status' => 'present', ]) ->assertForbidden(); } public function test_receptionist_dentistry_mutate_posts_are_forbidden(): void { [$user, , $visit] = $this->dentistryVisitForRole('receptionist', 'den-recv-post'); $this->actingAs($user) ->post(route('care.specialty.dentistry.stage', $visit), ['stage' => 'chair']) ->assertForbidden(); $this->actingAs($user) ->post(route('care.specialty.consultation.start', ['module' => 'dentistry', 'visit' => $visit])) ->assertForbidden(); } public function test_lab_technician_and_pharmacist_emergency_mutate_posts_are_forbidden(): void { foreach (['lab_technician' => 'lab-er-post', 'pharmacist' => 'rx-er-post', 'nurse' => 'nurse-er-post'] as $role => $suffix) { [$user, , $visit] = $this->emergencyVisitForRole($role, $suffix); $this->actingAs($user) ->post(route('care.specialty.emergency.stage', $visit), ['stage' => 'treatment']) ->assertForbidden(); $this->actingAs($user) ->post(route('care.specialty.consultation.start', ['module' => 'emergency', 'visit' => $visit])) ->assertForbidden(); } } public function test_ed_nurse_cannot_discharge_emergency_visit(): void { [$user, $member, $visit] = $this->emergencyVisitForRole('ed_nurse', 'edn-discharge'); $permissions = app(\App\Services\Care\CarePermissions::class); $this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'emergency')); $this->assertFalse($permissions->can($member, 'emergency.discharge')); $this->actingAs($user) ->post(route('care.specialty.emergency.disposition', $visit), [ 'payload' => [ 'disposition' => 'Discharged home', 'destination' => 'Home', 'medications' => 'None', ], ]) ->assertForbidden(); } public function test_rbac_matrix_smoke_key_roles(): void { $permissions = app(\App\Services\Care\CarePermissions::class); [, $ep] = $this->makeStaff('emergency_physician', 'matrix-ep'); [, $dentist] = $this->makeStaff('dentist', 'matrix-den'); [, $cashier] = $this->makeStaff('cashier', 'matrix-cash'); [, $lab] = $this->makeStaff('lab_technician', 'matrix-lab'); [, $labMgr] = $this->makeStaff('lab_manager', 'matrix-labm'); [, $billing] = $this->makeStaff('billing_officer', 'matrix-bill'); $this->assertTrue($this->modules->memberCanManage($this->organization, $ep, 'emergency')); $this->assertTrue($this->modules->memberCanManage($this->organization, $ep, 'radiology')); $this->assertFalse($this->modules->memberCanManage($this->organization, $ep, 'dentistry')); $this->assertTrue($this->modules->memberCanManage($this->organization, $dentist, 'dentistry')); $this->assertFalse($this->modules->memberCanManage($this->organization, $dentist, 'emergency')); $this->assertFalse($this->modules->shouldShowSpecialtyNav($this->organization, $dentist)); $this->assertTrue($permissions->can($cashier, 'payments.manage')); $this->assertFalse($permissions->can($cashier, 'bills.manage')); $this->assertFalse($permissions->can($cashier, 'consultations.manage')); $this->assertSame([], $this->modules->enabledModulesForMember($this->organization, $cashier)); $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')); } public function test_doctor_on_dentistry_sees_and_can_advance_stage(): void { [$doctorUser, $doctorMember, $visit] = $this->dentistryVisitForRole('dentist', 'den-doc'); $this->assertTrue($this->modules->memberCanManage($this->organization, $doctorMember, 'dentistry')); $this->assertTrue(app(\App\Services\Care\CarePermissions::class)->can($doctorMember, 'consultations.manage')); $this->actingAs($doctorUser) ->get(route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'overview'])) ->assertOk() ->assertSee('Seat at chair') ->assertSee('Start') ->assertDontSee('consultation access'); $this->actingAs($doctorUser) ->post(route('care.specialty.dentistry.stage', $visit), ['stage' => 'chair']) ->assertRedirect(); $this->assertSame('chair', $visit->fresh()->specialty_stage); } public function test_gp_with_consult_ability_but_no_module_manage_hides_mutate_ctas(): void { [$doctor, $member] = $this->makeStaff('general_physician', 'gp-no-manage'); Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dr GP No Manage', 'specialty' => 'General Practice', 'is_active' => true, ]); $permissions = app(\App\Services\Care\CarePermissions::class); $this->assertTrue($permissions->can($member, 'consultations.manage')); $this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'psychiatry')); $this->assertSame('refer', $this->modules->memberAccessLevel($this->organization, $member, 'psychiatry')); $dept = Department::query()->where('type', 'psychiatry')->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-GP-NM', 'first_name' => 'Yaw', 'last_name' => 'ReferOnly', 'gender' => 'male', 'date_of_birth' => '1975-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_WAITING, 'scheduled_at' => now(), 'checked_in_at' => now(), 'waiting_at' => now(), ]); $html = $this->actingAs($doctor) ->get(route('care.specialty.workspace', ['module' => 'psychiatry', 'visit' => $visit, 'tab' => 'overview'])) ->assertOk() ->assertSee('view + refer') ->assertDontSee('consultation access') ->assertDontSee('Seat at chair') ->assertDontSee('Start triage') ->getContent(); $this->assertStringNotContainsString( route('care.specialty.consultation.start', ['module' => 'psychiatry', 'visit' => $visit], absolute: false), $html, ); $this->assertStringNotContainsString('>Start', $html); } public function test_admin_on_dentistry_sees_mutate_actions(): void { $dept = Department::query()->where('type', 'dental')->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-DEN-ADMIN', 'first_name' => 'Admin', 'last_name' => 'Patient', 'gender' => 'male', 'date_of_birth' => '1980-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), 'specialty_stage' => 'waiting', ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_WAITING, 'scheduled_at' => now(), 'checked_in_at' => now(), 'waiting_at' => now(), ]); $this->actingAs($this->owner) ->get(route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'overview'])) ->assertOk() ->assertSee('Seat at chair') ->assertSee('Start'); } public function test_nurse_can_save_emergency_vitals_with_vitals_manage(): void { [$nurseUser, $nurseMember] = $this->makeStaff('ed_nurse', 'er-vitals'); $this->assertTrue(app(\App\Services\Care\CarePermissions::class)->can($nurseMember, 'vitals.manage')); $this->assertFalse(app(\App\Services\Care\CarePermissions::class)->can($nurseMember, 'consultations.manage')); $this->assertTrue($this->modules->memberCanManage($this->organization, $nurseMember, 'emergency')); $dept = Department::query()->where('type', 'emergency')->firstOrFail(); $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-ER-VITALS', 'first_name' => 'Kojo', 'last_name' => 'Mensah', 'gender' => 'male', 'date_of_birth' => '1988-01-01', ]); $visit = \App\Models\Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'department_id' => $dept->id, 'visit_id' => $visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_IN_CONSULTATION, 'scheduled_at' => now(), 'checked_in_at' => now(), 'started_at' => now(), ]); $this->actingAs($nurseUser) ->get(route('care.specialty.workspace', ['module' => 'emergency', 'visit' => $visit, 'tab' => 'vitals'])) ->assertOk() ->assertSee('Save vitals') ->assertDontSee('Seat at chair') ->assertDontSee('Start triage'); $this->actingAs($nurseUser) ->post(route('care.specialty.emergency.vitals', $visit), [ 'bp_systolic' => 120, 'bp_diastolic' => 80, 'pulse' => 72, ]) ->assertRedirect(); $this->actingAs($nurseUser) ->post(route('care.specialty.emergency.stage', $visit), ['stage' => 'treatment']) ->assertForbidden(); } public function test_doctor_with_prescriptions_manage_sees_prescribe_on_specialty_workspace(): void { [$doctorUser, $doctorMember, $visit] = $this->dentistryVisitForRole('dentist', 'den-rx'); $appointment = $visit->appointment()->firstOrFail(); $appointment->update([ 'status' => Appointment::STATUS_IN_CONSULTATION, 'started_at' => now(), 'queue_ticket_status' => 'serving', ]); $consultation = Consultation::create([ 'owner_ref' => $this->owner->public_id, 'visit_id' => $visit->id, 'appointment_id' => $appointment->id, 'patient_id' => $visit->patient_id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now(), ]); $permissions = app(\App\Services\Care\CarePermissions::class); $this->assertTrue($permissions->can($doctorMember, 'prescriptions.manage')); $this->assertTrue($this->modules->memberCanManage($this->organization, $doctorMember, 'dentistry')); $html = $this->actingAs($doctorUser) ->get(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $visit, 'tab' => 'overview', ])) ->assertOk() ->assertSee('Prescribe') ->assertSee('New prescription') ->getContent(); $this->assertStringContainsString( route('care.prescriptions.store', $consultation, absolute: false), $html, ); $this->assertStringContainsString('name="return_to" value="specialty"', $html); $this->assertStringContainsString('name="specialty_module" value="dentistry"', $html); } public function test_nurse_without_prescriptions_manage_does_not_see_prescribe_cta(): void { [$nurseUser, $nurseMember, $visit] = $this->emergencyVisitForRole('ed_nurse', 'er-no-rx'); $appointment = $visit->appointment()->firstOrFail(); $appointment->update([ 'status' => Appointment::STATUS_IN_CONSULTATION, 'started_at' => now(), 'queue_ticket_status' => 'serving', ]); Consultation::create([ 'owner_ref' => $this->owner->public_id, 'visit_id' => $visit->id, 'appointment_id' => $appointment->id, 'patient_id' => $visit->patient_id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now(), ]); $permissions = app(\App\Services\Care\CarePermissions::class); $this->assertTrue($this->modules->memberCanManage($this->organization, $nurseMember, 'emergency')); $this->assertFalse($permissions->can($nurseMember, 'prescriptions.manage')); $this->actingAs($nurseUser) ->get(route('care.specialty.workspace', [ 'module' => 'emergency', 'visit' => $visit, 'tab' => 'overview', ])) ->assertOk() ->assertDontSee('Prescribe') ->assertDontSee('New prescription'); } public function test_specialty_prescribe_store_returns_to_workspace(): void { [$doctorUser, , $visit] = $this->dentistryVisitForRole('dentist', 'den-rx-store'); $appointment = $visit->appointment()->firstOrFail(); $appointment->update([ 'status' => Appointment::STATUS_IN_CONSULTATION, 'started_at' => now(), 'queue_ticket_status' => 'serving', ]); $consultation = Consultation::create([ 'owner_ref' => $this->owner->public_id, 'visit_id' => $visit->id, 'appointment_id' => $appointment->id, 'patient_id' => $visit->patient_id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now(), ]); $this->actingAs($doctorUser) ->post(route('care.prescriptions.store', $consultation), [ 'activate' => true, 'return_to' => 'specialty', 'specialty_module' => 'dentistry', 'specialty_visit_id' => $visit->id, 'from_consultation' => $consultation->uuid, 'items' => [ [ 'name' => 'Amoxicillin 500mg', 'dosage' => '1 capsule', 'frequency' => 'TDS', 'duration' => '5 days', 'route' => 'oral', ], ], ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $visit, ])); $this->assertDatabaseHas('care_prescriptions', [ 'consultation_id' => $consultation->id, 'visit_id' => $visit->id, 'patient_id' => $visit->patient_id, ]); } }