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,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care\Workflow;
|
||||
|
||||
use App\Models\Branch;
|
||||
use App\Models\FacilityWorkflow;
|
||||
use App\Models\Organization;
|
||||
use App\Models\WorkflowStage;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Materializes a workflow template from config into a FacilityWorkflow +
|
||||
* WorkflowStage graph for an organization. Idempotent per (org, branch):
|
||||
* re-installing replaces the workflow's stages with the template definition.
|
||||
*/
|
||||
class WorkflowTemplateInstaller
|
||||
{
|
||||
public function __construct(
|
||||
protected WorkflowTemplateRegistry $registry,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array{name?: string, category?: string, activate?: bool, replace?: bool} $options
|
||||
*/
|
||||
public function install(Organization $organization, ?Branch $branch, string $templateKey, array $options = []): FacilityWorkflow
|
||||
{
|
||||
$template = $this->registry->template($templateKey);
|
||||
if ($template === null) {
|
||||
throw new \InvalidArgumentException("Unknown workflow template [{$templateKey}].");
|
||||
}
|
||||
|
||||
$ownerRef = (string) $organization->owner_ref;
|
||||
$activate = (bool) ($options['activate'] ?? true);
|
||||
$replace = (bool) ($options['replace'] ?? true);
|
||||
$name = (string) ($options['name'] ?? ($template['label'] ?? $templateKey));
|
||||
$category = $options['category'] ?? data_get($organization->settings, 'facility_category');
|
||||
|
||||
return DB::transaction(function () use ($organization, $branch, $template, $templateKey, $ownerRef, $activate, $replace, $name, $category) {
|
||||
$query = FacilityWorkflow::query()
|
||||
->where('organization_id', $organization->id)
|
||||
->where('branch_id', $branch?->id);
|
||||
|
||||
$workflow = $query->first();
|
||||
|
||||
if ($workflow === null) {
|
||||
$workflow = FacilityWorkflow::create([
|
||||
'owner_ref' => $ownerRef,
|
||||
'organization_id' => $organization->id,
|
||||
'branch_id' => $branch?->id,
|
||||
'name' => $name,
|
||||
'template_key' => $templateKey,
|
||||
'category' => $category,
|
||||
'is_active' => $activate,
|
||||
'settings' => ['description' => $template['description'] ?? null],
|
||||
]);
|
||||
} else {
|
||||
$workflow->forceFill([
|
||||
'name' => $name,
|
||||
'template_key' => $templateKey,
|
||||
'category' => $category,
|
||||
'is_active' => $activate ? true : $workflow->is_active,
|
||||
'settings' => array_merge($workflow->settings ?? [], [
|
||||
'description' => $template['description'] ?? null,
|
||||
]),
|
||||
])->save();
|
||||
|
||||
if ($replace) {
|
||||
WorkflowStage::query()->where('workflow_id', $workflow->id)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
if ($replace || $workflow->stages()->count() === 0) {
|
||||
$this->syncStages($workflow, $ownerRef, (array) ($template['stages'] ?? []));
|
||||
}
|
||||
|
||||
return $workflow->fresh(['stages']) ?? $workflow;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $stages
|
||||
*/
|
||||
protected function syncStages(FacilityWorkflow $workflow, string $ownerRef, array $stages): void
|
||||
{
|
||||
$order = 0;
|
||||
foreach ($stages as $stage) {
|
||||
$order += 10;
|
||||
WorkflowStage::create([
|
||||
'owner_ref' => $ownerRef,
|
||||
'workflow_id' => $workflow->id,
|
||||
'code' => (string) $stage['code'],
|
||||
'name' => (string) ($stage['name'] ?? $stage['code']),
|
||||
'type' => (string) ($stage['type'] ?? 'custom'),
|
||||
'queue_context' => $stage['queue_context'] ?? null,
|
||||
'department_type' => $stage['department_type'] ?? null,
|
||||
'sort_order' => $stage['sort_order'] ?? $order,
|
||||
'requires_payment' => (bool) ($stage['requires_payment'] ?? false),
|
||||
'payment_timing' => (string) ($stage['payment_timing'] ?? 'none'),
|
||||
'payment_modes' => $stage['payment_modes'] ?? ['internal_cashier', 'digital'],
|
||||
'insurance_eligible' => (bool) ($stage['insurance_eligible'] ?? false),
|
||||
'credit_allowed' => (bool) ($stage['credit_allowed'] ?? false),
|
||||
'allow_override' => (bool) ($stage['allow_override'] ?? true),
|
||||
'charge_code' => $stage['charge_code'] ?? null,
|
||||
'charge_label' => $stage['charge_label'] ?? null,
|
||||
'default_amount_minor' => $stage['default_amount_minor'] ?? null,
|
||||
'default_next' => $stage['default_next'] ?? null,
|
||||
'can_return' => (bool) ($stage['can_return'] ?? false),
|
||||
'can_skip' => (bool) ($stage['can_skip'] ?? false),
|
||||
'optional' => (bool) ($stage['optional'] ?? false),
|
||||
'creates_child' => (bool) ($stage['creates_child'] ?? false),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user