Deploy Ladill Care / deploy (push) Successful in 1m26s
Add a template-driven assessment system with universal intake, clinical pathways, disease instruments (stroke MVP + extended, diabetes, and ten specialty packs), scoring, patient outcome trends, REST/API + FHIR export, and Enterprise org-level assessment analytics. Seed packs and design/licensing docs ship for deploy and pre-GA review.
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('Pathway instruments')
|
|
->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();
|
|
}
|
|
}
|