diff --git a/app/Http/Controllers/Transfer/TransferController.php b/app/Http/Controllers/Transfer/TransferController.php
index 1c98199..2d13a19 100644
--- a/app/Http/Controllers/Transfer/TransferController.php
+++ b/app/Http/Controllers/Transfer/TransferController.php
@@ -4,10 +4,12 @@ namespace App\Http\Controllers\Transfer;
use App\Http\Controllers\Controller;
use App\Models\Transfer;
+use App\Services\Billing\BillingClient;
use App\Services\Qr\QrImageGeneratorService;
use App\Services\Qr\QrPdfExporter;
use App\Services\Transfer\TransferService;
use App\Services\Upload\ChunkedUploadService;
+use App\Support\TransferWalletErrors;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
@@ -59,12 +61,21 @@ class TransferController extends Controller
return view('transfer.transfers.index', compact('transfers', 'search', 'status', 'sort'));
}
- public function create(): View
+ public function create(BillingClient $billing): View
{
+ $account = ladill_account();
+ $balanceCedis = 0.0;
+ try {
+ $balanceCedis = $billing->balanceMinor((string) $account->public_id) / 100;
+ } catch (\Throwable) {
+ // Balance is informational on this page; ignore lookup failures.
+ }
+
return view('transfer.transfers.create', [
'maxFiles' => (int) config('transfer.max_files_per_transfer', 20),
'pricePerGb' => (float) config('transfer.price_per_gb_month', 0.30),
'gracePeriodDays' => (int) config('transfer.grace_period_days', 15),
+ 'balanceCedis' => $balanceCedis,
]);
}
@@ -108,7 +119,12 @@ class TransferController extends Controller
try {
$transfer = $this->transfers->create($account, $data, $files);
} catch (RuntimeException $e) {
- return back()->withInput()->with('error', $e->getMessage());
+ $redirect = back()->withInput()->with('error', $e->getMessage());
+ if (TransferWalletErrors::isInsufficientBalance($e)) {
+ $redirect->with('open_topup_modal', 'transfer');
+ }
+
+ return $redirect;
} finally {
foreach ($uploadIds as $uploadId) {
$this->chunkedUploads->cleanup($uploadId);
diff --git a/app/Http/Controllers/WalletTopupController.php b/app/Http/Controllers/WalletTopupController.php
new file mode 100644
index 0000000..e8e07e6
--- /dev/null
+++ b/app/Http/Controllers/WalletTopupController.php
@@ -0,0 +1,50 @@
+validate([
+ 'amount' => ['required', 'numeric', 'min:5', 'max:5000'],
+ 'return_url' => ['nullable', 'url', 'max:2048'],
+ ]);
+
+ $account = ladill_account();
+ if ($account === null) {
+ return $request->expectsJson()
+ ? response()->json(['error' => 'Unauthenticated.'], 401)
+ : redirect()->route('sso.connect');
+ }
+
+ $returnUrl = $validated['return_url'] ?? (string) (url()->previous() ?: route('transfer.transfers.create'));
+
+ try {
+ $checkoutUrl = $billing->topup((string) $account->public_id, (float) $validated['amount'], $returnUrl);
+ } catch (\Throwable) {
+ $checkoutUrl = '';
+ }
+
+ if ($checkoutUrl === '') {
+ return $request->expectsJson()
+ ? response()->json(['error' => 'Unable to start payment. Please try again.'], 422)
+ : back()->with('error', 'Unable to start payment. Please try again.');
+ }
+
+ return $request->expectsJson()
+ ? response()->json(['checkout_url' => $checkoutUrl])
+ : redirect()->away($checkoutUrl);
+ }
+}
diff --git a/app/Services/Billing/BillingClient.php b/app/Services/Billing/BillingClient.php
index 4c7caef..546fc33 100644
--- a/app/Services/Billing/BillingClient.php
+++ b/app/Services/Billing/BillingClient.php
@@ -35,6 +35,22 @@ class BillingClient
return (int) ($this->get('/balance', ['user' => $publicId])['balance_minor'] ?? 0);
}
+ /**
+ * Start a Paystack checkout to top up the central wallet; returns the
+ * checkout URL. Backs the in-app two-step "Add funds" modal.
+ */
+ public function topup(string $publicId, float $amount, string $returnUrl): string
+ {
+ $res = Http::withToken($this->token())->acceptJson()->timeout(15)->post($this->base().'/topup', [
+ 'user' => $publicId,
+ 'amount' => $amount,
+ 'return_url' => $returnUrl,
+ ]);
+ $res->throw();
+
+ return (string) ($res->json()['checkout_url'] ?? '');
+ }
+
public function canAfford(string $publicId, int $amountMinor): bool
{
return (bool) ($this->get('/can-afford', ['user' => $publicId, 'amount_minor' => $amountMinor])['affordable'] ?? false);
diff --git a/resources/views/components/user/service-topup-modal.blade.php b/resources/views/components/user/service-topup-modal.blade.php
index f9a5675..bd4793a 100644
--- a/resources/views/components/user/service-topup-modal.blade.php
+++ b/resources/views/components/user/service-topup-modal.blade.php
@@ -1,84 +1,83 @@
@props([
'id',
- 'title' => 'Add credits',
- 'description' => '',
+ 'title' => 'Add funds',
'topupAction',
'minTopup' => 5,
'suggestedAmount' => 10,
- 'ladillWalletBalance' => 0,
+ 'pricePerAction' => null,
+ 'actionNoun' => 'action',
'serviceBalance' => null,
- 'serviceBalanceLabel' => 'Current balance',
+ 'serviceBalanceLabel' => 'Wallet balance',
+ 'description' => '',
'openOnLoad' => false,
'returnUrl' => null,
])
-@php
- // Single-wallet (siloing step 2): there is one Ladill wallet, so "pay from
- // wallet into service credits" is a meaningless round-trip — top up the one
- // wallet directly (Paystack). Transfer option removed.
- $canPayFromWallet = false;
-@endphp
-
{{ $description }}{{ $title }}
- @if($description)
- How billing works
Monthly storage billing
+Monthly storage billing
+ +GHS {{ number_format($pricePerGb, 2) }} per GB per month, debited from your wallet. Files stay available while payments succeed. If a monthly renewal fails, files are kept for {{ $gracePeriodDays }} days before deletion.