withoutMiddleware(EnsurePlatformSession::class); $this->owner = User::create([ 'public_id' => 'care-modules-owner', 'name' => 'Owner', 'email' => 'modules-owner@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Modules Clinic', 'slug' => 'modules-clinic', 'settings' => [ 'onboarded' => true, 'facility_type' => 'clinic', 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ], ]); 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, ]); } public function test_activating_dentistry_creates_department_and_queue_stubs(): void { Http::fake(); $service = app(SpecialtyModuleService::class); $service->activate($this->organization, $this->owner->public_id, 'dentistry'); $this->organization->refresh(); $this->assertTrue((bool) data_get($this->organization->settings, 'specialty_modules.dentistry')); $this->assertDatabaseHas('care_departments', [ 'branch_id' => $this->branch->id, 'type' => 'dental', 'name' => 'Dentistry', 'is_active' => true, ]); $stubs = data_get($this->organization->settings, 'specialty_module_provisioning.dentistry.queues'); $this->assertIsArray($stubs); $this->assertNotEmpty($stubs); $this->assertSame('Dentistry', $stubs[0]['name']); $this->assertSame($this->branch->id, $stubs[0]['branch_id']); } public function test_activating_with_queue_integration_creates_native_service_queues(): void { Http::fake(); $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'queue_integration_enabled' => true, ]), ]); app(SpecialtyModuleService::class)->activate( $this->organization->fresh(), $this->owner->public_id, 'dentistry', ); $this->organization->refresh(); $stub = data_get($this->organization->settings, 'specialty_module_provisioning.dentistry.queues.0'); $this->assertTrue((bool) ($stub['synced'] ?? false)); $this->assertNotEmpty($stub['queue_uuid'] ?? null); $this->assertNotEmpty($stub['counter_uuid'] ?? null); $this->assertDatabaseHas('care_service_queues', [ 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'context' => 'dentistry', 'uuid' => $stub['queue_uuid'], ]); Http::assertNothingSent(); } public function test_reactivation_is_idempotent_for_queue_external_keys(): void { Http::fake(); $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'queue_integration_enabled' => true, ]), ]); app(SpecialtyModuleService::class)->activate( $this->organization->fresh(), $this->owner->public_id, 'dentistry', ); $firstQueue = CareServiceQueue::query() ->where('organization_id', $this->organization->id) ->where('context', 'dentistry') ->where('branch_id', $this->branch->id) ->firstOrFail(); $firstUuid = $firstQueue->uuid; $externalKey = $firstQueue->external_key; app(SpecialtyModuleService::class)->deactivate( $this->organization->fresh(), $this->owner->public_id, 'dentistry', ); app(SpecialtyModuleService::class)->activate( $this->organization->fresh(), $this->owner->public_id, 'dentistry', ); $this->organization->refresh(); $stub = data_get($this->organization->settings, 'specialty_module_provisioning.dentistry.queues.0'); $this->assertSame($externalKey, $stub['queue_external_key'] ?? $firstQueue->external_key); $this->assertSame($firstUuid, $stub['queue_uuid'] ?? null); $this->assertEquals( 1, CareServiceQueue::query() ->where('organization_id', $this->organization->id) ->where('context', 'dentistry') ->where('branch_id', $this->branch->id) ->count(), ); Http::assertNothingSent(); } public function test_deactivating_hides_department_without_deleting(): void { Http::fake(); $service = app(SpecialtyModuleService::class); $service->activate($this->organization, $this->owner->public_id, 'dentistry'); $department = Department::query()->where('type', 'dental')->firstOrFail(); $service->deactivate($this->organization->fresh(), $this->owner->public_id, 'dentistry'); $this->organization->refresh(); $this->assertFalse((bool) data_get($this->organization->settings, 'specialty_modules.dentistry')); $this->assertFalse($department->fresh()->is_active); $this->assertDatabaseHas('care_departments', ['id' => $department->id]); } public function test_free_plan_cannot_activate_modules_via_settings(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'free', 'plan_expires_at' => null, ]), ]); $this->actingAs($this->owner) ->put(route('care.settings.modules.update'), [ 'modules' => ['dentistry' => '1'], ]) ->assertRedirect(route('care.pro.index')) ->assertSessionHas('upsell'); } public function test_modules_settings_page_lists_catalog(): void { $this->actingAs($this->owner) ->get(route('care.settings.modules')) ->assertOk() ->assertSee('Dentistry') ->assertSee('Eye care') ->assertSee('Physiotherapy') ->assertSee("Women's Health") ->assertSee('Radiology'); } public function test_specialty_page_visible_when_module_enabled(): void { Http::fake(); app(SpecialtyModuleService::class)->activate($this->organization, $this->owner->public_id, 'dentistry'); $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' => $receptionist->public_id, 'role' => 'receptionist', 'branch_id' => $this->branch->id, ]); $this->actingAs($this->owner) ->get(route('care.specialty.show', 'dentistry')) ->assertOk() ->assertSee('Patient flow'); // Receptionist has refer access to dentistry (registration into specialty queues). $this->actingAs($receptionist) ->get(route('care.specialty.show', 'dentistry')) ->assertOk() ->assertSee('Patient flow'); } public function test_unassigned_gp_can_open_limited_specialty_module(): void { Http::fake(); app(SpecialtyModuleService::class)->activate($this->organization, $this->owner->public_id, 'dentistry'); $doctor = User::create([ 'public_id' => 'unassigned-doc', 'name' => 'General Doctor', 'email' => 'general-doc@example.com', ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $doctor->public_id, 'role' => 'doctor', 'branch_id' => $this->branch->id, ]); $this->actingAs($doctor) ->get(route('care.specialty.show', 'dentistry')) ->assertOk() ->assertSee('Patient flow'); } public function test_assigned_specialty_doctor_can_open_module(): void { Http::fake(); app(SpecialtyModuleService::class)->activate($this->organization, $this->owner->public_id, 'dentistry'); $department = Department::query()->where('type', 'dental')->firstOrFail(); $doctor = User::create([ 'public_id' => 'dental-doc', 'name' => 'Dental Doctor', 'email' => 'dental-doc@example.com', ]); $member = Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $doctor->public_id, 'role' => 'doctor', 'branch_id' => $this->branch->id, ]); \App\Models\Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'department_id' => $department->id, 'member_id' => $member->id, 'user_ref' => $doctor->public_id, 'name' => 'Dental Doctor', 'specialty' => 'Dentistry', 'is_active' => true, ]); $this->actingAs($doctor) ->get(route('care.specialty.show', 'dentistry')) ->assertOk() ->assertSee('Patient flow'); $this->assertTrue( app(SpecialtyModuleService::class)->memberCanAccess( $this->organization->fresh(), $member, 'dentistry', ), ); // Desk specialists only see matching modules — no cross-specialty refer. $this->assertFalse( app(SpecialtyModuleService::class)->memberCanManage( $this->organization->fresh(), $member, 'cardiology', ), ); app(SpecialtyModuleService::class)->activate($this->organization->fresh(), $this->owner->public_id, 'cardiology'); $this->assertFalse( app(SpecialtyModuleService::class)->memberCanRefer( $this->organization->fresh(), $member, 'cardiology', ), ); $this->assertSame( 'none', app(SpecialtyModuleService::class)->memberAccessLevel( $this->organization->fresh(), $member, 'emergency', ), ); } public function test_emergency_and_blood_bank_are_default_on_pro(): void { Http::fake(); $service = app(SpecialtyModuleService::class); $this->assertTrue($service->isEnabled($this->organization, 'emergency')); $this->assertTrue($service->isEnabled($this->organization, 'blood_bank')); $this->assertFalse($service->isEnabled($this->organization, 'cardiology')); $service->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id); $this->assertDatabaseHas('care_departments', [ 'branch_id' => $this->branch->id, 'type' => 'emergency', 'is_active' => true, ]); $this->assertDatabaseHas('care_departments', [ 'branch_id' => $this->branch->id, 'type' => 'blood_bank', 'is_active' => true, ]); $nurse = User::create([ 'public_id' => 'er-nurse', 'name' => 'ER Nurse', 'email' => 'er-nurse@example.com', ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $nurse->public_id, 'role' => 'nurse', 'branch_id' => $this->branch->id, ]); $this->actingAs($this->owner) ->get(route('care.specialty.show', 'emergency')) ->assertOk() ->assertSee('Patient flow'); $this->actingAs($nurse) ->get(route('care.specialty.show', 'emergency')) ->assertOk() ->assertSee('Patient flow'); $this->expectException(\RuntimeException::class); $service->deactivate($this->organization->fresh(), $this->owner->public_id, 'emergency'); } public function test_catalog_includes_expanded_specialty_modules(): void { $catalog = app(SpecialtyModuleService::class)->catalog(); foreach ([ 'emergency', 'blood_bank', 'cardiology', 'psychiatry', 'pediatrics', 'orthopedics', 'ent', 'oncology', 'renal', 'surgery', 'vaccination', 'pathology', 'infusion', 'dermatology', 'podiatry', 'fertility', 'child_welfare', 'ambulance', ] as $key) { $this->assertArrayHasKey($key, $catalog); } $this->assertTrue((bool) ($catalog['emergency']['default_on_paid_plans'] ?? false)); $this->assertTrue((bool) ($catalog['blood_bank']['default_on_paid_plans'] ?? false)); } public function test_each_specialty_module_has_a_unique_icon(): void { $service = app(SpecialtyModuleService::class); $catalog = $service->catalog(); $iconNames = []; $iconPaths = []; foreach (array_keys($catalog) as $key) { $name = $service->iconName($key); $path = $service->iconSvgPath($key); $this->assertNotSame('', $name, "Module [{$key}] missing icon name"); $this->assertNotSame('', $path, "Module [{$key}] missing icon SVG path"); $this->assertArrayHasKey($name, config('care.specialty_icons')); $iconNames[$key] = $name; $iconPaths[$key] = $path; } $this->assertCount(count($iconNames), array_unique(array_values($iconNames))); $this->assertCount(count($iconPaths), array_unique(array_values($iconPaths))); } public function test_modules_settings_renders_distinct_specialty_icons(): void { $response = $this->actingAs($this->owner) ->get(route('care.settings.modules')) ->assertOk(); $response->assertSee(care_specialty_icon_path('dentistry'), false); $response->assertSee(care_specialty_icon_path('emergency'), false); $this->assertNotSame( care_specialty_icon_path('dentistry'), care_specialty_icon_path('emergency'), ); $this->actingAs($this->owner) ->get(route('care.dashboard')) ->assertOk(); } }