Files
ladill-care/tests/Feature/CareDeviceTest.php
T
isaaccladandCursor e0a7a64d38
Deploy Ladill Care / deploy (push) Successful in 1m39s
Add clinical device registry, browser wedge scan, and local agent API.
Branch-scoped Care devices with hashed agent tokens, patient barcode lookup/labels, and provenance-aware vitals from a minimal device agent (Pro for agent hardware; wedge free).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 14:28:46 +00:00

301 lines
10 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Branch;
use App\Models\Consultation;
use App\Models\Device;
use App\Models\InvestigationRequest;
use App\Models\InvestigationType;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\User;
use App\Models\Visit;
use App\Models\VitalSign;
use App\Services\Care\DeviceService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
class CareDeviceTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Organization $organization;
protected Branch $branch;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
$this->user = User::create([
'public_id' => 'device-admin-001',
'name' => 'Device Admin',
'email' => 'devices@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->user->public_id,
'name' => 'Device Clinic',
'slug' => 'device-clinic',
'timezone' => 'UTC',
'settings' => [
'onboarded' => true,
'plan' => 'pro',
'plan_expires_at' => now()->addYear()->toIso8601String(),
],
]);
Member::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'user_ref' => $this->user->public_id,
'role' => 'hospital_admin',
]);
$this->branch = Branch::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'name' => 'Main',
'is_active' => true,
]);
}
public function test_admin_can_register_browser_wedge_scanner(): void
{
$this->actingAs($this->user)
->post(route('care.devices.store'), [
'name' => 'Front desk scanner',
'type' => 'barcode_scanner',
'connection_mode' => 'browser_wedge',
'branch_id' => $this->branch->id,
])
->assertRedirect();
$this->assertDatabaseHas('care_devices', [
'name' => 'Front desk scanner',
'type' => 'barcode_scanner',
'connection_mode' => 'browser_wedge',
'organization_id' => $this->organization->id,
]);
}
public function test_free_plan_cannot_register_agent_thermometer(): void
{
$this->organization->update([
'settings' => ['onboarded' => true, 'plan' => 'free'],
]);
$this->actingAs($this->user)
->from(route('care.devices.create'))
->post(route('care.devices.store'), [
'name' => 'Ward thermometer',
'type' => 'thermometer',
'connection_mode' => 'agent',
'branch_id' => $this->branch->id,
])
->assertRedirect(route('care.devices.create'))
->assertSessionHas('error');
$this->assertDatabaseMissing('care_devices', ['name' => 'Ward thermometer']);
}
public function test_device_token_heartbeat_and_vitals_provenance(): void
{
$devices = app(DeviceService::class);
$device = Device::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'name' => 'Agent thermometer',
'type' => 'thermometer',
'connection_mode' => Device::MODE_AGENT,
'status' => Device::STATUS_OFFLINE,
]);
$token = $devices->issueToken($device);
$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-DEV-0001',
'first_name' => 'Ama',
'last_name' => 'Mensah',
]);
$visit = Visit::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'status' => Visit::STATUS_IN_PROGRESS,
'checked_in_at' => now(),
]);
$consultation = Consultation::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->user->public_id,
'visit_id' => $visit->id,
'patient_id' => $patient->id,
'status' => Consultation::STATUS_DRAFT,
'started_at' => now(),
]);
$this->postJson(route('api.device-agent.heartbeat'), [
'agent_version' => '0.1.0',
'hostname' => 'test-host',
], [
'X-Care-Device-Token' => $token,
])
->assertOk()
->assertJsonPath('status', 'active');
$device->refresh();
$this->assertSame(Device::STATUS_ACTIVE, $device->status);
$this->assertNotNull($device->last_seen_at);
$this->postJson(route('api.device-agent.vitals'), [
'consultation_uuid' => $consultation->uuid,
'vitals' => [
'temperature' => 37.1,
'pulse' => 78,
],
'raw' => ['driver' => 'test'],
], [
'X-Care-Device-Token' => $token,
])
->assertCreated()
->assertJsonPath('source', 'device');
$vital = VitalSign::first();
$this->assertNotNull($vital);
$this->assertSame(VitalSign::SOURCE_DEVICE, $vital->source);
$this->assertSame($device->id, $vital->device_id);
$this->assertSame('37.1', (string) $vital->temperature);
$this->assertSame('test', $vital->raw_payload['driver'] ?? null);
}
public function test_invalid_device_token_is_rejected(): void
{
$this->postJson(route('api.device-agent.heartbeat'), [], [
'X-Care-Device-Token' => 'not-a-real-token',
])->assertUnauthorized();
}
public function test_patient_scan_lookup_exact_patient_number(): void
{
$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-SCAN-9999',
'first_name' => 'Kofi',
'last_name' => 'Asante',
]);
$this->actingAs($this->user)
->getJson(route('care.patients.scan', ['code' => 'LC-SCAN-9999']))
->assertOk()
->assertJsonPath('patient.patient_number', 'LC-SCAN-9999')
->assertJsonPath('patient.uuid', $patient->uuid);
$this->actingAs($this->user)
->getJson(route('care.patients.scan', ['code' => 'UNKNOWN-CODE']))
->assertNotFound();
}
public function test_agent_can_collect_sample_barcode(): void
{
$devices = app(DeviceService::class);
$device = Device::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'name' => 'Lab scanner agent',
'type' => 'barcode_scanner',
'connection_mode' => Device::MODE_AGENT,
'status' => Device::STATUS_OFFLINE,
]);
$token = $devices->issueToken($device);
$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-LAB-0001',
'first_name' => 'Efua',
'last_name' => 'Boateng',
]);
$visit = Visit::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'status' => Visit::STATUS_OPEN,
'checked_in_at' => now(),
]);
$type = InvestigationType::create([
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'name' => 'FBC',
'code' => 'FBC',
'category' => 'blood',
'is_active' => true,
]);
$investigation = InvestigationRequest::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->user->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'visit_id' => $visit->id,
'patient_id' => $patient->id,
'investigation_type_id' => $type->id,
'status' => InvestigationRequest::STATUS_PENDING,
'priority' => 'routine',
]);
$this->postJson(route('api.device-agent.samples.collect'), [
'investigation_uuid' => $investigation->uuid,
'sample_barcode' => 'SMP-AGENT-1',
], [
'Authorization' => 'Bearer '.$token,
])
->assertOk()
->assertJsonPath('sample_barcode', 'SMP-AGENT-1')
->assertJsonPath('status', InvestigationRequest::STATUS_SAMPLE_COLLECTED);
}
public function test_patient_label_prints(): void
{
$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-LBL-0001',
'first_name' => 'Yaw',
'last_name' => 'Owusu',
]);
$this->actingAs($this->user)
->get(route('care.patients.label', $patient))
->assertOk()
->assertSee('LC-LBL-0001')
->assertSee('Yaw Owusu');
}
}