Deploy Ladill Care / deploy (push) Successful in 50s
Only POST existed on that path, so navigating to the natural collection URL 404'd. Serve the patient assessments index with consultation return context, and cover it with a doctor feature test. Co-authored-by: Cursor <cursoragent@cursor.com>
503 lines
17 KiB
PHP
503 lines
17 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\Practitioner;
|
|
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->branch = Branch::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'name' => 'Main',
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->member = Member::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'user_ref' => $this->user->public_id,
|
|
'role' => 'doctor',
|
|
'branch_id' => $this->branch->id,
|
|
]);
|
|
|
|
// Doctors are branch-scoped via linked practitioner desks.
|
|
Practitioner::create([
|
|
'owner_ref' => $this->user->public_id,
|
|
'organization_id' => $this->organization->id,
|
|
'branch_id' => $this->branch->id,
|
|
'member_id' => $this->member->id,
|
|
'user_ref' => $this->user->public_id,
|
|
'name' => 'Dr Capture',
|
|
'specialty' => 'General Practice',
|
|
'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_doctor_can_view_consultation_scoped_assessments_index(): 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)
|
|
->get(route('care.consultations.assessments.index', $consultation))
|
|
->assertOk()
|
|
->assertSee('Clinical assessments')
|
|
->assertSee($this->patient->fullName());
|
|
|
|
// Path shape matches care.ladill.com/consultations/{uuid}/assessments
|
|
$this->actingAs($this->user)
|
|
->get('/consultations/'.$consultation->uuid.'/assessments')
|
|
->assertOk();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|