Files
ladill-care/tests/Feature/CareAssessmentCaptureTest.php
T
isaacclad 2ce4bc8993
Deploy Ladill Care / deploy (push) Successful in 1m26s
feat(assessments): layered clinical assessment engine end-to-end
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.
2026-07-16 22:58:09 +00:00

456 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Assessment;
use App\Models\AssessmentQuestion;
use App\Models\AssessmentTemplate;
use App\Models\Branch;
use App\Models\Consultation;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\User;
use App\Models\Visit;
use App\Services\Care\CareFeatures;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
class CareAssessmentCaptureTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected Branch $branch;
protected Patient $patient;
protected Member $member;
protected AssessmentTemplate $universal;
protected AssessmentTemplate $nihss;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
$this->user = User::create([
'public_id' => 'assess-capture-user',
'name' => 'Capture User',
'email' => 'capture@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->user->public_id,
'name' => 'Capture Clinic',
'slug' => 'capture-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-CAP-001',
'first_name' => 'Ama',
'last_name' => 'Mensah',
]);
$this->universal = AssessmentTemplate::create([
'code' => 'universal_intake',
'name' => 'Universal Intake',
'category' => AssessmentTemplate::CATEGORY_UNIVERSAL,
'version' => 1,
'is_current' => true,
'is_active' => true,
'meta' => ['capture_roles' => ['doctor', 'nurse']],
]);
AssessmentQuestion::create([
'template_id' => $this->universal->id,
'code' => 'chief_complaint',
'label' => 'Chief complaint',
'answer_type' => AssessmentQuestion::TYPE_TEXT,
'is_required' => true,
'sort_order' => 1,
]);
AssessmentQuestion::create([
'template_id' => $this->universal->id,
'code' => 'pain_score',
'label' => 'Pain score',
'answer_type' => AssessmentQuestion::TYPE_SCALE,
'options' => ['min' => 0, 'max' => 10],
'is_required' => false,
'sort_order' => 2,
]);
$this->nihss = AssessmentTemplate::create([
'code' => 'nihss',
'name' => 'NIH Stroke Scale',
'category' => AssessmentTemplate::CATEGORY_DISEASE,
'version' => 1,
'is_current' => true,
'is_active' => true,
'scoring_strategy' => 'sum_items',
'meta' => ['capture_roles' => ['doctor']],
]);
AssessmentQuestion::create([
'template_id' => $this->nihss->id,
'code' => 'loc',
'label' => 'Level of consciousness',
'answer_type' => AssessmentQuestion::TYPE_SCORE_ITEM,
'options' => [
'choices' => [
['code' => '0', 'label' => 'Alert', 'score' => 0],
['code' => '1', 'label' => 'Not alert', 'score' => 1],
['code' => '2', 'label' => 'Obtunded', 'score' => 2],
],
],
'is_required' => true,
'sort_order' => 1,
]);
}
protected function setMemberRole(string $role): void
{
$this->member->update(['role' => $role]);
}
public function test_routes_404_when_feature_flag_disabled(): void
{
$this->organization->update([
'settings' => ['onboarded' => true, 'rollout' => [CareFeatures::ASSESSMENTS_ENGINE => false]],
]);
$this->actingAs($this->user)
->get(route('care.assessments.index', $this->patient))
->assertNotFound();
}
public function test_doctor_can_start_nihss(): void
{
$this->setMemberRole('doctor');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), [
'template_code' => 'nihss',
])
->assertRedirect();
$assessment = Assessment::first();
$this->assertNotNull($assessment);
$this->assertSame(Assessment::STATUS_DRAFT, $assessment->status);
$this->assertSame($this->nihss->id, $assessment->template_id);
$this->assertDatabaseHas('care_audit_logs', ['action' => 'assessment.started']);
}
public function test_nurse_can_start_universal_but_not_nihss(): void
{
$this->setMemberRole('nurse');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), [
'template_code' => 'universal_intake',
])
->assertRedirect();
$this->assertSame(1, Assessment::count());
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), [
'template_code' => 'nihss',
])
->assertForbidden();
}
public function test_hospital_admin_can_start_nihss(): void
{
$this->setMemberRole('hospital_admin');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), [
'template_code' => 'nihss',
])
->assertRedirect();
$this->assertSame(1, Assessment::where('template_id', $this->nihss->id)->count());
}
public function test_super_admin_can_start_nihss(): void
{
$this->setMemberRole('super_admin');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), [
'template_code' => 'nihss',
])
->assertRedirect();
$this->assertSame(1, Assessment::count());
}
public function test_start_is_idempotent_for_same_draft(): void
{
$this->setMemberRole('doctor');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), ['template_code' => 'nihss'])
->assertRedirect();
$first = Assessment::first();
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), ['template_code' => 'nihss'])
->assertRedirect(route('care.assessments.show', $first));
$this->assertSame(1, Assessment::count());
}
public function test_save_answers_complete_and_immutable(): void
{
$this->setMemberRole('doctor');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), ['template_code' => 'universal_intake'])
->assertRedirect();
$assessment = Assessment::first();
$this->actingAs($this->user)
->put(route('care.assessments.update', $assessment), [
'answers' => [
'chief_complaint' => 'Headache',
'pain_score' => 4,
],
'notes' => 'Initial',
])
->assertRedirect(route('care.assessments.show', $assessment));
$this->assertDatabaseHas('care_assessment_answers', [
'assessment_id' => $assessment->id,
'value_text' => 'Headache',
]);
$this->assertDatabaseHas('care_assessment_answers', [
'assessment_id' => $assessment->id,
'value_number' => 4,
]);
$this->actingAs($this->user)
->post(route('care.assessments.complete', $assessment))
->assertRedirect();
$assessment->refresh();
$this->assertSame(Assessment::STATUS_COMPLETED, $assessment->status);
$this->assertNotNull($assessment->completed_at);
$this->assertDatabaseHas('care_audit_logs', ['action' => 'assessment.completed']);
$this->actingAs($this->user)
->put(route('care.assessments.update', $assessment), [
'answers' => ['chief_complaint' => 'Changed'],
])
->assertSessionHasErrors('status');
$this->actingAs($this->user)
->post(route('care.assessments.complete', $assessment))
->assertSessionHasErrors('status');
}
public function test_complete_requires_required_answers(): void
{
$this->setMemberRole('nurse');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), ['template_code' => 'universal_intake'])
->assertRedirect();
$assessment = Assessment::first();
$this->actingAs($this->user)
->post(route('care.assessments.complete', $assessment))
->assertSessionHasErrors('answers.chief_complaint');
$this->assertSame(Assessment::STATUS_DRAFT, $assessment->fresh()->status);
}
public function test_cancel_draft_only(): void
{
$this->setMemberRole('doctor');
$this->actingAs($this->user)
->post(route('care.assessments.store', $this->patient), ['template_code' => 'universal_intake'])
->assertRedirect();
$assessment = Assessment::first();
$this->actingAs($this->user)
->post(route('care.assessments.cancel', $assessment))
->assertRedirect(route('care.assessments.index', $this->patient));
$this->assertSame(Assessment::STATUS_CANCELLED, $assessment->fresh()->status);
$this->assertDatabaseHas('care_audit_logs', ['action' => 'assessment.cancelled']);
}
public function test_consultation_scoped_start_links_consultation_and_visit(): void
{
$this->setMemberRole('doctor');
$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)
->post(route('care.consultations.assessments.store', $consultation), [
'template_code' => 'universal_intake',
])
->assertRedirect();
$assessment = Assessment::first();
$this->assertSame($consultation->id, $assessment->consultation_id);
$this->assertSame($visit->id, $assessment->visit_id);
$this->assertSame($this->branch->id, $assessment->branch_id);
}
public function test_public_bodies_use_consultation_uuid(): void
{
$this->setMemberRole('doctor');
$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)
->post(route('care.assessments.store', $this->patient), [
'template_code' => 'universal_intake',
'consultation_uuid' => $consultation->uuid,
])
->assertRedirect();
$assessment = Assessment::first();
$this->assertSame($consultation->id, $assessment->consultation_id);
}
public function test_tenant_isolation_returns_404(): void
{
$this->setMemberRole('doctor');
$otherUser = User::create([
'public_id' => 'other-owner',
'name' => 'Other',
'email' => 'other@example.com',
]);
$otherOrg = Organization::create([
'owner_ref' => $otherUser->public_id,
'name' => 'Other Clinic',
'slug' => 'other-clinic',
'timezone' => 'UTC',
'settings' => ['onboarded' => true, 'rollout' => [CareFeatures::ASSESSMENTS_ENGINE => true]],
]);
Member::create([
'owner_ref' => $otherUser->public_id,
'organization_id' => $otherOrg->id,
'user_ref' => $otherUser->public_id,
'role' => 'doctor',
]);
$otherPatient = Patient::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $otherUser->public_id,
'organization_id' => $otherOrg->id,
'patient_number' => 'LC-OTHER',
'first_name' => 'Other',
'last_name' => 'Patient',
]);
$this->actingAs($this->user)
->get(route('care.assessments.index', $otherPatient))
->assertNotFound();
}
public function test_score_item_stores_numeric_score_from_choice_code(): void
{
$this->setMemberRole('doctor');
$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' => ['loc' => '2'],
])
->assertRedirect();
$this->assertDatabaseHas('care_assessment_answers', [
'assessment_id' => $assessment->id,
'value_number' => 2,
'value_text' => null,
]);
}
public function test_receptionist_cannot_view_assessments(): void
{
$this->setMemberRole('receptionist');
$this->actingAs($this->user)
->get(route('care.assessments.index', $this->patient))
->assertForbidden();
}
}