where('organization_id', $visit->organization_id) ->where('is_active', true) ->where(function ($q) use ($visit) { $q->where('branch_id', $visit->branch_id)->orWhereNull('branch_id'); }) ->orderByRaw('branch_id is null') ->with('stages') ->first(); } /** * Place the visit on the workflow's first stage. Idempotent — returns the * existing active advance if the visit is already started. */ public function start(Visit $visit, ?FacilityWorkflow $workflow = null): ?VisitStageAdvance { $workflow ??= $this->workflowFor($visit); if ($workflow === null) { return null; } $existing = $this->currentAdvance($visit); if ($existing !== null) { return $existing; } $first = $workflow->firstStage(); if ($first === null) { return null; } return $this->enterStage($visit, $workflow, $first); } public function currentAdvance(Visit $visit): ?VisitStageAdvance { return VisitStageAdvance::query() ->where('visit_id', $visit->id) ->whereIn('status', [VisitStageAdvance::STATUS_ACTIVE, VisitStageAdvance::STATUS_BLOCKED]) ->orderByDesc('id') ->first(); } public function currentStage(Visit $visit): ?WorkflowStage { return $this->currentAdvance($visit)?->stage; } /** * Whether the visit may advance to $toCode (or the current stage's * default_next when omitted). Blocked when the target stage has an unmet * "before" financial gate. */ public function canAdvance(Visit $visit, ?string $toCode = null): bool { $target = $this->resolveTarget($visit, $toCode); return $target !== null && $this->gate->isSatisfied($visit, $target); } /** * Advance the visit to the next stage. Completes the current advance and * enters the target stage; if the target is gated and unpaid the new * advance is created in the "blocked" state (visit is not queued until the * obligation clears). */ public function advance(Visit $visit, ?string $toCode = null): ?VisitStageAdvance { return DB::transaction(function () use ($visit, $toCode) { $workflow = $this->workflowFor($visit); if ($workflow === null) { return null; } $target = $this->resolveTarget($visit, $toCode, $workflow); if ($target === null) { return null; } $current = $this->currentAdvance($visit); if ($current !== null) { $current->forceFill([ 'status' => VisitStageAdvance::STATUS_COMPLETED, 'completed_at' => now(), ])->save(); } return $this->enterStage($visit, $workflow, $target); }); } /** * Whether the current stage may release the patient into its Queue service * point. False while a "before" gate is unmet. */ public function isQueueReleasable(Visit $visit): bool { $advance = $this->currentAdvance($visit); if ($advance === null || $advance->stage === null) { return true; } return $this->gate->isSatisfied($visit, $advance->stage); } /** * Re-evaluate the current advance after an obligation changes. Moves a * blocked advance to active once its gate clears. */ public function refreshGate(Visit $visit): ?VisitStageAdvance { $advance = $this->currentAdvance($visit); if ($advance === null || $advance->stage === null) { return $advance; } if ($advance->isBlocked() && $this->gate->isSatisfied($visit, $advance->stage)) { $advance->forceFill([ 'status' => VisitStageAdvance::STATUS_ACTIVE, 'blocked_reason' => null, 'cleared_at' => now(), ])->save(); } return $advance->fresh(); } protected function enterStage(Visit $visit, FacilityWorkflow $workflow, WorkflowStage $stage): VisitStageAdvance { $this->gate->obligationFor($visit, $stage); $satisfied = $this->gate->isSatisfied($visit, $stage); return VisitStageAdvance::create([ 'owner_ref' => $visit->owner_ref, 'visit_id' => $visit->id, 'workflow_id' => $workflow->id, 'workflow_stage_id' => $stage->id, 'stage_code' => $stage->code, 'status' => $satisfied ? VisitStageAdvance::STATUS_ACTIVE : VisitStageAdvance::STATUS_BLOCKED, 'blocked_reason' => $satisfied ? null : $this->gate->blockedReason($visit, $stage), 'entered_at' => now(), 'cleared_at' => $satisfied ? now() : null, ]); } protected function resolveTarget(Visit $visit, ?string $toCode, ?FacilityWorkflow $workflow = null): ?WorkflowStage { $workflow ??= $this->workflowFor($visit); if ($workflow === null) { return null; } if ($toCode !== null && $toCode !== '') { return $workflow->stage($toCode); } $current = $this->currentAdvance($visit)?->stage; $nextCode = $current?->default_next; return $nextCode ? $workflow->stage($nextCode) : null; } }