Deploy Ladill Care / deploy (push) Successful in 1m39s
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>
301 lines
10 KiB
PHP
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');
|
|
}
|
|
}
|