Add workflow-centric patient journey with financial gates.
Deploy Ladill Care / deploy (push) Failing after 45s
Deploy Ladill Care / deploy (push) Failing after 45s
Introduces a facility workflow engine as Care's primary configuration object: onboarding now leads with a facility category (which modules to enable) and a workflow template (how patients move + where money is collected), including standard herbal hospital/clinic templates. Templates materialize into care_facility_workflows/care_workflow_stages. The engine tracks a visit's position via care_visit_stage_advances and, with FinancialGateService, gates a stage's queue behind a cleared FinancialObligation (paid/authorized/waived/deferred) for "before" payment timing while leaving "after" (pay-at-exit) flows unblocked. Gated behavior is opt-in behind the workflow_engine/financial_gates rollout flags; legacy orgs keep current behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care\Workflow;
|
||||
|
||||
use App\Models\FinancialObligation;
|
||||
use App\Models\Visit;
|
||||
use App\Models\WorkflowStage;
|
||||
|
||||
/**
|
||||
* Owns FinancialObligation lifecycle and answers whether a stage's financial
|
||||
* gate is satisfied for a given visit.
|
||||
*
|
||||
* A "before" gate blocks entry to the stage's service queue until the stage's
|
||||
* obligation is cleared (paid / authorized / waived / deferred). An "after"
|
||||
* gate never blocks entry but is created so the balance is collected at exit.
|
||||
*/
|
||||
class FinancialGateService
|
||||
{
|
||||
/**
|
||||
* Create (or return existing) obligation for a visit + stage. Only stages
|
||||
* that require payment produce an obligation.
|
||||
*
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
public function obligationFor(Visit $visit, WorkflowStage $stage, array $overrides = []): ?FinancialObligation
|
||||
{
|
||||
if (! $stage->requires_payment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$existing = FinancialObligation::query()
|
||||
->where('visit_id', $visit->id)
|
||||
->where('workflow_stage_id', $stage->id)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($existing !== null) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
return FinancialObligation::create(array_merge([
|
||||
'owner_ref' => $visit->owner_ref,
|
||||
'organization_id' => $visit->organization_id,
|
||||
'branch_id' => $visit->branch_id,
|
||||
'visit_id' => $visit->id,
|
||||
'patient_id' => $visit->patient_id,
|
||||
'workflow_stage_id' => $stage->id,
|
||||
'stage_code' => $stage->code,
|
||||
'charge_code' => $stage->charge_code,
|
||||
'label' => $stage->charge_label ?? $stage->name,
|
||||
'amount_minor' => $stage->default_amount_minor ?? $this->defaultAmountFor($stage),
|
||||
'status' => FinancialObligation::STATUS_PENDING,
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the visit may enter the stage. Stages with a "before" payment
|
||||
* gate require a cleared obligation; everything else is allowed.
|
||||
*/
|
||||
public function isSatisfied(Visit $visit, WorkflowStage $stage): bool
|
||||
{
|
||||
if (! $stage->gatesBeforeService()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$obligation = FinancialObligation::query()
|
||||
->where('visit_id', $visit->id)
|
||||
->where('workflow_stage_id', $stage->id)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
return $obligation !== null && $obligation->isCleared();
|
||||
}
|
||||
|
||||
public function blockedReason(Visit $visit, WorkflowStage $stage): ?string
|
||||
{
|
||||
if ($this->isSatisfied($visit, $stage)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'awaiting_payment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear an obligation (paid / insurance authorized / waived / credit).
|
||||
*/
|
||||
public function clear(
|
||||
FinancialObligation $obligation,
|
||||
string $method,
|
||||
?string $actorRef = null,
|
||||
?string $paymentMode = null,
|
||||
?string $externalReference = null,
|
||||
): FinancialObligation {
|
||||
$status = match ($method) {
|
||||
'insurance' => FinancialObligation::STATUS_AUTHORIZED,
|
||||
'waiver' => FinancialObligation::STATUS_WAIVED,
|
||||
'credit' => FinancialObligation::STATUS_DEFERRED,
|
||||
default => FinancialObligation::STATUS_PAID,
|
||||
};
|
||||
|
||||
$obligation->forceFill([
|
||||
'status' => $status,
|
||||
'clearance_method' => $method,
|
||||
'payment_mode' => $paymentMode,
|
||||
'external_reference' => $externalReference,
|
||||
'cleared_by' => $actorRef,
|
||||
'cleared_at' => now(),
|
||||
])->save();
|
||||
|
||||
return $obligation;
|
||||
}
|
||||
|
||||
public function void(FinancialObligation $obligation): FinancialObligation
|
||||
{
|
||||
$obligation->forceFill(['status' => FinancialObligation::STATUS_VOID])->save();
|
||||
|
||||
return $obligation;
|
||||
}
|
||||
|
||||
protected function defaultAmountFor(WorkflowStage $stage): int
|
||||
{
|
||||
if ($stage->charge_code === 'consultation') {
|
||||
return (int) config('care.billing.consultation_fee_minor', 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user