Implement Care RBAC role→permission→app matrix.
Deploy Ladill Care / deploy (push) Successful in 57s
Deploy Ladill Care / deploy (push) Successful in 57s
Replace broad doctor/nurse specialty access with granular roles and primary apps, permission inheritance for lab/BB managers, and cannot-rules for discharge, lab approve, and cashier vs billing officer. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,8 @@ class CareBillTest extends TestCase
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected User $cashierUser;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected Visit $visit;
|
||||
@@ -31,8 +33,14 @@ class CareBillTest extends TestCase
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'test-user-001',
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'name' => 'Billing Officer',
|
||||
'email' => 'billing@example.com',
|
||||
]);
|
||||
|
||||
$this->cashierUser = User::create([
|
||||
'public_id' => 'test-cashier-001',
|
||||
'name' => 'Cashier',
|
||||
'email' => 'cashier@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
@@ -47,6 +55,13 @@ class CareBillTest extends TestCase
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'billing_officer',
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->cashierUser->public_id,
|
||||
'role' => 'cashier',
|
||||
]);
|
||||
|
||||
@@ -88,7 +103,7 @@ class CareBillTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_cashier_can_generate_bill_from_visit(): void
|
||||
public function test_billing_officer_can_generate_bill_from_visit(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.bills.generate', $this->visit))
|
||||
@@ -101,7 +116,24 @@ class CareBillTest extends TestCase
|
||||
$this->assertDatabaseHas('care_audit_logs', ['action' => 'bill.created']);
|
||||
}
|
||||
|
||||
public function test_cashier_can_add_line_item_and_record_payments(): void
|
||||
public function test_cashier_cannot_edit_invoice_lines(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.bills.generate', $this->visit));
|
||||
|
||||
$bill = Bill::firstOrFail();
|
||||
|
||||
$this->actingAs($this->cashierUser)
|
||||
->post(route('care.bills.line-items.store', $bill), [
|
||||
'type' => 'misc',
|
||||
'description' => 'Dressing supplies',
|
||||
'quantity' => 2,
|
||||
'unit_price_minor' => 1000,
|
||||
])
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_billing_officer_can_add_line_item_and_cashier_records_payments(): void
|
||||
{
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.bills.generate', $this->visit));
|
||||
@@ -123,7 +155,15 @@ class CareBillTest extends TestCase
|
||||
|
||||
$this->assertSame($expectedTotal, $bill->total_minor);
|
||||
|
||||
// Billing officer cannot receive payments.
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.bills.payments.store', $bill), [
|
||||
'amount' => 30.00,
|
||||
'method' => 'cash',
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($this->cashierUser)
|
||||
->post(route('care.bills.payments.store', $bill), [
|
||||
'amount' => 30.00,
|
||||
'method' => 'cash',
|
||||
@@ -134,7 +174,7 @@ class CareBillTest extends TestCase
|
||||
$this->assertSame(Bill::STATUS_PARTIAL, $bill->status);
|
||||
$this->assertSame(3000, $bill->amount_paid_minor);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
$this->actingAs($this->cashierUser)
|
||||
->post(route('care.bills.payments.store', $bill), [
|
||||
'amount' => $bill->balance_minor / 100,
|
||||
'method' => 'momo',
|
||||
@@ -153,7 +193,7 @@ class CareBillTest extends TestCase
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.bills.generate', $this->visit));
|
||||
|
||||
$this->actingAs($this->user)
|
||||
$this->actingAs($this->cashierUser)
|
||||
->get(route('care.bills.index'))
|
||||
->assertOk()
|
||||
->assertSee('INV-');
|
||||
@@ -181,14 +221,14 @@ class CareBillTest extends TestCase
|
||||
'balance_minor' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
$this->actingAs($this->cashierUser)
|
||||
->get(route('care.bills.index'))
|
||||
->assertOk()
|
||||
->assertSee($open->invoice_number)
|
||||
->assertSee('Pay')
|
||||
->assertDontSee($paid->invoice_number, false);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
$this->actingAs($this->cashierUser)
|
||||
->get(route('care.bills.index', ['status' => '']))
|
||||
->assertOk()
|
||||
->assertSee($paid->invoice_number);
|
||||
@@ -201,7 +241,7 @@ class CareBillTest extends TestCase
|
||||
|
||||
$bill = Bill::firstOrFail();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
$this->actingAs($this->cashierUser)
|
||||
->get(route('care.bills.index'))
|
||||
->assertOk()
|
||||
->assertSee($bill->invoice_number)
|
||||
@@ -211,7 +251,7 @@ class CareBillTest extends TestCase
|
||||
|
||||
$balanceMajor = number_format($bill->balance_minor / 100, 2, '.', '');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
$this->actingAs($this->cashierUser)
|
||||
->get(route('care.bills.show', $bill))
|
||||
->assertOk()
|
||||
->assertSee('Record payment')
|
||||
@@ -228,9 +268,9 @@ class CareBillTest extends TestCase
|
||||
$settings['queue_integration_enabled'] = true;
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.bills.call-next'), ['branch_id' => Branch::firstOrFail()->id])
|
||||
$this->actingAs($this->cashierUser)
|
||||
->post(route('care.bills.call-next'))
|
||||
->assertRedirect()
|
||||
->assertSessionHas('info', 'No patients waiting at the billing booth. Open an unpaid invoice above to record a walk-up payment.');
|
||||
->assertSessionHas('info');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,13 @@ class CareLabTest extends TestCase
|
||||
$this->assertSame(InvestigationRequest::STATUS_AWAITING_REVIEW, $request->status);
|
||||
$this->assertTrue($request->result->is_abnormal);
|
||||
|
||||
// Lab techs enter results; approval requires pathology.result.approve (manager+).
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.lab.requests.approve', $request))
|
||||
->assertForbidden();
|
||||
|
||||
Member::where('user_ref', $this->user->public_id)->update(['role' => 'lab_manager']);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.lab.requests.approve', $request))
|
||||
->assertRedirect();
|
||||
|
||||
@@ -167,7 +167,7 @@ class CarePatientQueueAccessTest extends TestCase
|
||||
|
||||
public function test_nurse_specialty_show_renders_list_not_auto_open_visit(): void
|
||||
{
|
||||
[$nurseUser] = $this->makeStaff('nurse', 'nurse-spec');
|
||||
[$nurseUser] = $this->makeStaff('ed_nurse', 'nurse-spec');
|
||||
|
||||
$modules = app(SpecialtyModuleService::class);
|
||||
$modules->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
@@ -242,7 +242,7 @@ class CarePatientQueueAccessTest extends TestCase
|
||||
|
||||
public function test_nurse_can_open_emergency_visit_from_specialty_list(): void
|
||||
{
|
||||
[$nurseUser] = $this->makeStaff('nurse', 'nurse-er-open');
|
||||
[$nurseUser] = $this->makeStaff('ed_nurse', 'nurse-er-open');
|
||||
|
||||
$modules = app(SpecialtyModuleService::class);
|
||||
$modules->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
@@ -315,7 +315,7 @@ class CarePatientQueueAccessTest extends TestCase
|
||||
|
||||
public function test_nurse_specialty_show_redirects_to_workspace_not_queue(): void
|
||||
{
|
||||
[$nurseUser] = $this->makeStaff('nurse', 'nurse-spec-er');
|
||||
[$nurseUser] = $this->makeStaff('ed_nurse', 'nurse-spec-er');
|
||||
|
||||
app(SpecialtyModuleService::class)
|
||||
->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
|
||||
@@ -69,7 +69,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
$this->modules = app(SpecialtyModuleService::class);
|
||||
$this->modules->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
foreach (['cardiology', 'infusion', 'pathology', 'dentistry'] as $key) {
|
||||
foreach (['cardiology', 'infusion', 'pathology', 'dentistry', 'psychiatry', 'surgery', 'radiology'] as $key) {
|
||||
$this->modules->activate($this->organization->fresh(), $this->owner->public_id, $key);
|
||||
}
|
||||
$this->organization->refresh();
|
||||
@@ -96,20 +96,26 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
public function test_nurse_and_pharmacist_manage_general_modules(): void
|
||||
{
|
||||
[, $nurse] = $this->makeStaff('nurse', 'nurse1');
|
||||
[, $edNurse] = $this->makeStaff('ed_nurse', 'edn1');
|
||||
[, $pharmacist] = $this->makeStaff('pharmacist', 'rx1');
|
||||
[, $lab] = $this->makeStaff('lab_technician', 'lab1');
|
||||
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $pharmacist, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $pharmacist, 'infusion'));
|
||||
// Legacy nurse is limited — no specialty modules.
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $nurse, 'emergency'));
|
||||
// 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.
|
||||
$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->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'blood_bank'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $lab, 'blood_bank'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'cardiology'));
|
||||
}
|
||||
|
||||
public function test_gp_doctor_manages_general_but_only_views_restricted(): void
|
||||
{
|
||||
[$doctor, $member] = $this->makeStaff('doctor', 'gp1');
|
||||
[$doctor, $member] = $this->makeStaff('general_physician', 'gp1');
|
||||
Practitioner::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
@@ -123,11 +129,15 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
$this->assertFalse($this->modules->isDeskSpecialist($this->organization, $member));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'dentistry'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'cardiology'));
|
||||
$this->assertTrue($this->modules->memberCanView($this->organization, $member, 'cardiology'));
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $member, 'cardiology'));
|
||||
$this->assertSame('refer', $this->modules->memberAccessLevel($this->organization, $member, 'cardiology'));
|
||||
// Dentistry is not a GP primary app.
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'dentistry'));
|
||||
// Cardiology is a GP primary app (manage).
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology'));
|
||||
// Psychiatry is outside GP apps — refer/view via legacy config for GP.
|
||||
$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_cardiologist_manages_restricted_cardiology(): void
|
||||
@@ -226,7 +236,8 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('>Specialty</p>', false)
|
||||
->assertSee('Emergency')
|
||||
->assertSee('Dentistry');
|
||||
->assertSee('Cardiology')
|
||||
->assertDontSee('Dentistry');
|
||||
}
|
||||
|
||||
public function test_dentistry_specialty_does_not_match_ent_keyword(): void
|
||||
@@ -290,7 +301,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_gp_cannot_save_clinical_on_restricted_module(): void
|
||||
{
|
||||
[$doctor, $member] = $this->makeStaff('doctor', 'gp3');
|
||||
[$doctor, $member] = $this->makeStaff('general_physician', 'gp3');
|
||||
Practitioner::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
@@ -302,14 +313,14 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$dept = Department::query()->where('type', 'cardiology')->firstOrFail();
|
||||
$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-CARD-1',
|
||||
'patient_number' => 'P-PSY-1',
|
||||
'first_name' => 'Kofi',
|
||||
'last_name' => 'Heart',
|
||||
'last_name' => 'Mind',
|
||||
'gender' => 'male',
|
||||
'date_of_birth' => '1980-01-01',
|
||||
]);
|
||||
@@ -336,7 +347,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit, 'tab' => 'exam']))
|
||||
->get(route('care.specialty.workspace', ['module' => 'psychiatry', 'visit' => $visit, 'tab' => 'exam']))
|
||||
->assertOk()
|
||||
->assertSee('view + refer')
|
||||
->assertDontSee('Save record')
|
||||
@@ -345,12 +356,12 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
->assertDontSee('Upload document')
|
||||
->assertSee('Refer to specialty queue')
|
||||
->assertDontSee(
|
||||
'action="'.route('care.specialty.consultation.start', ['module' => 'cardiology', 'visit' => $visit], false).'"',
|
||||
'action="'.route('care.specialty.consultation.start', ['module' => 'psychiatry', 'visit' => $visit], false).'"',
|
||||
false,
|
||||
);
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->post(route('care.specialty.clinical.save', ['module' => 'cardiology', 'visit' => $visit]), [
|
||||
->post(route('care.specialty.clinical.save', ['module' => 'psychiatry', 'visit' => $visit]), [
|
||||
'tab' => 'exam',
|
||||
'payload' => [
|
||||
'chief_complaint' => 'Should not save',
|
||||
@@ -362,23 +373,28 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
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');
|
||||
|
||||
$enabledForMember = collect($this->modules->enabledModulesForMember($this->organization, $pharmacistMember))
|
||||
$enabledForPharmacist = collect($this->modules->enabledModulesForMember($this->organization, $pharmacistMember))
|
||||
->pluck('key')
|
||||
->all();
|
||||
$enabledForEp = collect($this->modules->enabledModulesForMember($this->organization, $epMember))
|
||||
->pluck('key')
|
||||
->all();
|
||||
|
||||
$this->assertContains('emergency', $enabledForMember);
|
||||
$this->assertContains('infusion', $enabledForMember);
|
||||
$this->assertNotContains('cardiology', $enabledForMember);
|
||||
$this->assertNotContains('dentistry', $enabledForMember);
|
||||
$this->assertTrue($this->modules->shouldShowSpecialtyNav($this->organization, $pharmacistMember));
|
||||
// Pharmacist: pharmacy core only — no specialty modules.
|
||||
$this->assertSame([], $enabledForPharmacist);
|
||||
$this->assertFalse($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</p>', false)
|
||||
->assertSee('Emergency')
|
||||
->assertDontSee('Cardiology');
|
||||
->assertDontSee('>Specialty</p>', false);
|
||||
}
|
||||
|
||||
public function test_manage_role_still_sees_mutate_actions_on_restricted_module(): void
|
||||
@@ -444,13 +460,65 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
$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 ($role === 'doctor') {
|
||||
if (in_array($role, ['doctor', 'dentist'], true)) {
|
||||
Practitioner::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
@@ -553,10 +621,10 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
public static function floorRolesWithoutConsultManage(): array
|
||||
{
|
||||
return [
|
||||
['nurse', 'dentistry', 'floor-nurse-den'],
|
||||
['receptionist', 'dentistry', 'floor-recv-den'],
|
||||
['lab_technician', 'emergency', 'floor-lab-er'],
|
||||
['pharmacist', 'emergency', 'floor-rx-er'],
|
||||
['ed_nurse', 'emergency', 'floor-edn-er'],
|
||||
['theatre_nurse', 'surgery', 'floor-tn-sur'],
|
||||
['lab_technician', 'pathology', 'floor-lab-path'],
|
||||
['radiographer', 'radiology', 'floor-rad-tech'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -566,9 +634,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
string $module,
|
||||
string $suffix,
|
||||
): void {
|
||||
[$user, $member, $visit] = $module === 'dentistry'
|
||||
? $this->dentistryVisitForRole($role, $suffix)
|
||||
: $this->emergencyVisitForRole($role, $suffix);
|
||||
[$user, $member, $visit] = $this->specialtyVisitForRole($role, $module, $suffix);
|
||||
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, $module));
|
||||
@@ -605,7 +671,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
$html,
|
||||
);
|
||||
$this->assertStringContainsString('Waiting', $html);
|
||||
} else {
|
||||
} elseif ($module === 'emergency') {
|
||||
$this->assertStringNotContainsString(
|
||||
route('care.specialty.emergency.stage', $visit, absolute: false),
|
||||
$html,
|
||||
@@ -654,7 +720,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_lab_technician_and_pharmacist_emergency_mutate_posts_are_forbidden(): void
|
||||
{
|
||||
foreach (['lab_technician' => 'lab-er-post', 'pharmacist' => 'rx-er-post'] as $role => $suffix) {
|
||||
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)
|
||||
@@ -664,20 +730,64 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
$this->actingAs($user)
|
||||
->post(route('care.specialty.consultation.start', ['module' => 'emergency', 'visit' => $visit]))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('care.specialty.emergency.vitals', $visit), [
|
||||
'bp_systolic' => 120,
|
||||
'bp_diastolic' => 80,
|
||||
'pulse' => 72,
|
||||
])
|
||||
->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($permissions->can($labMgr, 'pathology.result.approve'));
|
||||
$this->assertTrue($permissions->can($labMgr, 'lab.catalog.manage'));
|
||||
|
||||
$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('doctor', 'den-doc');
|
||||
[$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'));
|
||||
@@ -698,7 +808,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_gp_with_consult_ability_but_no_module_manage_hides_mutate_ctas(): void
|
||||
{
|
||||
[$doctor, $member] = $this->makeStaff('doctor', 'gp-no-manage');
|
||||
[$doctor, $member] = $this->makeStaff('general_physician', 'gp-no-manage');
|
||||
Practitioner::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
@@ -712,10 +822,10 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
$this->assertTrue($permissions->can($member, 'consultations.manage'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'cardiology'));
|
||||
$this->assertSame('refer', $this->modules->memberAccessLevel($this->organization, $member, 'cardiology'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'psychiatry'));
|
||||
$this->assertSame('refer', $this->modules->memberAccessLevel($this->organization, $member, 'psychiatry'));
|
||||
|
||||
$dept = Department::query()->where('type', 'cardiology')->firstOrFail();
|
||||
$dept = Department::query()->where('type', 'psychiatry')->firstOrFail();
|
||||
$patient = Patient::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
@@ -749,7 +859,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
]);
|
||||
|
||||
$html = $this->actingAs($doctor)
|
||||
->get(route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit, 'tab' => 'overview']))
|
||||
->get(route('care.specialty.workspace', ['module' => 'psychiatry', 'visit' => $visit, 'tab' => 'overview']))
|
||||
->assertOk()
|
||||
->assertSee('view + refer')
|
||||
->assertDontSee('consultation access')
|
||||
@@ -758,7 +868,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
->getContent();
|
||||
|
||||
$this->assertStringNotContainsString(
|
||||
route('care.specialty.consultation.start', ['module' => 'cardiology', 'visit' => $visit], absolute: false),
|
||||
route('care.specialty.consultation.start', ['module' => 'psychiatry', 'visit' => $visit], absolute: false),
|
||||
$html,
|
||||
);
|
||||
$this->assertStringNotContainsString('>Start</button>', $html);
|
||||
@@ -809,9 +919,10 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_nurse_can_save_emergency_vitals_with_vitals_manage(): void
|
||||
{
|
||||
[$nurseUser, $nurseMember] = $this->makeStaff('nurse', 'er-vitals');
|
||||
[$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([
|
||||
@@ -868,7 +979,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_doctor_with_prescriptions_manage_sees_prescribe_on_specialty_workspace(): void
|
||||
{
|
||||
[$doctorUser, $doctorMember, $visit] = $this->dentistryVisitForRole('doctor', 'den-rx');
|
||||
[$doctorUser, $doctorMember, $visit] = $this->dentistryVisitForRole('dentist', 'den-rx');
|
||||
$appointment = $visit->appointment()->firstOrFail();
|
||||
$appointment->update([
|
||||
'status' => Appointment::STATUS_IN_CONSULTATION,
|
||||
@@ -909,7 +1020,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_nurse_without_prescriptions_manage_does_not_see_prescribe_cta(): void
|
||||
{
|
||||
[$nurseUser, $nurseMember, $visit] = $this->dentistryVisitForRole('nurse', 'den-no-rx');
|
||||
[$nurseUser, $nurseMember, $visit] = $this->emergencyVisitForRole('ed_nurse', 'er-no-rx');
|
||||
$appointment = $visit->appointment()->firstOrFail();
|
||||
$appointment->update([
|
||||
'status' => Appointment::STATUS_IN_CONSULTATION,
|
||||
@@ -926,24 +1037,23 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
]);
|
||||
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $nurseMember, 'dentistry'));
|
||||
$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' => 'dentistry',
|
||||
'module' => 'emergency',
|
||||
'visit' => $visit,
|
||||
'tab' => 'overview',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertDontSee('Prescribe')
|
||||
->assertDontSee('New prescription')
|
||||
->assertDontSee(route('care.prescriptions.store', Consultation::query()->where('visit_id', $visit->id)->first(), absolute: false));
|
||||
->assertDontSee('New prescription');
|
||||
}
|
||||
|
||||
public function test_specialty_prescribe_store_returns_to_workspace(): void
|
||||
{
|
||||
[$doctorUser, , $visit] = $this->dentistryVisitForRole('doctor', 'den-rx-store');
|
||||
[$doctorUser, , $visit] = $this->dentistryVisitForRole('dentist', 'den-rx-store');
|
||||
$appointment = $visit->appointment()->firstOrFail();
|
||||
$appointment->update([
|
||||
'status' => Appointment::STATUS_IN_CONSULTATION,
|
||||
|
||||
Reference in New Issue
Block a user