Files
ladill-care/tests/Feature/CareAssessmentEngineTest.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

340 lines
12 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Assessment;
use App\Models\AssessmentAnswer;
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 Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
/**
* PR 1 — schema, models, relations, catalog uniqueness (no service/UI yet).
*/
class CareAssessmentEngineTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected Branch $branch;
protected Patient $patient;
protected function setUp(): void
{
parent::setUp();
$this->user = User::create([
'public_id' => 'test-user-assess-001',
'name' => 'Assessment Test User',
'email' => 'assess@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->user->public_id,
'name' => 'Assess Clinic',
'slug' => 'assess-clinic',
'timezone' => 'UTC',
'settings' => ['onboarded' => true],
]);
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-2026-ASSESS-001',
'first_name' => 'Ama',
'last_name' => 'Mensah',
]);
}
public function test_template_auto_generates_uuid_and_has_no_owner_ref(): void
{
$template = AssessmentTemplate::create([
'code' => 'universal_intake',
'name' => 'Universal Intake',
'category' => AssessmentTemplate::CATEGORY_UNIVERSAL,
'version' => 1,
'is_current' => true,
'is_active' => true,
'meta' => ['capture_roles' => ['nurse', 'doctor']],
]);
$this->assertNotEmpty($template->uuid);
$this->assertTrue(Str::isUuid($template->uuid));
$this->assertNull($template->organization_id);
$this->assertFalse(isset($template->getAttributes()['owner_ref']));
$this->assertSame(['capture_roles' => ['nurse', 'doctor']], $template->meta);
$this->assertSame('uuid', $template->getRouteKeyName());
}
public function test_system_template_code_version_is_unique(): void
{
AssessmentTemplate::create([
'code' => 'nihss',
'name' => 'NIHSS v1',
'category' => AssessmentTemplate::CATEGORY_DISEASE,
'version' => 1,
'is_current' => true,
]);
$this->expectException(QueryException::class);
AssessmentTemplate::create([
'code' => 'nihss',
'name' => 'NIHSS v1 duplicate',
'category' => AssessmentTemplate::CATEGORY_DISEASE,
'version' => 1,
'is_current' => false,
]);
}
public function test_same_code_different_versions_allowed(): void
{
AssessmentTemplate::create([
'code' => 'mrs',
'name' => 'mRS v1',
'category' => AssessmentTemplate::CATEGORY_DISEASE,
'version' => 1,
'is_current' => false,
]);
$v2 = AssessmentTemplate::create([
'code' => 'mrs',
'name' => 'mRS v2',
'category' => AssessmentTemplate::CATEGORY_DISEASE,
'version' => 2,
'is_current' => true,
]);
$this->assertSame(2, AssessmentTemplate::where('code', 'mrs')->count());
$this->assertTrue($v2->is_current);
$this->assertSame($v2->id, AssessmentTemplate::currentSystemByCode('mrs')?->id);
}
public function test_question_unique_per_template_and_ordered_relation(): void
{
$template = AssessmentTemplate::create([
'code' => 'fixture_scale',
'name' => 'Fixture',
'category' => AssessmentTemplate::CATEGORY_SCREENING,
'version' => 1,
'is_current' => true,
]);
AssessmentQuestion::create([
'template_id' => $template->id,
'code' => 'item_b',
'label' => 'Item B',
'answer_type' => AssessmentQuestion::TYPE_SCORE_ITEM,
'sort_order' => 2,
'is_required' => true,
]);
AssessmentQuestion::create([
'template_id' => $template->id,
'code' => 'item_a',
'label' => 'Item A',
'answer_type' => AssessmentQuestion::TYPE_SCORE_ITEM,
'sort_order' => 1,
'is_required' => true,
]);
$codes = $template->questions()->pluck('code')->all();
$this->assertSame(['item_a', 'item_b'], $codes);
$this->expectException(QueryException::class);
AssessmentQuestion::create([
'template_id' => $template->id,
'code' => 'item_a',
'label' => 'Duplicate',
'answer_type' => AssessmentQuestion::TYPE_TEXT,
'sort_order' => 3,
]);
}
public function test_assessment_instance_relations_and_owned_scope(): void
{
$template = AssessmentTemplate::create([
'code' => 'universal_intake',
'name' => 'Universal Intake',
'category' => AssessmentTemplate::CATEGORY_UNIVERSAL,
'version' => 1,
'is_current' => true,
]);
$question = AssessmentQuestion::create([
'template_id' => $template->id,
'code' => 'chief_complaint',
'label' => 'Chief complaint',
'answer_type' => AssessmentQuestion::TYPE_TEXT,
'sort_order' => 1,
'is_required' => true,
]);
$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(),
]);
$assessment = Assessment::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'template_id' => $template->id,
'consultation_id' => $consultation->id,
'visit_id' => $visit->id,
'status' => Assessment::STATUS_DRAFT,
'started_by' => $this->user->public_id,
]);
$this->assertNotEmpty($assessment->uuid);
$this->assertTrue(Str::isUuid($assessment->uuid));
$this->assertTrue($assessment->isDraft());
$this->assertFalse($assessment->isCompleted());
AssessmentAnswer::create([
'owner_ref' => $this->user->public_id,
'assessment_id' => $assessment->id,
'question_id' => $question->id,
'value_text' => 'Headache for 2 days',
]);
$this->assertSame(1, $assessment->answers()->count());
$this->assertSame('Headache for 2 days', $assessment->answers->first()->authoritativeValue());
$this->assertTrue($this->patient->assessments->contains($assessment));
$this->assertTrue($consultation->assessments->contains($assessment));
$this->assertTrue($visit->assessments->contains($assessment));
$this->assertSame($template->id, $assessment->template->id);
$this->assertSame(1, Assessment::query()->owned($this->user->public_id)->count());
$this->assertSame(0, Assessment::query()->owned('other-owner')->count());
}
public function test_answer_unique_per_assessment_and_question(): void
{
$template = AssessmentTemplate::create([
'code' => 'fixture',
'name' => 'Fixture',
'category' => AssessmentTemplate::CATEGORY_UNIVERSAL,
'version' => 1,
'is_current' => true,
]);
$question = AssessmentQuestion::create([
'template_id' => $template->id,
'code' => 'pain_score',
'label' => 'Pain',
'answer_type' => AssessmentQuestion::TYPE_NUMBER,
'sort_order' => 1,
]);
$assessment = Assessment::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'template_id' => $template->id,
'status' => Assessment::STATUS_DRAFT,
]);
AssessmentAnswer::create([
'owner_ref' => $this->user->public_id,
'assessment_id' => $assessment->id,
'question_id' => $question->id,
'value_number' => 5,
]);
$this->expectException(QueryException::class);
AssessmentAnswer::create([
'owner_ref' => $this->user->public_id,
'assessment_id' => $assessment->id,
'question_id' => $question->id,
'value_number' => 7,
]);
}
public function test_deleting_assessment_cascades_answers_but_not_template(): void
{
$template = AssessmentTemplate::create([
'code' => 'cascade_fixture',
'name' => 'Cascade',
'category' => AssessmentTemplate::CATEGORY_UNIVERSAL,
'version' => 1,
'is_current' => true,
]);
$question = AssessmentQuestion::create([
'template_id' => $template->id,
'code' => 'q1',
'label' => 'Q1',
'answer_type' => AssessmentQuestion::TYPE_BOOLEAN,
'sort_order' => 1,
]);
$assessment = Assessment::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'patient_id' => $this->patient->id,
'template_id' => $template->id,
'status' => Assessment::STATUS_DRAFT,
]);
AssessmentAnswer::create([
'owner_ref' => $this->user->public_id,
'assessment_id' => $assessment->id,
'question_id' => $question->id,
'value_boolean' => true,
]);
$assessmentId = $assessment->id;
$assessment->forceDelete();
$this->assertDatabaseMissing('care_assessments', ['id' => $assessmentId]);
$this->assertDatabaseMissing('care_assessment_answers', ['assessment_id' => $assessmentId]);
$this->assertDatabaseHas('care_assessment_templates', ['id' => $template->id]);
$this->assertDatabaseHas('care_assessment_questions', ['id' => $question->id]);
}
}