withoutMiddleware(EnsurePlatformSession::class); $this->user = User::create([ 'public_id' => 'test-user-001', 'name' => 'Test User', 'email' => 'test@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->user->public_id, 'name' => 'Test Clinic', 'slug' => 'test-clinic', 'timezone' => 'UTC', 'settings' => ['onboarded' => true, 'facility_type' => 'clinic'], ]); Member::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->user->public_id, 'role' => 'hospital_admin', ]); $branch = Branch::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'name' => 'Main Branch', 'is_active' => true, ]); Department::create([ 'owner_ref' => $this->user->public_id, 'branch_id' => $branch->id, 'name' => 'General Outpatient', 'type' => 'outpatient', 'is_active' => true, ]); } public function test_guest_is_redirected_to_sso(): void { $this->get('/dashboard')->assertRedirect(); } public function test_unonboarded_user_is_redirected_to_onboarding(): void { Organization::query()->delete(); Member::query()->delete(); $this->actingAs($this->user) ->get(route('care.dashboard')) ->assertRedirect(route('care.onboarding.show')); } public function test_dashboard_loads_for_authenticated_user(): void { $this->actingAs($this->user) ->get(route('care.dashboard')) ->assertOk() ->assertSee('Test Clinic') ->assertSee('Revenue today') ->assertSee('Open bills') ->assertSee('Payments today') ->assertSee('Recently paid bills'); } public function test_doctor_dashboard_hides_finance_metrics(): void { Member::where('user_ref', $this->user->public_id)->update(['role' => 'doctor']); $this->actingAs($this->user) ->get(route('care.dashboard')) ->assertOk() ->assertSee('Patients today') ->assertSee('Appointments today') ->assertSee('Patients in queue') ->assertSee('No patients waiting right now.') ->assertDontSee('Revenue today') ->assertDontSee('Open bills') ->assertDontSee('Payments today') ->assertDontSee('Recently paid bills') ->assertDontSee('Team members') ->assertDontSee('Active branches'); } public function test_cashier_dashboard_shows_payments_and_paid_bills(): void { Member::where('user_ref', $this->user->public_id)->update([ 'role' => 'cashier', 'branch_id' => Branch::firstOrFail()->id, ]); $branch = Branch::firstOrFail(); $patient = \App\Models\Patient::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $branch->id, 'patient_number' => 'LC-2026-00099', 'first_name' => 'Ama', 'last_name' => 'Mensah', ]); $visit = \App\Models\Visit::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => \App\Models\Visit::STATUS_COMPLETED, 'checked_in_at' => now()->subHour(), 'completed_at' => now(), ]); $bill = \App\Models\Bill::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $branch->id, 'visit_id' => $visit->id, 'patient_id' => $patient->id, 'invoice_number' => 'INV-DASH-001', 'status' => \App\Models\Bill::STATUS_PAID, 'subtotal_minor' => 4500, 'total_minor' => 4500, 'amount_paid_minor' => 4500, 'balance_minor' => 0, ]); \App\Models\Payment::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'bill_id' => $bill->id, 'amount_minor' => 3000, 'method' => \App\Models\Payment::METHOD_CASH, 'status' => \App\Models\Payment::STATUS_PAID, 'paid_at' => now(), 'recorded_by' => $this->user->public_id, ]); \App\Models\Payment::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, 'bill_id' => $bill->id, 'amount_minor' => 1500, 'method' => \App\Models\Payment::METHOD_MOMO, 'status' => \App\Models\Payment::STATUS_PAID, 'paid_at' => now(), 'recorded_by' => $this->user->public_id, ]); $this->actingAs($this->user) ->get(route('care.dashboard')) ->assertOk() ->assertSee('Patients today') ->assertSee('Open bills') ->assertSee('Payments today') ->assertSee('Collected') ->assertSee('GHS 45.00') ->assertSee('Cash') ->assertSee('GHS 30.00') ->assertSee('Other methods') ->assertSee('GHS 15.00') ->assertSee('Recently paid bills') ->assertSee('INV-DASH-001') ->assertSee('Ama Mensah') ->assertSee(route('care.bills.show', $bill), false) ->assertDontSee('Revenue today') ->assertDontSee('Patients in queue'); } public function test_cashier_pos_app_access_is_stripped_from_staff_ux(): void { Member::where('user_ref', $this->user->public_id)->update(['role' => 'cashier']); $member = Member::where('user_ref', $this->user->public_id)->firstOrFail(); $permissions = app(\App\Services\Care\CarePermissions::class); $this->assertSame( ['care'], $permissions->filterAllowedAppSlugs($member, ['care', 'pos']), ); $filtered = $permissions->filterStaffAppAccess($member, [ 'full_access' => false, 'apps' => ['care', 'pos'], 'show_hub' => true, 'show_billing' => false, ]); $this->assertSame(['care'], $filtered['apps']); $this->assertFalse($filtered['show_hub']); foreach (['pro', 'enterprise'] as $tier) { $cashier = collect(\App\Support\DemoWorld::staff($tier))->firstWhere('key', 'cashier'); $this->assertNotNull($cashier); $this->assertSame(['care'], $cashier['apps']); $this->assertArrayNotHasKey('pos', $cashier['roles']); $this->assertSame('cashier', $cashier['roles']['care'] ?? null); } } public function test_cashier_dashboard_hides_pos_launcher_and_keeps_billing(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); Member::where('user_ref', $this->user->public_id)->update([ 'role' => 'cashier', 'branch_id' => Branch::firstOrFail()->id, ]); // Simulate a stale Identity grant that still includes POS. \App\Support\StaffUx::remember([ 'full_access' => false, 'apps' => ['care', 'pos'], 'show_hub' => true, 'show_billing' => false, ]); $this->actingAs($this->user) ->get(route('care.dashboard')) ->assertOk() ->assertSee('Billing') ->assertSee(route('care.bills.index'), false) ->assertDontSee('https://pos.', false) ->assertDontSee('>POS<', false); } public function test_onboarding_creates_organization(): void { Organization::query()->delete(); Member::query()->delete(); Branch::query()->delete(); Department::query()->delete(); $this->actingAs($this->user) ->post(route('care.onboarding.store'), [ 'organization_name' => 'Accra Medical Centre', 'facility_category' => 'hospital', 'workflow_template' => 'public_hospital_gh', 'branch_name' => 'Head Office', 'branch_address' => '123 Main St', 'branch_phone' => '+233201234567', 'timezone' => 'Africa/Accra', ]) ->assertRedirect(route('care.dashboard')); $this->assertDatabaseHas('care_organizations', ['name' => 'Accra Medical Centre']); $this->assertDatabaseHas('care_branches', ['name' => 'Head Office']); $this->assertDatabaseHas('care_departments', ['name' => 'General Outpatient']); $this->assertDatabaseHas('care_facility_workflows', ['template_key' => 'public_hospital_gh']); $this->assertDatabaseHas('care_workflow_stages', ['code' => 'consultation', 'requires_payment' => true]); } public function test_branches_index_loads(): void { // Free plan: multi-branch management is Pro-gated (setup branch still exists). $this->actingAs($this->user) ->get(route('care.branches.index')) ->assertOk() ->assertSee('Upgrade your plan') ->assertSee('View plans') ->assertDontSee('Main Branch'); } public function test_members_index_loads(): void { $this->actingAs($this->user) ->get(route('care.members.index')) ->assertOk() ->assertSee('Team members') ->assertSee('test@example.com') ->assertSee('Settings'); } public function test_audit_log_index_loads(): void { $this->actingAs($this->user) ->get(route('care.audit.index')) ->assertOk() ->assertSee('Audit log'); } }