withoutMiddleware(EnsurePlatformSession::class); $this->owner = User::create([ 'public_id' => 'care-workflow-owner', 'name' => 'Owner', 'email' => 'workflow-engine@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Gate Hospital', 'slug' => 'gate-hospital', 'settings' => [ 'onboarded' => true, 'facility_category' => 'hospital', 'rollout' => [ CareFeatures::WORKFLOW_ENGINE => true, CareFeatures::FINANCIAL_GATES => true, ], ], ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, 'role' => 'hospital_admin', ]); $this->branch = Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'Main', 'is_active' => true, ]); } protected function makeVisit(): Visit { $patient = Patient::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_number' => 'LC-0001', 'first_name' => 'Ama', 'last_name' => 'Mensah', ]); return Visit::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'branch_id' => $this->branch->id, 'patient_id' => $patient->id, 'status' => Visit::STATUS_OPEN, 'checked_in_at' => now(), ]); } public function test_installer_materializes_template_stages(): void { $workflow = app(WorkflowTemplateInstaller::class) ->install($this->organization, $this->branch, 'public_hospital_gh'); $this->assertSame('public_hospital_gh', $workflow->template_key); $this->assertTrue((bool) $workflow->stages()->where('code', 'consultation')->value('requires_payment')); $this->assertGreaterThanOrEqual(6, $workflow->stages()->count()); } public function test_herbal_hospital_template_is_registered_and_installable(): void { $registry = app(WorkflowTemplateRegistry::class); $this->assertTrue($registry->hasTemplate('herbal_hospital')); $this->assertTrue($registry->hasTemplate('herbal_clinic')); $this->assertArrayHasKey('herbal_hospital', $registry->categories()); $workflow = app(WorkflowTemplateInstaller::class) ->install($this->organization, $this->branch, 'herbal_hospital'); $this->assertNotNull($workflow->stage('dispensing')); $consult = $workflow->stage('consultation'); $this->assertTrue($consult->requires_payment); $this->assertFalse($consult->insurance_eligible); } public function test_gate_blocks_stage_until_obligation_cleared(): void { app(WorkflowTemplateInstaller::class) ->install($this->organization, $this->branch, 'public_hospital_gh'); $engine = app(WorkflowEngine::class); $visit = $this->makeVisit(); $engine->start($visit); // Registration is a "before" gate → visit starts blocked, not queueable. $this->assertFalse($engine->isQueueReleasable($visit)); $advance = $engine->currentAdvance($visit); $this->assertSame('registration', $advance->stage_code); $this->assertSame(VisitStageAdvance::STATUS_BLOCKED, $advance->status); $obligation = FinancialObligation::query() ->where('visit_id', $visit->id) ->where('stage_code', 'registration') ->first(); $this->assertNotNull($obligation); $this->assertSame(FinancialObligation::STATUS_PENDING, $obligation->status); app(FinancialGateService::class)->clear($obligation, 'cash', $this->owner->public_id, 'internal_cashier'); $engine->refreshGate($visit); $this->assertTrue($engine->isQueueReleasable($visit)); $this->assertSame(VisitStageAdvance::STATUS_ACTIVE, $engine->currentAdvance($visit)->fresh()->status); } public function test_advance_follows_default_next_and_blocks_on_next_gate(): void { app(WorkflowTemplateInstaller::class) ->install($this->organization, $this->branch, 'public_hospital_gh'); $engine = app(WorkflowEngine::class); $gate = app(FinancialGateService::class); $visit = $this->makeVisit(); $engine->start($visit); $reg = FinancialObligation::query()->where('visit_id', $visit->id)->where('stage_code', 'registration')->first(); $gate->clear($reg, 'cash', $this->owner->public_id); $engine->refreshGate($visit); // registration → triage (no gate) → advance again → consultation (gated) $engine->advance($visit); // to triage $this->assertSame('triage', $engine->currentAdvance($visit)->stage_code); $this->assertTrue($engine->isQueueReleasable($visit)); $engine->advance($visit); // to consultation, gated $advance = $engine->currentAdvance($visit); $this->assertSame('consultation', $advance->stage_code); $this->assertSame(VisitStageAdvance::STATUS_BLOCKED, $advance->status); // Pharmacy is also a "before" gate, so canAdvance is false until paid. $this->assertFalse($engine->canAdvance($visit, 'pharmacy')); } public function test_after_timing_stage_never_blocks_entry(): void { app(WorkflowTemplateInstaller::class) ->install($this->organization, $this->branch, 'cashless_clinic'); $engine = app(WorkflowEngine::class); $visit = $this->makeVisit(); $engine->start($visit); $this->assertSame('reception', $engine->currentAdvance($visit)->stage_code); $this->assertTrue($engine->isQueueReleasable($visit)); $engine->advance($visit); // consultation $engine->advance($visit); // pharmacy $engine->advance($visit); // checkout (after gate) — still enterable $this->assertSame('checkout', $engine->currentAdvance($visit)->stage_code); $this->assertTrue($engine->isQueueReleasable($visit)); } public function test_onboarding_stores_category_modules_and_installs_workflow(): void { $newOwner = User::create([ 'public_id' => 'onboard-owner', 'name' => 'B', 'email' => 'b@example.com', ]); $org = app(OrganizationResolver::class)->completeOnboarding($newOwner, [ 'organization_name' => 'Herbal Wellness', 'facility_category' => 'herbal_clinic', 'workflow_template' => 'herbal_clinic', 'branch_name' => 'Main', 'timezone' => 'UTC', ]); $this->assertSame('herbal_clinic', data_get($org->settings, 'facility_category')); $this->assertContains('pharmacy', data_get($org->settings, 'modules', [])); $this->assertDatabaseHas('care_facility_workflows', [ 'organization_id' => $org->id, 'template_key' => 'herbal_clinic', ]); } }