Files
ladill-care/app/Services/Care/Workflow/WorkflowTemplateInstaller.php
T
isaaccladandCursor 86bfce1e17
Deploy Ladill Care / deploy (push) Failing after 45s
Add workflow-centric patient journey with financial gates.
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>
2026-07-17 20:47:33 +00:00

115 lines
4.9 KiB
PHP

<?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),
]);
}
}
}