Activate Care workflows across check-in and financial clearance.
Deploy Ladill Care / deploy (push) Failing after 1m9s
Deploy Ladill Care / deploy (push) Failing after 1m9s
Enforce service-queue gates, cashier settlements, and clinical stage progression so patient journeys cannot bypass configured payment or authorization rules. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,8 +3,12 @@
|
||||
namespace App\Services\Care\Workflow;
|
||||
|
||||
use App\Models\FinancialObligation;
|
||||
use App\Models\InvestigationRequest;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Prescription;
|
||||
use App\Models\Visit;
|
||||
use App\Models\WorkflowStage;
|
||||
use App\Services\Care\CareFeatures;
|
||||
|
||||
/**
|
||||
* Owns FinancialObligation lifecycle and answers whether a stage's financial
|
||||
@@ -16,6 +20,10 @@ use App\Models\WorkflowStage;
|
||||
*/
|
||||
class FinancialGateService
|
||||
{
|
||||
public function __construct(
|
||||
protected CareFeatures $features,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create (or return existing) obligation for a visit + stage. Only stages
|
||||
* that require payment produce an obligation.
|
||||
@@ -28,29 +36,51 @@ class FinancialGateService
|
||||
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;
|
||||
$organization = Organization::query()->find($visit->organization_id);
|
||||
if ($organization && ! $this->features->enabled($organization, CareFeatures::FINANCIAL_GATES)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FinancialObligation::create(array_merge([
|
||||
$amount = (int) ($overrides['amount_minor']
|
||||
?? $stage->default_amount_minor
|
||||
?? $this->defaultAmountFor($stage, $visit));
|
||||
$defaults = 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));
|
||||
'amount_minor' => $amount,
|
||||
'status' => $amount > 0
|
||||
? FinancialObligation::STATUS_PENDING
|
||||
: FinancialObligation::STATUS_WAIVED,
|
||||
'clearance_method' => $amount > 0 ? null : 'no_charge',
|
||||
'cleared_at' => $amount > 0 ? null : now(),
|
||||
], $overrides);
|
||||
|
||||
$obligation = FinancialObligation::firstOrCreate(
|
||||
[
|
||||
'visit_id' => $visit->id,
|
||||
'workflow_stage_id' => $stage->id,
|
||||
],
|
||||
$defaults,
|
||||
);
|
||||
|
||||
// A stage may initially have no billable items (for example pharmacy
|
||||
// before a prescription is saved). Re-evaluate no-charge clearances
|
||||
// whenever the workflow gate is refreshed.
|
||||
if ($obligation->clearance_method === 'no_charge' && $amount > 0) {
|
||||
$obligation->forceFill([
|
||||
'amount_minor' => $amount,
|
||||
'status' => FinancialObligation::STATUS_PENDING,
|
||||
'clearance_method' => null,
|
||||
'cleared_at' => null,
|
||||
])->save();
|
||||
}
|
||||
|
||||
return $obligation;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,6 +93,11 @@ class FinancialGateService
|
||||
return true;
|
||||
}
|
||||
|
||||
$organization = Organization::query()->find($visit->organization_id);
|
||||
if ($organization && ! $this->features->enabled($organization, CareFeatures::FINANCIAL_GATES)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$obligation = FinancialObligation::query()
|
||||
->where('visit_id', $visit->id)
|
||||
->where('workflow_stage_id', $stage->id)
|
||||
@@ -93,7 +128,7 @@ class FinancialGateService
|
||||
): FinancialObligation {
|
||||
$status = match ($method) {
|
||||
'insurance' => FinancialObligation::STATUS_AUTHORIZED,
|
||||
'waiver' => FinancialObligation::STATUS_WAIVED,
|
||||
'waiver', 'override' => FinancialObligation::STATUS_WAIVED,
|
||||
'credit' => FinancialObligation::STATUS_DEFERRED,
|
||||
default => FinancialObligation::STATUS_PAID,
|
||||
};
|
||||
@@ -117,12 +152,53 @@ class FinancialGateService
|
||||
return $obligation;
|
||||
}
|
||||
|
||||
protected function defaultAmountFor(WorkflowStage $stage): int
|
||||
protected function defaultAmountFor(WorkflowStage $stage, Visit $visit): int
|
||||
{
|
||||
if ($stage->charge_code === 'consultation') {
|
||||
return (int) config('care.billing.consultation_fee_minor', 0);
|
||||
}
|
||||
return match ($stage->charge_code) {
|
||||
'patient_card', 'registration' => (int) config('care.billing.patient_card_fee_minor', 0),
|
||||
'consultation' => (int) config('care.billing.consultation_fee_minor', 0),
|
||||
'lab' => $this->investigationTotal($visit, imaging: false),
|
||||
'imaging' => $this->investigationTotal($visit, imaging: true),
|
||||
'pharmacy' => $this->pharmacyTotal($visit),
|
||||
'visit_total' => (int) $visit->bills()
|
||||
->whereNotIn('status', ['paid', 'void'])
|
||||
->sum('balance_minor'),
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
|
||||
return 0;
|
||||
protected function investigationTotal(Visit $visit, bool $imaging): int
|
||||
{
|
||||
$imagingCategories = ['xray', 'ultrasound', 'ct', 'mri'];
|
||||
|
||||
return (int) InvestigationRequest::query()
|
||||
->where('visit_id', $visit->id)
|
||||
->with('investigationType')
|
||||
->get()
|
||||
->filter(function (InvestigationRequest $request) use ($imaging, $imagingCategories) {
|
||||
$isImaging = in_array($request->investigationType?->category, $imagingCategories, true);
|
||||
|
||||
return $imaging ? $isImaging : ! $isImaging;
|
||||
})
|
||||
->sum(fn (InvestigationRequest $request) => (int) ($request->investigationType?->price_minor ?? 0));
|
||||
}
|
||||
|
||||
protected function pharmacyTotal(Visit $visit): int
|
||||
{
|
||||
return (int) Prescription::query()
|
||||
->where('visit_id', $visit->id)
|
||||
->with('items.drug')
|
||||
->get()
|
||||
->sum(function (Prescription $prescription) {
|
||||
return $prescription->items->sum(function ($item) {
|
||||
if ($item->is_procedure) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$quantity = max(1, (int) preg_replace('/\D/', '', (string) $item->quantity) ?: 1);
|
||||
|
||||
return $quantity * (int) ($item->drug?->unit_price_minor ?? 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user