$overrides */ public function obligationFor(Visit $visit, WorkflowStage $stage, array $overrides = []): ?FinancialObligation { if (! $stage->requires_payment) { return null; } $organization = Organization::query()->find($visit->organization_id); if ($organization && ! $this->features->enabled($organization, CareFeatures::FINANCIAL_GATES)) { return null; } $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, 'patient_id' => $visit->patient_id, 'stage_code' => $stage->code, 'charge_code' => $stage->charge_code, 'label' => $stage->charge_label ?? $stage->name, '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; } /** * 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; } $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) ->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', 'override' => 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, Visit $visit): int { $org = Organization::query()->find($visit->organization_id); $pricing = app(CarePricingService::class); return match ($stage->charge_code) { 'patient_card', 'registration' => $org ? $pricing->patientCardFee($org) : (int) config('care.billing.patient_card_fee_minor', 0), 'consultation' => $org ? $pricing->consultationFee($org) : (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, }; } 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); }); }); } }