diff --git a/app/Http/Controllers/WalletBalanceController.php b/app/Http/Controllers/WalletBalanceController.php new file mode 100644 index 0000000..29d4267 --- /dev/null +++ b/app/Http/Controllers/WalletBalanceController.php @@ -0,0 +1,40 @@ +user()->public_id; + + $minor = Cache::remember("wallet_balance:{$publicId}", now()->addSeconds(30), function () use ($billing, $publicId) { + try { + return $billing->balanceMinor($publicId); + } catch (\Throwable) { + return null; + } + }); + + if ($minor === null) { + return response()->json(['available' => false]); + } + + $currency = (string) config('billing.currency', 'GHS'); + + return response()->json([ + 'available' => true, + 'balance_minor' => $minor, + 'currency' => $currency, + 'formatted' => $currency.' '.number_format($minor / 100, 2), + ]); + } +} diff --git a/app/Services/Meet/MeetBillingService.php b/app/Services/Meet/MeetBillingService.php index 76f7b63..589b277 100644 --- a/app/Services/Meet/MeetBillingService.php +++ b/app/Services/Meet/MeetBillingService.php @@ -83,8 +83,14 @@ class MeetBillingService max(1, (int) config('meet.billing.minimum_streaming_hours', 1)) ); + $affordable = $this->canAffordSafely($organization->owner_ref, $amountMinor); + + if ($affordable === null) { + abort(503, 'Billing is temporarily unavailable. Please try again shortly.'); + } + abort_unless( - $this->billing->canAfford($organization->owner_ref, $amountMinor), + $affordable, 402, sprintf( 'Insufficient wallet balance. Meetings are billed at GHS %.2f per hour — top up your Ladill wallet to start.', @@ -198,7 +204,7 @@ class MeetBillingService $periodEnd = $this->nextPeriodEnd($base); $reference = sprintf('meet-%s-renewal:%s:through:%s', $type, $uuid, $periodEnd->format('Y-m-d')); - if (! $this->billing->canAfford($organization->owner_ref, $amountMinor)) { + if (! ($this->canAffordSafely($organization->owner_ref, $amountMinor) ?? false)) { return false; } @@ -301,7 +307,7 @@ class MeetBillingService return true; } - if (! $this->billing->canAfford($organization->owner_ref, $amountMinor)) { + if (! ($this->canAffordSafely($organization->owner_ref, $amountMinor) ?? false)) { return false; } @@ -357,6 +363,22 @@ class MeetBillingService ]); } + /** @return bool|null null when the billing API is unreachable */ + protected function canAffordSafely(string $ownerRef, int $amountMinor): ?bool + { + try { + return $this->billing->canAfford($ownerRef, $amountMinor); + } catch (\Throwable $e) { + Log::warning('Meet billing can-afford check failed', [ + 'owner_ref' => $ownerRef, + 'amount_minor' => $amountMinor, + 'error' => $e->getMessage(), + ]); + + return null; + } + } + protected function debit( string $ownerRef, int $amountMinor, diff --git a/app/Services/Meet/SecurityPolicyService.php b/app/Services/Meet/SecurityPolicyService.php index 5e4a86b..f908442 100644 --- a/app/Services/Meet/SecurityPolicyService.php +++ b/app/Services/Meet/SecurityPolicyService.php @@ -67,11 +67,11 @@ class SecurityPolicyService /** * @return array */ - protected function mergedPolicy(Organization $org, Room $room): array + protected function mergedPolicy(?Organization $org, Room $room): array { return array_merge( config('meet.security_defaults'), - $org->security_policy ?? [], + $org?->security_policy ?? [], $room->settings['security'] ?? [], ); } diff --git a/config/billing.php b/config/billing.php index 40fe958..bf8eaf4 100644 --- a/config/billing.php +++ b/config/billing.php @@ -4,6 +4,6 @@ return [ 'api_url' => env('BILLING_API_URL', 'https://ladill.com/api/billing'), 'api_key' => env('BILLING_API_KEY_MEET'), 'service' => 'meet', - 'wallet_balance_route' => 'meet.wallet', + 'wallet_balance_route' => 'wallet.balance', 'currency' => 'GHS', ]; diff --git a/config/ladill_launcher.php b/config/ladill_launcher.php index f100662..df19219 100644 --- a/config/ladill_launcher.php +++ b/config/ladill_launcher.php @@ -31,9 +31,11 @@ return [ ['name' => 'Link', 'url' => 'https://link.'.$root.'/sso/connect?redirect='.urlencode('https://link.'.$root.'/dashboard'), 'icon' => 'link.svg'], ['name' => 'Frontdesk', 'url' => 'https://frontdesk.'.$root.'/sso/connect?redirect='.urlencode('https://frontdesk.'.$root.'/dashboard'), 'icon' => 'frontdesk.svg'], ['name' => 'Care', 'url' => 'https://care.'.$root.'/sso/connect?redirect='.urlencode('https://care.'.$root.'/dashboard'), 'icon' => 'care.svg'], + ['name' => 'Queue', 'url' => 'https://queue.'.$root.'/sso/connect?redirect='.urlencode('https://queue.'.$root.'/dashboard'), 'icon' => 'queue.svg'], ['name' => 'SMS', 'url' => 'https://sms.'.$root, 'icon' => 'sms.svg'], ['name' => 'Bird', 'url' => 'https://bird.'.$root, 'icon' => 'bird.svg'], ['name' => 'Mail', 'url' => 'https://mail.'.$root, 'icon' => 'mail.svg'], + ['name' => 'Meet', 'url' => 'https://meet.'.$root.'/sso/connect?redirect='.urlencode('https://meet.'.$root.'/dashboard'), 'icon' => 'meet.svg'], ['name' => 'Email', 'url' => 'https://email.'.$root, 'icon' => 'email.svg'], ['name' => 'Domains', 'url' => 'https://domains.'.$root, 'icon' => 'domains.svg'], ['name' => 'Servers', 'url' => 'https://servers.'.$root, 'icon' => 'servers.svg'], diff --git a/routes/web.php b/routes/web.php index af022f9..45e5391 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,6 +24,7 @@ use App\Http\Controllers\Meet\TemplateController; use App\Http\Controllers\Meet\WebinarRegistrationController; use App\Http\Controllers\Meet\WebhookSettingsController; use App\Http\Controllers\NotificationController; +use App\Http\Controllers\WalletBalanceController; use Illuminate\Support\Facades\Route; Route::get('/', fn () => auth()->check() @@ -184,6 +185,7 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::post('/members', [MemberController::class, 'store'])->name('meet.members.store'); Route::delete('/members/{member}', [MemberController::class, 'destroy'])->name('meet.members.destroy'); + Route::get('/wallet/balance', [WalletBalanceController::class, 'balance'])->name('wallet.balance'); Route::get('/wallet', fn () => redirect()->away(ladill_account_url('/wallet')))->name('meet.wallet'); Route::get('/team', fn () => redirect()->away(ladill_account_url('/account/team')))->name('meet.team'); });