Add Paystack, Flutterwave, and Hubtel for encounter billing.
Deploy Ladill Care / deploy (push) Successful in 1m0s

Clinics on Pro/Enterprise can connect their own gateway in Integrations and collect card or MoMo on bills with 0% Ladill fee, while cash payments and balance totals stay correct for pending checkouts.
This commit is contained in:
isaacclad
2026-07-16 10:24:28 +00:00
parent 068c5aa63f
commit 7a0a7fcb88
19 changed files with 1283 additions and 31 deletions
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Confirming payment</title>
<style>
body { font-family: system-ui, sans-serif; background: #f8fafc; color: #0f172a; display: grid; place-items: center; min-height: 100vh; margin: 0; }
p { font-size: 0.95rem; color: #475569; }
</style>
</head>
<body>
<p>Confirming payment…</p>
<script>
(function () {
var url = @json($redirect);
try {
if (window.top && window.top !== window.self) {
window.top.location.replace(url);
return;
}
} catch (e) {}
window.location.replace(url);
})();
</script>
</body>
</html>
+37 -2
View File
@@ -81,17 +81,52 @@
<form method="POST" action="{{ route('care.bills.payments.store', $bill) }}" class="mt-4 space-y-3">
@csrf
<input type="number" name="amount_minor" value="{{ $bill->balance_minor }}" min="1" required class="w-full rounded-lg border-slate-300 text-sm">
<select name="method" class="w-full rounded-lg border-slate-300 text-sm">@foreach ($paymentMethods as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach</select>
<select name="method" class="w-full rounded-lg border-slate-300 text-sm">@foreach ($manualPaymentMethods as $k => $v)<option value="{{ $k }}">{{ $v }}</option>@endforeach</select>
<input type="text" name="reference" placeholder="Reference" class="w-full rounded-lg border-slate-300 text-sm">
<button type="submit" class="btn-primary w-full">Record payment</button>
</form>
</section>
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase text-slate-500">Collect online</h2>
<p class="mt-1 text-xs text-slate-500">
Card or mobile money via your connected gateway
@if ($gatewayConfigured && $gatewayProvider)
({{ $gatewayLabels[$gatewayProvider] ?? ucfirst($gatewayProvider) }})
@endif
0% Ladill fee.
</p>
@if ($gatewayConfigured)
<form method="POST" action="{{ route('care.bills.payments.gateway', $bill) }}" class="mt-4 space-y-3">
@csrf
<input type="number" name="amount_minor" value="{{ $bill->balance_minor }}" min="1" max="{{ $bill->balance_minor }}" required class="w-full rounded-lg border-slate-300 text-sm" aria-label="Amount (minor units)">
<button type="submit" class="btn-primary w-full">Open checkout</button>
</form>
@else
<p class="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Connect Paystack, Flutterwave, or Hubtel in
<a href="{{ route('care.integrations') }}" class="font-medium underline">Integrations</a>
to collect online payments.
</p>
@endif
</section>
@endif
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase text-slate-500">Payments</h2>
@forelse ($bill->payments as $payment)
<p class="mt-2 text-sm">{{ $money($payment->amount_minor) }} · {{ $paymentMethods[$payment->method] ?? $payment->method }} · {{ $payment->paid_at->format('d M Y') }}</p>
@php
$methodLabel = $payment->method === \App\Models\Payment::METHOD_GATEWAY
? ($gatewayLabels[$payment->gateway_provider] ?? 'Online')
: ($paymentMethods[$payment->method] ?? $payment->method);
$statusSuffix = $payment->status === \App\Models\Payment::STATUS_PAID
? ''
: ' · '.ucfirst($payment->status);
$when = $payment->isPaid()
? ($payment->paid_at?->format('d M Y') ?? $payment->created_at->format('d M Y'))
: $payment->created_at->format('d M Y');
@endphp
<p class="mt-2 text-sm">{{ $money($payment->amount_minor) }} · {{ $methodLabel }}{{ $statusSuffix }} · {{ $when }}@if($payment->reference)<span class="text-slate-400"> · {{ $payment->reference }}</span>@endif</p>
@empty
<p class="mt-2 text-sm text-slate-400">No payments recorded.</p>
@endforelse
@@ -1,9 +1,10 @@
<x-app-layout title="Integrations">
<x-settings.page title="Messaging integrations">
<x-settings.page title="Integrations">
<p class="text-sm text-slate-600">
Configure <a href="https://sms.ladill.com" target="_blank" rel="noopener" class="text-teal-700 underline">SMS</a>
Configure messaging and payment gateways for encounter billing.
Use <a href="https://sms.ladill.com" target="_blank" rel="noopener" class="text-teal-700 underline">SMS</a>
and <a href="https://bird.ladill.com" target="_blank" rel="noopener" class="text-teal-700 underline">email</a>
credentials for outbound messaging.
for outbound messages; connect Paystack, Flutterwave, or Hubtel so bill payments land in your account (0% Ladill fee).
</p>
@if (! $canManage)
@@ -124,6 +125,88 @@
@endif
@endif
</x-settings.card>
<x-settings.card title="Payment gateway" description="Encounter bill payments via your own Paystack, Flutterwave, or Hubtel account — 0% Ladill platform fee. Pro &amp; Enterprise only.">
@if ($canUsePaymentGateway ?? false)
@if ($gateway?->isConfigured())
<div class="mb-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">
Connected · {{ $gatewayLabels[$gateway->provider] ?? ucfirst($gateway->provider) }}
@if (! $gateway->is_active)
· <span class="font-medium text-amber-800">disabled</span>
@endif
</div>
@else
<div class="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Online checkout stays disabled until a gateway is connected.
</div>
@endif
@if ($canManage)
<form method="POST" action="{{ route('care.integrations.gateway.save') }}" class="space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700">Provider</label>
<select name="gateway_provider" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Select a provider</option>
@foreach ($gatewayLabels as $value => $label)
<option value="{{ $value }}" @selected(old('gateway_provider', $gateway?->provider) === $value)>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm font-medium text-slate-700">Public key / Merchant account</label>
<input type="text" name="gateway_public_key" value="{{ old('gateway_public_key') }}"
placeholder="{{ $gateway?->public_key ? '•••• saved — leave blank to keep' : 'pk_live_… or Hubtel account #' }}"
class="mt-1 w-full rounded-lg border-slate-300 font-mono text-sm" autocomplete="off">
<p class="mt-1 text-xs text-slate-500">Hubtel: merchant account number. Paystack/Flutterwave: public key.</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Secret / API key</label>
<input type="password" name="gateway_secret_key" value=""
placeholder="{{ $gateway?->secret_key ? '•••• saved — leave blank to keep' : 'sk_live_…' }}"
class="mt-1 w-full rounded-lg border-slate-300 font-mono text-sm" autocomplete="new-password">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Webhook secret / Hubtel client auth (optional)</label>
<input type="password" name="gateway_webhook_secret" value=""
placeholder="{{ $gateway?->webhook_secret ? '•••• saved — leave blank to keep' : 'Optional · Hubtel: clientId:clientSecret' }}"
class="mt-1 w-full rounded-lg border-slate-300 font-mono text-sm" autocomplete="new-password">
</div>
<label class="inline-flex items-center gap-2 text-sm text-slate-700">
<input type="hidden" name="gateway_is_active" value="0">
<input type="checkbox" name="gateway_is_active" value="1" @checked(old('gateway_is_active', $gateway?->is_active ?? true)) class="rounded border-slate-300 text-teal-600 focus:ring-teal-500">
Gateway enabled for encounter billing
</label>
<div class="flex flex-wrap gap-2">
<button type="submit" class="btn-primary">Save gateway</button>
</div>
</form>
@if ($gateway)
<div class="mt-3">
<x-confirm-dialog
name="disconnect-gateway"
title="Disconnect payment gateway?"
message="Online checkout for bills will stop until credentials are connected again."
:action="route('care.integrations.gateway.disconnect')"
confirm-label="Disconnect"
>
<x-slot:trigger>
<button type="button" class="text-sm text-rose-700 hover:underline">Disconnect</button>
</x-slot:trigger>
</x-confirm-dialog>
</div>
@endif
@endif
@else
<div class="rounded-lg border border-teal-100 bg-teal-50 px-4 py-3 text-sm text-teal-900">
<p class="font-medium">Your own payment gateway is a Pro feature</p>
<p class="mt-1 text-teal-800/80">Upgrade to Care Pro or Enterprise to connect Paystack, Flutterwave, or Hubtel for encounter bill payments with 0% Ladill fee.</p>
<a href="{{ route('care.pro.index') }}" class="mt-3 inline-flex text-sm font-semibold text-teal-700 hover:text-teal-900">View plans </a>
</div>
@endif
</x-settings.card>
</div>
<p class="mt-6 text-sm text-slate-500">
+1
View File
@@ -101,6 +101,7 @@
<li>Full clinical workflows</li>
<li>Laboratory & pharmacy</li>
<li>Encounter billing</li>
<li>Your own Paystack / Flutterwave / Hubtel</li>
<li>Ladill Queue integration</li>
<li>Unlimited branches (billed per branch)</li>
</ul>