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>
115 lines
4.9 KiB
PHP
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),
|
|
]);
|
|
}
|
|
}
|
|
}
|