withoutMiddleware(EnsurePlatformSession::class); $this->user = User::create([ 'public_id' => 'pathway-user', 'name' => 'Pathway User', 'email' => 'pathway@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->user->public_id, 'name' => 'Pathway Clinic', 'slug' => 'pathway-clinic', 'timezone' => 'UTC', 'settings' => [ 'onboarded' => true, 'rollout' => [CareFeatures::ASSESSMENTS_ENGINE => true], ], ]); $this->member = Member::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->user->public_id, 'role' => 'doctor', ]); $this->branch = Branch::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'name' => 'Main', 'is_active' => true, ]); $this->patient = Patient::create([ 'uuid' => (string) Str::uuid(), 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'LC-PATH-001', 'first_name' => 'Kojo', 'last_name' => 'Stroke', ]); } public function test_clinical_pathway_seeder_loads_stroke(): void { $this->seed(ClinicalPathwaySeeder::class); $pathway = ClinicalPathway::findByCode('stroke'); $this->assertNotNull($pathway); $this->assertTrue($pathway->is_active); $this->assertContains('I63', $pathway->match_rules['icd_prefixes'] ?? []); $this->assertGreaterThanOrEqual(2, $pathway->templates()->count()); $this->assertTrue($pathway->templates->contains('template_code', 'nihss')); $this->assertTrue($pathway->templates->contains('template_code', 'mrs')); $required = $pathway->templates()->where('is_required_on_activation', true)->pluck('template_code')->all(); $this->assertEqualsCanonicalizing(['nihss', 'mrs'], $required); } public function test_matcher_icd_keyword_exclude_and_no_match(): void { $this->seed(ClinicalPathwaySeeder::class); $matcher = app(PathwayMatcher::class); $icd = $matcher->match([['code' => 'I63.9', 'description' => '']]); $this->assertCount(1, $icd); $this->assertSame('stroke', $icd[0]['pathway_code']); $this->assertSame(100, $icd[0]['rank']); $this->assertStringStartsWith('icd_prefix:', $icd[0]['match_reason']); $kw = $matcher->match([['code' => '', 'description' => 'Ischaemic stroke']]); $this->assertCount(1, $kw); $this->assertSame(50, $kw[0]['rank']); $this->assertStringContainsString('keyword:', $kw[0]['match_reason']); $tia = $matcher->match([['code' => 'G45.9', 'description' => 'TIA']]); $this->assertCount(1, $tia); $this->assertSame('stroke', $tia[0]['pathway_code']); $none = $matcher->match([['code' => '', 'description' => 'History unclear']]); $this->assertCount(0, $none); $excluded = $matcher->match([['code' => '', 'description' => 'Family history of stroke']]); $this->assertCount(0, $excluded); } public function test_activate_is_idempotent_and_creates_required_drafts(): void { $this->seed(ClinicalPathwaySeeder::class); // Minimal current templates so activation can draft them. foreach (['nihss' => 'NIHSS', 'mrs' => 'mRS'] as $code => $name) { $t = AssessmentTemplate::create([ 'code' => $code, 'name' => $name, 'category' => AssessmentTemplate::CATEGORY_DISEASE, 'version' => 1, 'is_current' => true, 'scoring_strategy' => $code === 'mrs' ? 'single_value' : 'sum_items', 'meta' => ['capture_roles' => ['doctor']], ]); AssessmentQuestion::create([ 'template_id' => $t->id, 'code' => 'item', 'label' => 'Item', 'answer_type' => AssessmentQuestion::TYPE_SCORE_ITEM, 'options' => ['choices' => [['code' => '1', 'label' => '1', 'score' => 1]], 'max' => 1], 'is_required' => true, 'sort_order' => 1, ]); } $visit = Visit::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $this->patient->id, 'status' => Visit::STATUS_OPEN, 'checked_in_at' => now(), ]); $consultation = Consultation::create([ 'owner_ref' => $this->user->public_id, 'visit_id' => $visit->id, 'patient_id' => $this->patient->id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now(), ]); Diagnosis::create([ 'owner_ref' => $this->user->public_id, 'consultation_id' => $consultation->id, 'code' => 'I63.9', 'description' => 'Cerebral infarction', 'is_primary' => true, ]); $this->actingAs($this->user) ->post(route('care.pathways.store', $this->patient), [ 'pathway_code' => 'stroke', 'consultation_uuid' => $consultation->uuid, ]) ->assertRedirect(route('care.pathways.index', $this->patient)); $this->assertSame(1, PatientPathway::where('status', PatientPathway::STATUS_ACTIVE)->count()); $pp = PatientPathway::first(); $this->assertStringContainsString('I63.9', (string) $pp->activation_diagnosis_text); $this->assertSame($consultation->id, $pp->activation_consultation_id); $this->assertSame(2, Assessment::where('status', Assessment::STATUS_DRAFT)->count()); $this->assertTrue(Assessment::where('patient_pathway_id', $pp->id)->exists()); $this->assertDatabaseHas('care_audit_logs', ['action' => 'pathway.activated']); // Idempotent re-activate $this->actingAs($this->user) ->post(route('care.pathways.store', $this->patient), [ 'pathway_code' => 'stroke', 'consultation_uuid' => $consultation->uuid, ]) ->assertRedirect(); $this->assertSame(1, PatientPathway::where('status', PatientPathway::STATUS_ACTIVE)->count()); $this->assertSame(2, Assessment::where('status', Assessment::STATUS_DRAFT)->count()); } public function test_suggestions_use_persisted_diagnoses_only(): void { $this->seed(ClinicalPathwaySeeder::class); $visit = Visit::create([ 'owner_ref' => $this->user->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $this->patient->id, 'status' => Visit::STATUS_OPEN, 'checked_in_at' => now(), ]); $consultation = Consultation::create([ 'owner_ref' => $this->user->public_id, 'visit_id' => $visit->id, 'patient_id' => $this->patient->id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now(), ]); $this->actingAs($this->user) ->getJson(route('care.consultations.pathway-suggestions', $consultation)) ->assertOk() ->assertJsonPath('data', []); Diagnosis::create([ 'owner_ref' => $this->user->public_id, 'consultation_id' => $consultation->id, 'code' => null, 'description' => 'Ischaemic stroke', 'is_primary' => true, ]); $this->actingAs($this->user) ->getJson(route('care.consultations.pathway-suggestions', $consultation)) ->assertOk() ->assertJsonPath('data.0.pathway_code', 'stroke') ->assertJsonPath('data.0.already_active', false); $this->actingAs($this->user) ->get(route('care.consultations.show', $consultation)) ->assertOk() ->assertSee('Clinical pathways') ->assertSee('Stroke') ->assertSee('keyword:'); } public function test_deactivate_pathway(): void { $this->seed(ClinicalPathwaySeeder::class); $pathway = ClinicalPathway::findByCode('stroke'); $pp = app(PathwayService::class)->activate( $this->patient, $pathway, $this->user->public_id, $this->member, ['actor' => $this->user->public_id, 'activation_diagnosis_text' => 'stroke'], ); $this->actingAs($this->user) ->post(route('care.pathways.deactivate', [$this->patient, $pp])) ->assertRedirect(); $this->assertSame(PatientPathway::STATUS_INACTIVE, $pp->fresh()->status); $this->assertDatabaseHas('care_audit_logs', ['action' => 'pathway.deactivated']); } public function test_nurse_cannot_activate_pathway(): void { $this->seed(ClinicalPathwaySeeder::class); $this->member->update(['role' => 'nurse']); $this->actingAs($this->user) ->post(route('care.pathways.store', $this->patient), [ 'pathway_code' => 'stroke', ]) ->assertForbidden(); } public function test_template_version_bump_still_resolves_pathway_binding(): void { $this->seed(ClinicalPathwaySeeder::class); AssessmentTemplate::create([ 'code' => 'nihss', 'name' => 'NIHSS v1', 'category' => AssessmentTemplate::CATEGORY_DISEASE, 'version' => 1, 'is_current' => false, 'meta' => ['capture_roles' => ['doctor']], ]); $v2 = AssessmentTemplate::create([ 'code' => 'nihss', 'name' => 'NIHSS v2', 'category' => AssessmentTemplate::CATEGORY_DISEASE, 'version' => 2, 'is_current' => true, 'meta' => ['capture_roles' => ['doctor']], ]); AssessmentQuestion::create([ 'template_id' => $v2->id, 'code' => 'item', 'label' => 'Item', 'answer_type' => AssessmentQuestion::TYPE_SCORE_ITEM, 'options' => ['choices' => [['code' => '1', 'label' => '1', 'score' => 1]]], 'is_required' => true, 'sort_order' => 1, ]); // Only nihss required for this test — remove mrs binding temporarily via pathway seed already has both. // Activate stroke; mrs missing is skipped; nihss v2 should draft. $pathway = ClinicalPathway::findByCode('stroke'); $pp = app(PathwayService::class)->activate( $this->patient, $pathway, $this->user->public_id, $this->member, ['actor' => $this->user->public_id], ); $draft = Assessment::where('patient_pathway_id', $pp->id)->where('template_id', $v2->id)->first(); $this->assertNotNull($draft); $this->assertSame(2, $draft->template->version); } }