withoutMiddleware(EnsurePlatformSession::class); $this->owner = User::create([ 'public_id' => 'bb-owner', 'name' => 'Owner', 'email' => 'bb-owner@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Blood Bank Clinic', 'slug' => 'blood-bank-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, 'blood_bank', ); $department = Department::query() ->where('branch_id', $this->branch->id) ->where('type', 'blood_bank') ->firstOrFail(); $this->patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'P-BB-SUITE', 'first_name' => 'Ama', 'last_name' => 'Mensah', 'gender' => 'female', 'date_of_birth' => '1992-03-15', ]); $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(), 'specialty_stage' => 'request', ]); 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_overview_and_requests_tabs_render(): void { $this->actingAs($this->owner) ->get(route('care.specialty.workspace', [ 'module' => 'blood_bank', 'visit' => $this->visit, 'tab' => 'overview', ])) ->assertOk() ->assertSee('Blood Bank overview') ->assertSee('data-care-stage-bar', false) ->assertSee('Request received') ->assertSee('Start cross-match') ->assertSee('BB reports'); $this->actingAs($this->owner) ->get(route('care.specialty.workspace', [ 'module' => 'blood_bank', 'visit' => $this->visit, 'tab' => 'requests', ])) ->assertOk() ->assertSee('Product') ->assertSee('Units requested'); } public function test_request_sets_stage_and_alerts(): void { $this->actingAs($this->owner) ->post(route('care.specialty.clinical.save', [ 'module' => 'blood_bank', 'visit' => $this->visit, ]), [ 'tab' => 'requests', 'payload' => [ 'product' => 'Packed RBC', 'units' => '2', 'urgency' => 'Emergency / massive', 'patient_blood_group' => 'O+', 'indication' => 'Trauma bleed', 'crossmatch_status' => 'In progress', ], ]) ->assertRedirect(); $this->assertSame('crossmatch', $this->visit->fresh()->specialty_stage); $record = SpecialtyClinicalRecord::query() ->where('visit_id', $this->visit->id) ->where('record_type', 'blood_request') ->firstOrFail(); $codes = collect($record->alerts)->pluck('code')->all(); $this->assertContains('bb.massive_transfusion', $codes); } public function test_stage_advance_issue_and_transfusion(): void { $this->actingAs($this->owner) ->post(route('care.specialty.clinical.save', [ 'module' => 'blood_bank', 'visit' => $this->visit, ]), [ 'tab' => 'requests', 'payload' => [ 'product' => 'Packed RBC', 'units' => '2', 'urgency' => 'Urgent', 'patient_blood_group' => 'A+', 'indication' => 'Anaemia', 'crossmatch_status' => 'Compatible', ], ]) ->assertRedirect(); $this->actingAs($this->owner) ->post(route('care.specialty.blood-bank.stage', $this->visit), [ 'stage' => 'crossmatch', ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'blood_bank', 'visit' => $this->visit, 'tab' => 'requests', ])); $this->assertSame('crossmatch', $this->visit->fresh()->specialty_stage); $this->actingAs($this->owner) ->post(route('care.specialty.blood-bank.issue', $this->visit), [ 'tab' => 'issue', 'bill_product' => '1', 'bill_crossmatch' => '1', 'payload' => [ 'product' => 'Packed RBC', 'units_issued' => '2', 'bag_numbers' => 'BB-1001, BB-1002', 'issued_to' => 'Ward A', 'issued_at' => now()->format('Y-m-d H:i'), ], ]) ->assertRedirect(route('care.specialty.workspace', [ 'module' => 'blood_bank', 'visit' => $this->visit, 'tab' => 'issue', ])); $this->assertSame('issue', $this->visit->fresh()->specialty_stage); $this->assertDatabaseHas('care_specialty_clinical_records', [ 'visit_id' => $this->visit->id, 'record_type' => 'issue_note', 'status' => SpecialtyClinicalRecord::STATUS_COMPLETED, ]); $this->visit->load('bill.lineItems'); $this->assertNotNull($this->visit->bill); $this->visit->load('bill.lineItems'); $this->assertNotNull($this->visit->bill); $descriptions = $this->visit->bill->lineItems->pluck('description')->all(); $this->assertContains('Packed RBC unit', $descriptions); $this->assertContains('Cross-match', $descriptions); $packed = $this->visit->bill->lineItems->firstWhere('description', 'Packed RBC unit'); $this->assertSame(2, (int) $packed->quantity); $this->actingAs($this->owner) ->post(route('care.specialty.blood-bank.transfusion', $this->visit), [ 'tab' => 'transfusion', 'payload' => [ 'started_at' => now()->format('Y-m-d H:i'), 'units_transfused' => '2', 'vitals_ok' => '1', 'reaction' => 'None', 'outcome' => 'Completed uneventfully', 'notes' => 'Tolerated well', ], ]) ->assertRedirect(); $this->assertSame('completed', $this->visit->fresh()->specialty_stage); $this->assertSame(Visit::STATUS_COMPLETED, $this->visit->fresh()->status); $this->assertDatabaseHas('care_specialty_clinical_records', [ 'visit_id' => $this->visit->id, 'record_type' => 'transfusion_note', 'status' => SpecialtyClinicalRecord::STATUS_COMPLETED, ]); } public function test_inventory_low_stock_and_reports_print(): void { $this->actingAs($this->owner) ->post(route('care.specialty.clinical.save', [ 'module' => 'blood_bank', 'visit' => $this->visit, ]), [ 'tab' => 'inventory', 'payload' => [ 'whole_blood_units' => '1', 'packed_rbc_units' => '8', 'platelet_units' => '4', 'ffp_units' => '3', 'low_stock_alert' => '1', ], ]) ->assertRedirect(); $record = SpecialtyClinicalRecord::query() ->where('visit_id', $this->visit->id) ->where('record_type', 'inventory_note') ->firstOrFail(); $codes = collect($record->alerts)->pluck('code')->all(); $this->assertContains('bb.low_stock', $codes); $this->assertContains('bb.unit_critical', $codes); $this->actingAs($this->owner) ->get(route('care.specialty.blood-bank.reports')) ->assertOk() ->assertSee('Blood Bank reports') ->assertSee('Requests today'); $this->actingAs($this->owner) ->get(route('care.specialty.blood-bank.print', $this->visit)) ->assertOk() ->assertSee('Blood Bank summary') ->assertSee($this->patient->fullName()); } public function test_blood_bank_manager_can_manage_admin_inventory(): void { Member::query()->where('user_ref', $this->owner->public_id)->update([ 'role' => 'blood_bank_manager', 'branch_id' => $this->branch->id, ]); $this->actingAs($this->owner) ->get(route('care.blood-bank.admin.index', ['branch_id' => $this->branch->id])) ->assertOk() ->assertSee('Blood Bank admin') ->assertSee('Operational inventory'); $this->actingAs($this->owner) ->post(route('care.blood-bank.admin.stock.update'), [ 'branch_id' => $this->branch->id, 'stock' => [ [ 'product_code' => 'packed_rbc', 'units_on_hand' => 12, 'reorder_level' => 3, 'notes' => 'Restocked', ], [ 'product_code' => 'whole_blood', 'units_on_hand' => 1, 'reorder_level' => 2, 'notes' => null, ], [ 'product_code' => 'platelets', 'units_on_hand' => 4, 'reorder_level' => 2, ], [ 'product_code' => 'ffp', 'units_on_hand' => 5, 'reorder_level' => 2, ], [ 'product_code' => 'cryo', 'units_on_hand' => 2, 'reorder_level' => 1, ], ], ]) ->assertRedirect(); $this->assertDatabaseHas('care_blood_bank_stock', [ 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'product_code' => 'packed_rbc', 'units_on_hand' => 12, 'reorder_level' => 3, ]); $this->actingAs($this->owner) ->get(route('care.specialty.show', 'blood_bank')) ->assertOk(); $this->actingAs($this->owner) ->get(route('care.specialty.blood-bank.reports')) ->assertOk() ->assertSee('Blood Bank reports'); } public function test_lab_technician_cannot_access_blood_bank_admin(): void { Member::query()->where('user_ref', $this->owner->public_id)->update([ 'role' => 'lab_technician', 'branch_id' => $this->branch->id, ]); $this->actingAs($this->owner) ->get(route('care.blood-bank.admin.index')) ->assertForbidden(); } }