Deploy Ladill Care / deploy (push) Successful in 1m22s
Replace internal design jargon (Layer 1, narrative source of truth, pathway instruments) with plain clinical language on consultations, patient charts, settings, and nav.
290 lines
10 KiB
PHP
290 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Http\Middleware\EnsurePlatformSession;
|
|
use App\Models\Assessment;
|
|
use App\Models\AssessmentScore;
|
|
use App\Models\AssessmentTemplate;
|
|
use App\Models\Branch;
|
|
use App\Models\ClinicalPathway;
|
|
use App\Models\Consultation;
|
|
use App\Models\Diagnosis;
|
|
use App\Models\Member;
|
|
use App\Models\Organization;
|
|
use App\Models\Patient;
|
|
use App\Models\PatientPathway;
|
|
use App\Models\User;
|
|
use App\Models\Visit;
|
|
use App\Services\Care\CareFeatures;
|
|
use App\Services\Care\ScoringService;
|
|
use Database\Seeders\AssessmentTemplateSeeder;
|
|
use Database\Seeders\ClinicalPathwaySeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* PR 6 — Stroke MVP content packs (NIHSS + mRS) and M2 vertical slice.
|
|
*/
|
|
class CareStrokeMvpTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $user;
|
|
|
|
protected Organization $organization;
|
|
|
|
protected Branch $branch;
|
|
|
|
protected Patient $patient;
|
|
|
|
protected Member $member;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->withoutMiddleware(EnsurePlatformSession::class);
|
|
|
|
$this->user = User::create([
|
|
'public_id' => 'stroke-mvp-user',
|
|
'name' => 'Stroke MVP',
|
|
'email' => 'stroke@example.com',
|
|
]);
|
|
|
|
$this->organization = Organization::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'name' => 'Stroke Clinic',
|
|
'slug' => 'stroke-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-STROKE-001',
|
|
'first_name' => 'Efua',
|
|
'last_name' => 'Asante',
|
|
]);
|
|
|
|
$this->seed(AssessmentTemplateSeeder::class);
|
|
$this->seed(ClinicalPathwaySeeder::class);
|
|
}
|
|
|
|
public function test_seed_packs_load_nihss_and_mrs(): void
|
|
{
|
|
$nihss = AssessmentTemplate::currentSystemByCode('nihss');
|
|
$mrs = AssessmentTemplate::currentSystemByCode('mrs');
|
|
|
|
$this->assertNotNull($nihss);
|
|
$this->assertNotNull($mrs);
|
|
$this->assertSame('sum_items', $nihss->scoring_strategy);
|
|
$this->assertSame('single_value', $mrs->scoring_strategy);
|
|
$this->assertSame(15, $nihss->questions()->count());
|
|
$this->assertSame(1, $mrs->questions()->count());
|
|
$this->assertSame(['doctor'], $nihss->meta['capture_roles'] ?? null);
|
|
|
|
$pathway = ClinicalPathway::findByCode('stroke');
|
|
$this->assertNotNull($pathway);
|
|
$codes = $pathway->templates()->pluck('template_code')->all();
|
|
$this->assertContains('nihss', $codes);
|
|
$this->assertContains('mrs', $codes);
|
|
$required = $pathway->templates()->where('is_required_on_activation', true)->pluck('template_code')->all();
|
|
$this->assertEqualsCanonicalizing(['nihss', 'mrs'], $required);
|
|
}
|
|
|
|
public function test_golden_mrs_score_three(): void
|
|
{
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.store', $this->patient), ['template_code' => 'mrs'])
|
|
->assertRedirect();
|
|
|
|
$assessment = Assessment::first();
|
|
$this->actingAs($this->user)
|
|
->put(route('care.assessments.update', $assessment), [
|
|
'answers' => ['mrs_score' => '3'],
|
|
])
|
|
->assertRedirect();
|
|
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.complete', $assessment))
|
|
->assertRedirect();
|
|
|
|
$score = AssessmentScore::where('assessment_id', $assessment->id)->first();
|
|
$this->assertNotNull($score);
|
|
$this->assertEquals(3, (float) $score->total_score);
|
|
$this->assertEquals(6, (float) $score->max_score);
|
|
$this->assertSame('mrs', $score->template_code);
|
|
}
|
|
|
|
public function test_golden_nihss_fixture_total_twelve_max_forty_two(): void
|
|
{
|
|
// Moderate left hemisphere pattern → total 12; max 42.
|
|
$answers = [
|
|
'1a_loc' => '1',
|
|
'1b_loc_questions' => '1',
|
|
'1c_loc_commands' => '0',
|
|
'2_gaze' => '0',
|
|
'3_visual' => '1',
|
|
'4_facial' => '2',
|
|
'5a_motor_arm_left' => '2',
|
|
'5b_motor_arm_right' => '0',
|
|
'6a_motor_leg_left' => '2',
|
|
'6b_motor_leg_right' => '0',
|
|
'7_ataxia' => '0',
|
|
'8_sensory' => '1',
|
|
'9_language' => '1',
|
|
'10_dysarthria' => '1',
|
|
'11_extinction' => '0',
|
|
];
|
|
// 1+1+0+0+1+2+2+0+2+0+0+1+1+1+0 = 12
|
|
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.store', $this->patient), ['template_code' => 'nihss'])
|
|
->assertRedirect();
|
|
|
|
$assessment = Assessment::first();
|
|
$this->actingAs($this->user)
|
|
->put(route('care.assessments.update', $assessment), ['answers' => $answers])
|
|
->assertRedirect();
|
|
|
|
$preview = app(ScoringService::class)->preview(
|
|
$assessment->fresh(['template.questions', 'answers.question'])
|
|
);
|
|
$this->assertEquals(12.0, $preview['total']);
|
|
$this->assertEquals(42.0, $preview['max']);
|
|
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.complete', $assessment))
|
|
->assertRedirect();
|
|
|
|
$score = AssessmentScore::first();
|
|
$this->assertEquals(12, (float) $score->total_score);
|
|
$this->assertEquals(42, (float) $score->max_score);
|
|
$this->assertArrayHasKey('loc', $score->subscores);
|
|
$this->assertEquals(2.0, (float) $score->subscores['loc']); // 1a+1b+1c = 2
|
|
}
|
|
|
|
public function test_m2_e2e_activate_stroke_complete_nihss_and_mrs(): void
|
|
{
|
|
$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, unspecified',
|
|
'is_primary' => true,
|
|
]);
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('care.consultations.show', $consultation))
|
|
->assertOk()
|
|
->assertSee('Stroke')
|
|
->assertSee('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());
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('care.consultations.show', $consultation))
|
|
->assertOk()
|
|
->assertSee('Recommended forms')
|
|
->assertSee('NIH Stroke Scale')
|
|
->assertSee('Modified Rankin Scale')
|
|
->assertSee('Continue');
|
|
|
|
$nihss = Assessment::query()
|
|
->whereHas('template', fn ($q) => $q->where('code', 'nihss'))
|
|
->first();
|
|
$mrs = Assessment::query()
|
|
->whereHas('template', fn ($q) => $q->where('code', 'mrs'))
|
|
->first();
|
|
|
|
$this->assertNotNull($nihss);
|
|
$this->assertNotNull($mrs);
|
|
$this->assertSame($consultation->id, $nihss->consultation_id);
|
|
|
|
// Minimal valid NIHSS (all zeros) → total 0.
|
|
$zeroAnswers = collect($nihss->template->questions)->mapWithKeys(
|
|
fn ($q) => [$q->code => '0']
|
|
)->all();
|
|
|
|
$this->actingAs($this->user)
|
|
->put(route('care.assessments.update', $nihss), ['answers' => $zeroAnswers])
|
|
->assertRedirect();
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.complete', $nihss))
|
|
->assertRedirect();
|
|
|
|
$this->assertEquals(0, (float) AssessmentScore::where('assessment_id', $nihss->id)->value('total_score'));
|
|
$this->assertEquals(42, (float) AssessmentScore::where('assessment_id', $nihss->id)->value('max_score'));
|
|
|
|
$this->actingAs($this->user)
|
|
->put(route('care.assessments.update', $mrs), ['answers' => ['mrs_score' => '2']])
|
|
->assertRedirect();
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.complete', $mrs))
|
|
->assertRedirect();
|
|
|
|
$this->assertEquals(2, (float) AssessmentScore::where('assessment_id', $mrs->id)->value('total_score'));
|
|
|
|
$this->actingAs($this->user)
|
|
->get(route('care.consultations.show', $consultation))
|
|
->assertOk()
|
|
->assertSee('score 0')
|
|
->assertSee('score 2');
|
|
}
|
|
|
|
public function test_nurse_cannot_start_seeded_nihss(): void
|
|
{
|
|
$this->member->update(['role' => 'nurse']);
|
|
|
|
$this->actingAs($this->user)
|
|
->post(route('care.assessments.store', $this->patient), ['template_code' => 'nihss'])
|
|
->assertForbidden();
|
|
}
|
|
}
|