Activate Care workflows across check-in and financial clearance.
Deploy Ladill Care / deploy (push) Failing after 1m9s

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>
This commit is contained in:
isaacclad
2026-07-17 21:13:06 +00:00
co-authored by Cursor
parent 86bfce1e17
commit 3ee59a0956
38 changed files with 1953 additions and 107 deletions
+67 -17
View File
@@ -40,22 +40,26 @@ class WorkflowEngine
*/
public function start(Visit $visit, ?FacilityWorkflow $workflow = null): ?VisitStageAdvance
{
$workflow ??= $this->workflowFor($visit);
if ($workflow === null) {
return null;
}
return DB::transaction(function () use ($visit, $workflow) {
Visit::query()->whereKey($visit->id)->lockForUpdate()->first();
$existing = $this->currentAdvance($visit);
if ($existing !== null) {
return $existing;
}
$workflow ??= $this->workflowFor($visit);
if ($workflow === null) {
return null;
}
$first = $workflow->firstStage();
if ($first === null) {
return null;
}
$existing = $this->currentAdvance($visit);
if ($existing !== null) {
return $existing;
}
return $this->enterStage($visit, $workflow, $first);
$first = $workflow->firstStage();
if ($first === null) {
return null;
}
return $this->enterStage($visit, $workflow, $first);
});
}
public function currentAdvance(Visit $visit): ?VisitStageAdvance
@@ -79,9 +83,31 @@ class WorkflowEngine
*/
public function canAdvance(Visit $visit, ?string $toCode = null): bool
{
$current = $this->refreshGate($visit);
if ($current === null || $current->isBlocked()) {
return false;
}
$target = $this->resolveTarget($visit, $toCode);
return $target !== null && $this->gate->isSatisfied($visit, $target);
return $target !== null;
}
public function canManuallyAdvance(Visit $visit): bool
{
$stage = $this->refreshGate($visit)?->stage;
if ($stage === null || in_array($stage->type, [
'consultation',
'laboratory',
'imaging',
'procedure',
'pharmacy',
'payment',
], true)) {
return false;
}
return $this->canAdvance($visit);
}
/**
@@ -93,6 +119,8 @@ class WorkflowEngine
public function advance(Visit $visit, ?string $toCode = null): ?VisitStageAdvance
{
return DB::transaction(function () use ($visit, $toCode) {
Visit::query()->whereKey($visit->id)->lockForUpdate()->first();
$workflow = $this->workflowFor($visit);
if ($workflow === null) {
return null;
@@ -103,7 +131,11 @@ class WorkflowEngine
return null;
}
$current = $this->currentAdvance($visit);
$current = $this->refreshGate($visit);
if ($current?->isBlocked()) {
throw new \DomainException('Clear the current stage financial gate before advancing this visit.');
}
if ($current !== null) {
$current->forceFill([
'status' => VisitStageAdvance::STATUS_COMPLETED,
@@ -140,12 +172,21 @@ class WorkflowEngine
return $advance;
}
if ($advance->isBlocked() && $this->gate->isSatisfied($visit, $advance->stage)) {
$this->gate->obligationFor($visit, $advance->stage);
$satisfied = $this->gate->isSatisfied($visit, $advance->stage);
if ($advance->isBlocked() && $satisfied) {
$advance->forceFill([
'status' => VisitStageAdvance::STATUS_ACTIVE,
'blocked_reason' => null,
'cleared_at' => now(),
])->save();
} elseif (! $advance->isBlocked() && ! $satisfied) {
$advance->forceFill([
'status' => VisitStageAdvance::STATUS_BLOCKED,
'blocked_reason' => $this->gate->blockedReason($visit, $advance->stage),
'cleared_at' => null,
])->save();
}
return $advance->fresh();
@@ -178,7 +219,16 @@ class WorkflowEngine
}
if ($toCode !== null && $toCode !== '') {
return $workflow->stage($toCode);
$target = $workflow->stage($toCode);
$current = $this->currentAdvance($visit)?->stage;
if ($target === null || $current === null) {
return null;
}
$isConfiguredNext = $current->default_next === $target->code;
$mayBranch = $current->creates_child || $current->can_return || $current->can_skip;
return $isConfiguredNext || $mayBranch ? $target : null;
}
$current = $this->currentAdvance($visit)?->stage;