withoutMiddleware(EnsurePlatformSession::class); $this->owner = User::create([ 'public_id' => 'den-owner', 'name' => 'Owner', 'email' => 'den-owner@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Dental Clinic', 'slug' => 'dental-clinic', 'settings' => [ 'onboarded' => true, 'facility_type' => 'clinic', 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), 'queue_integration_enabled' => true, ], ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, 'role' => 'hospital_admin', 'branch_id' => null, ]); $this->branch = Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'Main', 'is_active' => true, ]); app(SpecialtyModuleService::class)->activate( $this->organization, $this->owner->public_id, 'dentistry', ); $department = Department::query() ->where('branch_id', $this->branch->id) ->where('type', 'dental') ->firstOrFail(); $this->patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-DEN-SUITE', 'first_name' => 'Afia', 'last_name' => 'Darko', 'gender' => 'female', 'date_of_birth' => '1992-01-01', ]); $this->visit = Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $this->patient->id, 'status' => Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now(), ]); Appointment::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $this->patient->id, 'department_id' => $department->id, 'visit_id' => $this->visit->id, 'type' => Appointment::TYPE_WALK_IN, 'status' => Appointment::STATUS_IN_CONSULTATION, 'scheduled_at' => now(), 'waiting_at' => now(), 'checked_in_at' => now(), 'started_at' => now(), ]); } public function test_chart_tab_renders_odontogram_not_text_form(): void { $this->actingAs($this->owner) ->get(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $this->visit, 'tab' => 'odontogram', ])) ->assertOk() ->assertSee('data-care-stage-bar', false) ->assertSee('Waiting room') ->assertSee('Seat at chair') ->assertSee('Odontogram (FDI)') ->assertSee('18') ->assertSee('Save tooth') ->assertDontSee('Teeth affected (FDI numbers'); } public function test_updating_tooth_persists_chart_and_event(): void { $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.tooth', $this->visit), [ 'tooth_code' => '16', 'status' => 'present', 'surfaces' => ['O', 'M'], 'conditions' => ['caries'], 'notes' => 'Occlusal caries', ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $this->visit, 'tab' => 'odontogram', ])); $chart = DentalChart::query()->where('patient_id', $this->patient->id)->first(); $this->assertNotNull($chart); $tooth = $chart->teeth()->where('tooth_code', '16')->first(); $this->assertSame(['O', 'M'], $tooth->surfaces); $this->assertSame(['caries'], $tooth->conditions); $this->assertDatabaseHas('care_dental_chart_events', [ 'dental_chart_id' => $chart->id, 'tooth_code' => '16', 'event_type' => 'tooth_updated', ]); $this->assertSame(1, DentalChartEvent::query()->where('dental_chart_id', $chart->id)->count()); } public function test_plan_procedure_and_bill_flow(): void { $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.plan-items', $this->visit), [ 'procedure_code' => 'den.fill', 'tooth_code' => '16', 'surfaces' => ['O'], 'priority' => 10, ]) ->assertRedirect(); $plan = DentalTreatmentPlan::query()->where('patient_id', $this->patient->id)->first(); $this->assertNotNull($plan); $item = $plan->items()->first(); $this->assertSame('den.fill', $item->procedure_code); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.plan.accept', $this->visit)) ->assertRedirect(); $this->assertSame(DentalTreatmentPlan::STATUS_ACCEPTED, $plan->fresh()->status); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.procedures', $this->visit), [ 'procedure_code' => 'den.fill', 'tooth_code' => '16', 'surfaces' => ['O'], 'plan_item_id' => $item->id, 'bill' => 1, 'anesthesia' => 'Local', ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $this->visit, 'tab' => 'treat', ])); $procedure = DentalProcedure::query()->where('visit_id', $this->visit->id)->first(); $this->assertNotNull($procedure); $this->assertNotNull($procedure->bill_line_item_id); $this->assertSame(DentalPlanItem::STATUS_DONE, $item->fresh()->status); $this->assertDatabaseHas('care_bill_line_items', [ 'id' => $procedure->bill_line_item_id, 'source_type' => 'dental_procedure', 'source_id' => $procedure->id, ]); } public function test_imaging_upload_and_reports_page(): void { Storage::fake('public'); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.images', $this->visit), [ 'attachment' => UploadedFile::fake()->image('xray.jpg'), 'modality' => 'pa', 'tooth_codes' => ['16'], 'caption' => 'PA 16', 'add_fee' => 0, ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $this->visit, 'tab' => 'imaging', ])); $this->assertDatabaseHas('care_dental_images', [ 'visit_id' => $this->visit->id, 'modality' => 'pa', ]); $this->actingAs($this->owner) ->get(route('care.specialty.dentistry.reports')) ->assertOk() ->assertSee('Dentistry reports'); } public function test_notes_and_print_views(): void { $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.notes', $this->visit), [ 'chief_complaint' => 'Toothache upper right', 'soft_tissue' => 'WNL', 'occlusion' => 'Class I', 'notes' => 'Demo note', ]) ->assertRedirect(); $this->assertDatabaseHas('care_dental_visit_notes', [ 'visit_id' => $this->visit->id, 'chief_complaint' => 'Toothache upper right', ]); $this->actingAs($this->owner) ->get(route('care.specialty.dentistry.print', $this->visit)) ->assertOk() ->assertSee('Odontogram (FDI)') ->assertSee('Afia Darko'); } public function test_plan_item_cancel_and_reject(): void { $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.plan-items', $this->visit), [ 'procedure_code' => 'den.fill', 'tooth_code' => '16', 'priority' => 10, ]) ->assertRedirect(); $plan = DentalTreatmentPlan::query()->where('patient_id', $this->patient->id)->firstOrFail(); $item = $plan->items()->firstOrFail(); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.plan-items.cancel', [$this->visit, $item])) ->assertRedirect(); $this->assertSame(DentalPlanItem::STATUS_CANCELLED, $item->fresh()->status); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.plan-items', $this->visit), [ 'procedure_code' => 'den.cleaning', 'priority' => 20, ]) ->assertRedirect(); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.plan.reject', $this->visit), [ 'rejection_reason' => 'Patient declined', ]) ->assertRedirect(); $this->assertSame(DentalTreatmentPlan::STATUS_REJECTED, $plan->fresh()->status); } public function test_visit_stage_advance_and_primary_dentition(): void { $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.stage', $this->visit), [ 'stage' => 'chair', ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'dentistry', 'visit' => $this->visit, 'tab' => 'odontogram', ])); $this->assertSame('chair', $this->visit->fresh()->specialty_stage); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.dentition', $this->visit), [ 'dentition' => 'primary', ]) ->assertRedirect(); $chart = DentalChart::query()->where('patient_id', $this->patient->id)->firstOrFail(); $this->assertSame('primary', $chart->dentition); $this->assertTrue($chart->teeth()->where('tooth_code', '55')->exists()); } public function test_imaging_void_perio_lab_and_recall(): void { Storage::fake('public'); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.images', $this->visit), [ 'attachment' => UploadedFile::fake()->image('xray.jpg'), 'modality' => 'bw', 'tooth_codes' => ['16'], 'add_fee' => 0, ]) ->assertRedirect(); $image = \App\Models\DentalImage::query()->where('visit_id', $this->visit->id)->firstOrFail(); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.images.void', [$this->visit, $image])) ->assertRedirect(); $this->assertSoftDeleted('care_dental_images', ['id' => $image->id]); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.perio', $this->visit), [ 'sites' => [ ['tooth_code' => '16', 'surface' => 'B', 'probing_mm' => 4, 'bleeding' => 1], ], 'notes' => 'Perio demo', ]) ->assertRedirect(); $this->assertDatabaseHas('care_dental_perio_exams', [ 'patient_id' => $this->patient->id, 'visit_id' => $this->visit->id, ]); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.lab-cases', $this->visit), [ 'case_type' => 'crown', 'lab_name' => 'City Lab', 'tooth_codes' => ['16'], 'status' => 'ordered', ]) ->assertRedirect(); $case = \App\Models\DentalLabCase::query()->where('patient_id', $this->patient->id)->firstOrFail(); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.lab-cases.status', [$this->visit, $case]), [ 'status' => 'received', ]) ->assertRedirect(); $this->assertSame('received', $case->fresh()->status); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.recalls', $this->visit), [ 'due_on' => now()->addMonths(3)->toDateString(), 'reason' => 'Check-up', ]) ->assertRedirect(); $recall = \App\Models\DentalRecall::query()->where('patient_id', $this->patient->id)->firstOrFail(); $this->actingAs($this->owner) ->post(route('care.specialty.dentistry.recalls.complete', [$this->visit, $recall])) ->assertRedirect(); $this->assertSame('completed', $recall->fresh()->status); $this->actingAs($this->owner) ->get(route('care.specialty.dentistry.reports')) ->assertOk() ->assertSee('Revenue by procedure'); } }