Files
ladill-care/tests/Feature/CareWorkflowEngineTest.php
T
isaaccladandCursor 3ee59a0956
Deploy Ladill Care / deploy (push) Failing after 1m9s
Activate Care workflows across check-in and financial clearance.
Enforce service-queue gates, cashier settlements, and clinical stage progression so patient journeys cannot bypass configured payment or authorization rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 21:13:06 +00:00

221 lines
8.2 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Branch;
use App\Models\FinancialObligation;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\User;
use App\Models\Visit;
use App\Models\VisitStageAdvance;
use App\Services\Care\CareFeatures;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\Workflow\FinancialGateService;
use App\Services\Care\Workflow\WorkflowEngine;
use App\Services\Care\Workflow\WorkflowTemplateInstaller;
use App\Services\Care\Workflow\WorkflowTemplateRegistry;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CareWorkflowEngineTest extends TestCase
{
use RefreshDatabase;
protected User $owner;
protected Organization $organization;
protected Branch $branch;
protected function setUp(): void
{
parent::setUp();
$this->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',
]);
}
}