authorizeAbility($request, 'bills.view'); $organization = $this->organization($request); abort_unless($this->features->enabled($organization, CareFeatures::WORKFLOW_ENGINE), 404); $member = $this->member($request); $permissions = app(CarePermissions::class); $branchScope = app(OrganizationResolver::class)->branchScope($member); $branchId = $this->branchContext->resolve($request, $organization, $member); $status = (string) $request->query('status', FinancialObligation::STATUS_PENDING); $obligations = FinancialObligation::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->when($branchScope, fn ($query) => $query->where('branch_id', $branchScope)) ->when($status !== '', fn ($query) => $query->where('status', $status)) ->with(['patient', 'branch', 'stage', 'visit', 'bill']) ->latest('created_at') ->paginate(20) ->withQueryString(); return view('care.financial-obligations.index', [ 'obligations' => $obligations, 'statuses' => config('care_workflows.obligation_statuses', []), 'clearanceMethods' => config('care_workflows.clearance_methods', []), 'paymentModes' => config('care_workflows.payment_modes', []), 'paymentMethods' => collect(config('care.payment_methods', []))->except(['gateway'])->all(), 'canClear' => $permissions->can($member, 'payments.manage'), 'branchId' => $branchId, 'queueIntegration' => $branchId && $permissions->handlesFloorCare($member) ? $this->queueBridge->listMeta($organization, CareQueueContexts::BILLING, $branchId) : ['enabled' => false], 'canManageQueue' => $permissions->can($member, 'service_queues.console') && $permissions->handlesFloorCare($member), ]); } public function clear(Request $request, FinancialObligation $financialObligation): RedirectResponse { $this->authorizeAbility($request, 'payments.manage'); $this->authorizeObligation($request, $financialObligation); if ($financialObligation->status !== FinancialObligation::STATUS_PENDING) { return back()->with('info', 'This financial obligation is no longer pending.'); } $manualPaymentMethods = array_keys( collect(config('care.payment_methods', []))->except(['gateway'])->all() ); $validated = $request->validate([ 'method' => ['required', Rule::in(array_keys(config('care_workflows.clearance_methods', [])))], 'payment_mode' => ['nullable', Rule::in(array_keys(config('care_workflows.payment_modes', [])))], 'payment_method' => ['nullable', Rule::in($manualPaymentMethods)], 'reference' => ['nullable', 'string', 'max:100'], ]); $stage = $financialObligation->stage; $method = (string) $validated['method']; $paymentMode = $validated['payment_mode'] ?? null; $reference = trim((string) ($validated['reference'] ?? '')) ?: null; $this->validateClearanceRules($stage, $method, $paymentMode, $reference); if ($method === 'bank_receipt') { $paymentMode = 'external_bank'; } $settled = DB::transaction(function () use ($financialObligation, $validated, $method, $paymentMode, $reference, $request) { $locked = FinancialObligation::query()->lockForUpdate()->findOrFail($financialObligation->id); if ($locked->status !== FinancialObligation::STATUS_PENDING) { return $locked; } if (in_array($method, ['payment', 'bank_receipt'], true) && $locked->amount_minor > 0) { $bill = $this->bills->billForObligation( $locked, $this->ownerRef($request), $this->ownerRef($request), ); $this->bills->recordPayment($bill, $this->ownerRef($request), [ 'amount_minor' => $locked->amount_minor, 'method' => $method === 'bank_receipt' ? Payment::METHOD_OTHER : ($validated['payment_method'] ?? Payment::METHOD_CASH), 'reference' => $reference, 'notes' => 'Workflow financial obligation '.$locked->uuid, ], $this->ownerRef($request)); } $this->gates->clear( $locked, $method, $this->ownerRef($request), $paymentMode, $reference, ); AuditLogger::record( $this->ownerRef($request), 'financial_obligation.cleared', $locked->organization_id, $this->ownerRef($request), FinancialObligation::class, $locked->id, ['method' => $method, 'amount_minor' => $locked->amount_minor], ); return $locked->fresh(); }); $organization = $this->organization($request); $visit = $settled->visit; if ($visit) { $advance = $this->workflows->refreshGate($visit); if ($advance?->stage?->type === 'payment') { $this->workflowCompletion->complete( $organization, $visit, CareQueueContexts::BILLING, $this->ownerRef($request), $this->ownerRef($request), ); } else { $this->queueRelease->releaseCurrent($organization, $visit); } } return back()->with('success', 'Financial obligation cleared. The patient may now continue.'); } protected function validateClearanceRules(?WorkflowStage $stage, string $method, ?string $paymentMode, ?string $reference): void { if ($method === 'insurance' && ! $stage?->insurance_eligible) { throw ValidationException::withMessages(['method' => 'Insurance is not allowed for this stage.']); } if ($method === 'credit' && ! $stage?->credit_allowed) { throw ValidationException::withMessages(['method' => 'Credit is not allowed for this stage.']); } if (in_array($method, ['waiver', 'override'], true) && ! $stage?->allow_override) { throw ValidationException::withMessages(['method' => 'Waivers and overrides are not allowed for this stage.']); } if ($method === 'bank_receipt' && $reference === null) { throw ValidationException::withMessages(['reference' => 'Enter the external bank receipt or slip number.']); } if ($method === 'bank_receipt' && $stage && ! in_array('external_bank', (array) $stage->payment_modes, true)) { throw ValidationException::withMessages(['method' => 'External bank receipts are not allowed for this stage.']); } if ($method === 'payment') { $allowedModes = (array) ($stage?->payment_modes ?? []); if ($paymentMode === null || ($allowedModes !== [] && ! in_array($paymentMode, $allowedModes, true))) { throw ValidationException::withMessages(['payment_mode' => 'Select a payment mode allowed for this stage.']); } if ($paymentMode === 'external_bank') { throw ValidationException::withMessages(['method' => 'Use external bank receipt verification for bank payments.']); } } } protected function authorizeObligation(Request $request, FinancialObligation $obligation): void { $this->authorizeOwner($request, $obligation); abort_unless($obligation->organization_id === $this->organization($request)->id, 404); $branchId = app(OrganizationResolver::class)->branchScope($this->member($request)); if ($branchId !== null && $obligation->branch_id !== $branchId) { abort(404); } } }