Fix join 500, wallet dropdown balance, and launcher Meet entry.
Deploy Ladill Meet / deploy (push) Successful in 1m35s
Deploy Ladill Meet / deploy (push) Successful in 1m35s
Handle billing API failures gracefully on session start, add wallet.balance JSON endpoint, and sync Meet into the app launcher. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Billing\BillingClient;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight wallet balance for the avatar dropdown widget.
|
||||||
|
*/
|
||||||
|
class WalletBalanceController extends Controller
|
||||||
|
{
|
||||||
|
public function balance(Request $request, BillingClient $billing): JsonResponse
|
||||||
|
{
|
||||||
|
$publicId = (string) $request->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),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,8 +83,14 @@ class MeetBillingService
|
|||||||
max(1, (int) config('meet.billing.minimum_streaming_hours', 1))
|
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(
|
abort_unless(
|
||||||
$this->billing->canAfford($organization->owner_ref, $amountMinor),
|
$affordable,
|
||||||
402,
|
402,
|
||||||
sprintf(
|
sprintf(
|
||||||
'Insufficient wallet balance. Meetings are billed at GHS %.2f per hour — top up your Ladill wallet to start.',
|
'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);
|
$periodEnd = $this->nextPeriodEnd($base);
|
||||||
$reference = sprintf('meet-%s-renewal:%s:through:%s', $type, $uuid, $periodEnd->format('Y-m-d'));
|
$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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +307,7 @@ class MeetBillingService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->billing->canAfford($organization->owner_ref, $amountMinor)) {
|
if (! ($this->canAffordSafely($organization->owner_ref, $amountMinor) ?? false)) {
|
||||||
return 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(
|
protected function debit(
|
||||||
string $ownerRef,
|
string $ownerRef,
|
||||||
int $amountMinor,
|
int $amountMinor,
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ class SecurityPolicyService
|
|||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
protected function mergedPolicy(Organization $org, Room $room): array
|
protected function mergedPolicy(?Organization $org, Room $room): array
|
||||||
{
|
{
|
||||||
return array_merge(
|
return array_merge(
|
||||||
config('meet.security_defaults'),
|
config('meet.security_defaults'),
|
||||||
$org->security_policy ?? [],
|
$org?->security_policy ?? [],
|
||||||
$room->settings['security'] ?? [],
|
$room->settings['security'] ?? [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,6 +4,6 @@ return [
|
|||||||
'api_url' => env('BILLING_API_URL', 'https://ladill.com/api/billing'),
|
'api_url' => env('BILLING_API_URL', 'https://ladill.com/api/billing'),
|
||||||
'api_key' => env('BILLING_API_KEY_MEET'),
|
'api_key' => env('BILLING_API_KEY_MEET'),
|
||||||
'service' => 'meet',
|
'service' => 'meet',
|
||||||
'wallet_balance_route' => 'meet.wallet',
|
'wallet_balance_route' => 'wallet.balance',
|
||||||
'currency' => 'GHS',
|
'currency' => 'GHS',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ return [
|
|||||||
['name' => 'Link', 'url' => 'https://link.'.$root.'/sso/connect?redirect='.urlencode('https://link.'.$root.'/dashboard'), 'icon' => 'link.svg'],
|
['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' => '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' => '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' => 'SMS', 'url' => 'https://sms.'.$root, 'icon' => 'sms.svg'],
|
||||||
['name' => 'Bird', 'url' => 'https://bird.'.$root, 'icon' => 'bird.svg'],
|
['name' => 'Bird', 'url' => 'https://bird.'.$root, 'icon' => 'bird.svg'],
|
||||||
['name' => 'Mail', 'url' => 'https://mail.'.$root, 'icon' => 'mail.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' => 'Email', 'url' => 'https://email.'.$root, 'icon' => 'email.svg'],
|
||||||
['name' => 'Domains', 'url' => 'https://domains.'.$root, 'icon' => 'domains.svg'],
|
['name' => 'Domains', 'url' => 'https://domains.'.$root, 'icon' => 'domains.svg'],
|
||||||
['name' => 'Servers', 'url' => 'https://servers.'.$root, 'icon' => 'servers.svg'],
|
['name' => 'Servers', 'url' => 'https://servers.'.$root, 'icon' => 'servers.svg'],
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use App\Http\Controllers\Meet\TemplateController;
|
|||||||
use App\Http\Controllers\Meet\WebinarRegistrationController;
|
use App\Http\Controllers\Meet\WebinarRegistrationController;
|
||||||
use App\Http\Controllers\Meet\WebhookSettingsController;
|
use App\Http\Controllers\Meet\WebhookSettingsController;
|
||||||
use App\Http\Controllers\NotificationController;
|
use App\Http\Controllers\NotificationController;
|
||||||
|
use App\Http\Controllers\WalletBalanceController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', fn () => auth()->check()
|
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::post('/members', [MemberController::class, 'store'])->name('meet.members.store');
|
||||||
Route::delete('/members/{member}', [MemberController::class, 'destroy'])->name('meet.members.destroy');
|
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('/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');
|
Route::get('/team', fn () => redirect()->away(ladill_account_url('/account/team')))->name('meet.team');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user