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.
340 lines
12 KiB
PHP
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]);
|
|
}
|
|
}
|