diff --git a/app/Http/Controllers/Qr/AccountController.php b/app/Http/Controllers/Qr/AccountController.php index dc99792..fe37f1a 100644 --- a/app/Http/Controllers/Qr/AccountController.php +++ b/app/Http/Controllers/Qr/AccountController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Qr; use App\Http\Controllers\Controller; use App\Models\QrSetting; +use App\Models\Transfer; use App\Services\Billing\BillingClient; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -49,10 +50,30 @@ class AccountController extends Controller $user = ladill_account(); [$balanceMinor, $ledger] = $this->billingSnapshot($user?->public_id); + $activeTransfers = Transfer::query() + ->where('user_id', $user->id) + ->where('status', Transfer::STATUS_ACTIVE) + ->where(function ($query) { + $query->whereNull('expires_at')->orWhere('expires_at', '>', now()); + }) + ->orderByDesc('storage_bytes') + ->get(); + + $storageBytes = (int) $activeTransfers->sum('storage_bytes'); + $storageGb = round($storageBytes / (1024 * 1024 * 1024), 2); + $pricePerGb = (float) config('transfer.price_per_gb_month', 1.0); + $monthlyTotalGhs = round($activeTransfers->sum(fn (Transfer $transfer) => $transfer->monthlyCostGhs()), 2); + return view('qr.account.billing', [ 'balanceMinor' => $balanceMinor, 'spentMinor' => (int) ($ledger['spent_minor'] ?? 0), 'creditedMinor' => (int) ($ledger['credited_minor'] ?? 0), + 'activeTransfers' => $activeTransfers, + 'storageBytes' => $storageBytes, + 'storageGb' => $storageGb, + 'pricePerGb' => $pricePerGb, + 'monthlyTotalGhs' => $monthlyTotalGhs, + 'walletUrl' => route('account.wallet'), 'topupUrl' => $this->topupUrl(), ]); } diff --git a/app/Services/Afia/AfiaService.php b/app/Services/Afia/AfiaService.php index 1ab2838..ff2a7ec 100644 --- a/app/Services/Afia/AfiaService.php +++ b/app/Services/Afia/AfiaService.php @@ -109,7 +109,8 @@ class AfiaService - After creating a transfer: users get a share link and downloadable QR code; recipients scan ladill.com/q/{code} or open the link. - Files: browse all stored files across transfers with sizes and download counts. - Analytics: download trends, top transfers, recent download activity. - - Wallet & Billing: prepaid storage is billed from the Ladill wallet (sidebar → Wallet). Top up at account.ladill.com/wallet. + - Wallet: Ladill wallet balance and top-up (sidebar → Wallet). Top up at account.ladill.com/wallet. + - Billing: storage usage, per-transfer monthly costs, and spend history (sidebar → Billing). - Team: invite teammates to manage transfers together (sidebar → Team). - Settings: notification preferences for transfer activity. diff --git a/resources/views/qr/account/billing.blade.php b/resources/views/qr/account/billing.blade.php index a7505ba..bba14a8 100644 --- a/resources/views/qr/account/billing.blade.php +++ b/resources/views/qr/account/billing.blade.php @@ -1,13 +1,83 @@ Billing - @php $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); @endphp + @php + $fmt = fn ($m) => 'GHS '.number_format($m / 100, 2); + $fmtBytes = function (int $bytes) { + if ($bytes >= 1073741824) { + return number_format($bytes / 1073741824, 2).' GB'; + } + if ($bytes >= 1048576) { + return number_format($bytes / 1048576, 1).' MB'; + } + + return number_format($bytes / 1024, 0).' KB'; + }; + @endphp

Billing

-

File storage is billed from your Ladill wallet (GHS {{ number_format(config('transfer.price_per_gb_month', 1), 2) }} per GB per month of retention).

-
-

Current balance

-

{{ $fmt($balanceMinor) }}

- Top up wallet +

