Bill transfer storage monthly with a 15-day grace period before deletion.
Deploy Ladill Transfer / deploy (push) Successful in 51s

Files stay available while wallet renewals succeed each month. Failed renewals
keep files for TRANSFER_GRACE_PERIOD_DAYS (default 15) before automatic cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 12:28:33 +00:00
co-authored by Cursor
parent 65b634bb0b
commit 311b5b40bb
23 changed files with 502 additions and 56 deletions
+6 -4
View File
@@ -15,7 +15,7 @@
@endphp
<div class="mx-auto max-w-3xl">
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Billing</h1>
<p class="mt-0.5 text-sm text-slate-500">Storage is metered at GHS {{ number_format($pricePerGb, 2) }} per GB per month of retention and debited from your <a href="{{ $walletUrl }}" class="font-medium text-indigo-600 hover:text-indigo-800">Ladill wallet</a>.</p>
<p class="mt-0.5 text-sm text-slate-500">Storage is billed monthly at GHS {{ number_format($pricePerGb, 2) }} per GB while each transfer stays active. Renewals debit your <a href="{{ $walletUrl }}" class="font-medium text-indigo-600 hover:text-indigo-800">Ladill wallet</a>; unpaid transfers are kept for {{ (int) config('transfer.grace_period_days', 15) }} days before deletion.</p>
<div class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="rounded-2xl border border-slate-200 bg-white p-5">
@@ -51,8 +51,10 @@
<a href="{{ route('transfer.transfers.show', $transfer) }}" class="truncate text-sm font-medium text-slate-900 hover:text-indigo-700">{{ $transfer->title }}</a>
<p class="text-xs text-slate-400">
{{ $fmtBytes($transfer->storage_bytes) }}
@if($transfer->expires_at)
· expires {{ $transfer->expires_at->format('M j, Y') }}
@if($transfer->isInGracePeriod())
· payment due · kept until {{ $transfer->grace_ends_at?->format('M j, Y') }}
@elseif($transfer->paid_until)
· paid through {{ $transfer->paid_until->format('M j, Y') }}
@endif
</p>
</div>
@@ -78,6 +80,6 @@
</div>
</div>
<p class="mt-4 text-xs text-slate-400">Retention is prepaid from your wallet. Keep it funded so transfers stay available through their expiry date.</p>
<p class="mt-4 text-xs text-slate-400">Keep your wallet funded so monthly renewals succeed. If a renewal fails, you have {{ (int) config('transfer.grace_period_days', 15) }} days to top up before files are deleted.</p>
</div>
</x-user-layout>
+1 -1
View File
@@ -58,7 +58,7 @@
<div class="rounded-2xl border border-indigo-100 bg-indigo-50/60 p-6">
<p class="text-xs font-medium uppercase tracking-wide text-indigo-600">Wallet balance</p>
<p class="mt-1.5 text-2xl font-semibold text-slate-900">{{ $fmt($balanceMinor) }}</p>
<p class="mt-2 text-sm text-slate-600">Storage is billed at GHS {{ number_format((float) config('transfer.price_per_gb_month', 0.30), 2) }} per GB per month from your Ladill wallet.</p>
<p class="mt-2 text-sm text-slate-600">Storage is billed monthly at GHS {{ number_format((float) config('transfer.price_per_gb_month', 0.30), 2) }} per GB while your wallet can pay. Unpaid renewals keep files for {{ (int) config('transfer.grace_period_days', 15) }} days before deletion.</p>
<div class="mt-4 space-y-3">
<a href="{{ route('transfer.transfers.create') }}" class="flex items-center justify-between rounded-xl border border-white bg-white px-4 py-3 text-sm hover:bg-indigo-50/50">
<span class="font-medium text-slate-700">New transfer</span>
@@ -4,7 +4,7 @@
<div>
<a href="{{ route('transfer.transfers.index') }}" class="text-sm text-slate-500 hover:text-slate-700"> All transfers</a>
<h1 class="mt-2 text-xl font-semibold text-slate-900">New transfer</h1>
<p class="mt-1 text-sm text-slate-500">Upload files, set how long they stay hosted, and get a share link + QR code.</p>
<p class="mt-1 text-sm text-slate-500">Upload files and get a share link + QR code. Storage is billed monthly from your Ladill wallet while the transfer stays active.</p>
</div>
@if(session('error'))
@@ -51,19 +51,16 @@
@error('files.*')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="retention_days" class="block text-sm font-medium text-slate-700">Retention (days)</label>
<input type="number" name="retention_days" id="retention_days" value="{{ old('retention_days', $defaultRetentionDays) }}" min="1" max="365"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-slate-500">GHS {{ number_format($pricePerGb, 2) }}/GB/month while files are stored.</p>
</div>
<div>
<label for="password" class="block text-sm font-medium text-slate-700">Password <span class="font-normal text-slate-400">(optional)</span></label>
<input type="password" name="password" id="password" minlength="4" maxlength="64"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
autocomplete="new-password">
</div>
<div class="rounded-xl border border-indigo-100 bg-indigo-50/60 px-4 py-3 text-sm text-slate-600">
<p class="font-medium text-slate-900">Monthly storage billing</p>
<p class="mt-1">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.</p>
</div>
<div>
<label for="password" class="block text-sm font-medium text-slate-700">Password <span class="font-normal text-slate-400">(optional)</span></label>
<input type="password" name="password" id="password" minlength="4" maxlength="64"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
autocomplete="new-password">
</div>
<div class="flex justify-end gap-3 pt-2">
@@ -13,7 +13,16 @@
<div>
<a href="{{ route('transfer.transfers.index') }}" class="text-sm text-slate-500 hover:text-slate-700"> All transfers</a>
<h1 class="mt-2 text-xl font-semibold text-slate-900">{{ $transfer->title }}</h1>
<p class="mt-1 text-sm text-slate-500">Expires {{ $transfer->expires_at?->format('M j, Y') ?? '—' }} · {{ $fmtBytes($transfer->storage_bytes) }} stored</p>
<p class="mt-1 text-sm text-slate-500">
@if($transfer->isInGracePeriod())
<span class="font-medium text-amber-700">Payment due</span> · files kept until {{ $transfer->grace_ends_at?->format('M j, Y') }}
@elseif($transfer->paid_until)
Paid through {{ $transfer->paid_until->format('M j, Y') }}
@else
Billing pending
@endif
· {{ $fmtBytes($transfer->storage_bytes) }} stored · GHS {{ number_format($transfer->monthlyCostGhs(), 2) }}/mo
</p>
</div>
<form method="post" action="{{ route('transfer.transfers.destroy', $transfer) }}" onsubmit="return confirm('Delete this transfer and its files?')">
@csrf