Storage is metered at GHS {{ number_format($pricePerGb, 2) }} per GB per month of retention and debited from your Ladill wallet.

+ +
+
+

Storage used

+

{{ $fmtBytes($storageBytes) }}

+
+
+

Monthly estimate

+

GHS {{ number_format($monthlyTotalGhs, 2) }}

+
+
+

Spent on Transfer

+

{{ $fmt($spentMinor) }}

+
+
+

Refunded / credited

+

{{ $fmt($creditedMinor) }}

+
+ +
+
+

Active transfer storage

+ GHS {{ number_format($pricePerGb, 2) }}/GB/mo +
+ @if($activeTransfers->isEmpty()) +

No active transfers with stored files yet.

+ @else +
    + @foreach($activeTransfers as $transfer) +
  • +
    + {{ $transfer->title }} +

    + {{ $fmtBytes($transfer->storage_bytes) }} + @if($transfer->expires_at) + · expires {{ $transfer->expires_at->format('M j, Y') }} + @endif +

    +
    + GHS {{ number_format($transfer->monthlyCostGhs(), 2) }}/mo +
  • + @endforeach +
+
+ Monthly total + GHS {{ number_format($monthlyTotalGhs, 2) }} +
+ @endif +
+ +
+
+

Wallet balance

+

{{ $fmt($balanceMinor) }}

+
+ +
+ +

Retention is prepaid from your wallet. Keep it funded so transfers stay available through their expiry date.

diff --git a/resources/views/qr/account/wallet.blade.php b/resources/views/qr/account/wallet.blade.php index 15151c8..5fa3c91 100644 --- a/resources/views/qr/account/wallet.blade.php +++ b/resources/views/qr/account/wallet.blade.php @@ -19,5 +19,9 @@

{{ $fmt($creditedMinor) }}

+

+ For storage usage and monthly estimates, see + Billing. +

diff --git a/tests/Feature/BillingPageTest.php b/tests/Feature/BillingPageTest.php new file mode 100644 index 0000000..a706d35 --- /dev/null +++ b/tests/Feature/BillingPageTest.php @@ -0,0 +1,75 @@ + (string) Str::uuid(), 'name' => 'A', 'email' => 'a+'.uniqid().'@x.com']); + } + + private function fakeBillingApi(string $publicId): void + { + $base = rtrim((string) config('billing.api_url'), '/'); + + Http::fake([ + $base.'/balance*' => Http::response(['balance_minor' => 50000]), + $base.'/service-ledger*' => Http::response([ + 'spent_minor' => 1200, + 'credited_minor' => 0, + 'net_minor' => -1200, + ]), + ]); + } + + public function test_billing_page_shows_storage_breakdown(): void + { + $user = $this->user(); + $this->fakeBillingApi($user->public_id); + + Transfer::create([ + 'user_id' => $user->id, + 'title' => 'Client deliverables', + 'retention_days' => 30, + 'expires_at' => now()->addDays(30), + 'status' => Transfer::STATUS_ACTIVE, + 'storage_bytes' => 2 * 1024 * 1024 * 1024, + ]); + + $this->actingAs($user) + ->get(route('account.billing')) + ->assertOk() + ->assertSee('Active transfer storage') + ->assertSee('Client deliverables') + ->assertSee('Monthly estimate') + ->assertDontSee('Add funds'); + } + + public function test_wallet_page_focuses_on_balance(): void + { + $user = $this->user(); + $this->fakeBillingApi($user->public_id); + + $this->actingAs($user) + ->get(route('account.wallet')) + ->assertOk() + ->assertSee('Add funds') + ->assertDontSee('Active transfer storage'); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index eece6d4..664de82 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,5 +10,11 @@ abstract class TestCase extends BaseTestCase { parent::setUp(); $this->withoutVite(); + + foreach (['storage/framework/views', 'storage/framework/cache/data'] as $dir) { + if (! is_dir($path = base_path($dir))) { + mkdir($path, 0755, true); + } + } } }