From cc3d4e9a63f027e953f5619706501f77d118cbd9 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 28 Jun 2026 02:12:44 +0000 Subject: [PATCH] Set Pro to GHS 100/mo and add sidebar Go Pro UI like Accounting. Adds a dedicated Pro page with wallet upgrade, gradient Upgrade to Pro button in the sidebar footer, and view composer for plan status. Co-authored-by: Cursor --- .../Controllers/Frontdesk/ProController.php | 89 +++++++++++++++ .../Frontdesk/WalletController.php | 43 +------- app/Providers/AppServiceProvider.php | 19 ++++ app/Services/Frontdesk/PlanService.php | 2 +- config/frontdesk.php | 2 +- .../frontdesk/integrations/upgrade.blade.php | 7 +- resources/views/frontdesk/pro/index.blade.php | 59 ++++++++++ .../views/frontdesk/settings/edit.blade.php | 5 +- resources/views/partials/sidebar.blade.php | 17 ++- routes/web.php | 7 +- tests/Feature/FrontdeskProTest.php | 104 ++++++++++++++++++ 11 files changed, 297 insertions(+), 57 deletions(-) create mode 100644 app/Http/Controllers/Frontdesk/ProController.php create mode 100644 resources/views/frontdesk/pro/index.blade.php create mode 100644 tests/Feature/FrontdeskProTest.php diff --git a/app/Http/Controllers/Frontdesk/ProController.php b/app/Http/Controllers/Frontdesk/ProController.php new file mode 100644 index 0000000..4d2d8cb --- /dev/null +++ b/app/Http/Controllers/Frontdesk/ProController.php @@ -0,0 +1,89 @@ +organization($request); + $canManage = app(FrontdeskPermissions::class)->isAdmin($this->member($request)); + $settings = $organization->settings ?? []; + $expiresAt = ! empty($settings['plan_expires_at']) + ? Carbon::parse($settings['plan_expires_at']) + : null; + + return view('frontdesk.pro.index', [ + 'organization' => $organization, + 'isPro' => $plans->isPro($organization), + 'canManage' => $canManage, + 'priceMinor' => $plans->proPriceMinor(), + 'currency' => (string) config('billing.currency', 'GHS'), + 'planExpiresAt' => $expiresAt, + 'proFeatures' => [ + 'Unlimited branches & kiosk devices', + 'Unlimited host email alerts', + 'Webhooks & calendar (iCal) feeds', + 'Scheduled report exports', + 'Custom badge templates', + ], + ]); + } + + public function subscribe(Request $request, PlanService $plans, BillingClient $billing): RedirectResponse + { + $this->authorizeAbility($request, 'settings.manage'); + $organization = $this->organization($request); + + if ($plans->isPro($organization)) { + return redirect()->route('frontdesk.pro.index') + ->with('success', 'Your organization is already on Frontdesk Pro.'); + } + + $priceMinor = $plans->proPriceMinor(); + $ownerRef = $organization->owner_ref; + + try { + if (! $billing->canAfford($ownerRef, $priceMinor)) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); + } + + $charged = $billing->debit( + $ownerRef, + $priceMinor, + 'frontdesk_pro', + 'frontdesk-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'), + 'Ladill Frontdesk Pro — monthly subscription', + ); + } catch (\Throwable) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Could not process upgrade. Please try again.'); + } + + if (! $charged) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); + } + + $settings = $organization->settings ?? []; + $settings['plan'] = 'pro'; + $settings['plan_expires_at'] = now()->addMonth()->toIso8601String(); + $organization->update(['settings' => $settings]); + + return redirect()->route('frontdesk.pro.index') + ->with('success', 'Welcome to Frontdesk Pro — active for one month.'); + } +} diff --git a/app/Http/Controllers/Frontdesk/WalletController.php b/app/Http/Controllers/Frontdesk/WalletController.php index a34bf32..e0db2e6 100644 --- a/app/Http/Controllers/Frontdesk/WalletController.php +++ b/app/Http/Controllers/Frontdesk/WalletController.php @@ -5,8 +5,7 @@ namespace App\Http\Controllers\Frontdesk; use App\Http\Controllers\Controller; use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount; use App\Services\Billing\BillingClient; -use App\Services\Frontdesk\PlanService; -use Illuminate\Http\RedirectResponse; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -39,44 +38,4 @@ class WalletController extends Controller 'formatted' => $currency.' '.number_format($minor / 100, 2), ]); } - - public function upgrade(Request $request, PlanService $plans, BillingClient $billing): RedirectResponse - { - $this->authorizeAbility($request, 'settings.manage'); - $organization = $this->organization($request); - - if ($plans->isPro($organization)) { - return back()->with('success', 'Your organization is already on Frontdesk Pro.'); - } - - $priceMinor = $plans->proPriceMinor(); - $ownerRef = $organization->owner_ref; - - try { - if (! $billing->canAfford($ownerRef, $priceMinor)) { - return back()->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); - } - - $charged = $billing->debit( - $ownerRef, - $priceMinor, - 'frontdesk_pro', - 'frontdesk-pro-'.$organization->id.'-'.now()->format('Y-m'), - 'Ladill Frontdesk Pro — monthly subscription', - ); - } catch (\Throwable) { - return back()->with('error', 'Could not process upgrade. Please try again.'); - } - - if (! $charged) { - return back()->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); - } - - $settings = $organization->settings ?? []; - $settings['plan'] = 'pro'; - $settings['plan_expires_at'] = now()->addMonth()->toIso8601String(); - $organization->update(['settings' => $settings]); - - return back()->with('success', 'Upgraded to Frontdesk Pro for one month.'); - } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 383e659..940edc1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,9 +2,13 @@ namespace App\Providers; +use App\Models\User; +use App\Services\Frontdesk\OrganizationResolver; +use App\Services\Frontdesk\PlanService; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -29,5 +33,20 @@ class AppServiceProvider extends ServiceProvider RateLimiter::for('device-heartbeat', function (Request $request) { return Limit::perMinute(60)->by((string) ($request->header('X-Device-Token') ?? $request->ip())); }); + + View::composer('partials.sidebar', function ($view) { + /** @var User|null $user */ + $user = auth()->user(); + $isPro = false; + + if ($user) { + $organization = app(OrganizationResolver::class)->resolveForUser($user); + if ($organization) { + $isPro = app(PlanService::class)->isPro($organization); + } + } + + $view->with('isPro', $isPro); + }); } } diff --git a/app/Services/Frontdesk/PlanService.php b/app/Services/Frontdesk/PlanService.php index 679f572..702eb20 100644 --- a/app/Services/Frontdesk/PlanService.php +++ b/app/Services/Frontdesk/PlanService.php @@ -78,6 +78,6 @@ class PlanService public function proPriceMinor(): int { - return (int) config('frontdesk.plans.pro.price_minor', 9900); + return (int) config('frontdesk.plans.pro.price_minor', 10000); } } diff --git a/config/frontdesk.php b/config/frontdesk.php index 9549520..5e50a37 100644 --- a/config/frontdesk.php +++ b/config/frontdesk.php @@ -132,7 +132,7 @@ return [ ], 'pro' => [ 'label' => 'Pro', - 'price_minor' => (int) env('FRONTDESK_PRO_PRICE_MINOR', 9900), + 'price_minor' => (int) env('FRONTDESK_PRO_PRICE_MINOR', 10000), 'max_branches' => null, 'max_kiosk_devices' => null, 'free_emails_per_month' => env('FRONTDESK_PRO_FREE_EMAILS_PER_MONTH') !== null diff --git a/resources/views/frontdesk/integrations/upgrade.blade.php b/resources/views/frontdesk/integrations/upgrade.blade.php index 7f3690f..7f2215b 100644 --- a/resources/views/frontdesk/integrations/upgrade.blade.php +++ b/resources/views/frontdesk/integrations/upgrade.blade.php @@ -6,12 +6,9 @@

Upgrade to Pro

- GHS {{ number_format($proPriceMinor / 100, 2) }} / month from your Ladill wallet — unlimited branches and kiosks, integrations, scheduled reports, and more. + GHS {{ number_format($proPriceMinor / 100, 0) }} / month from your Ladill wallet — unlimited branches and kiosks, integrations, scheduled reports, and more.

-
- @csrf - -
+ View Pro plans

Top up your wallet if you need more balance.

diff --git a/resources/views/frontdesk/pro/index.blade.php b/resources/views/frontdesk/pro/index.blade.php new file mode 100644 index 0000000..54e1e80 --- /dev/null +++ b/resources/views/frontdesk/pro/index.blade.php @@ -0,0 +1,59 @@ + + @php + $price = number_format($priceMinor / 100, 2); + @endphp + +
+ @if (session('upsell')) +
{{ session('upsell') }}
+ @endif + +
+
+
+ Pro + @if ($isPro) + Active + @endif +
+

Ladill Frontdesk Pro

+

Everything in Free, plus unlimited scale, integrations, and unlimited host email alerts.

+

{{ $currency }} {{ $price }} / month

+
+ +
+
    + @foreach ($proFeatures as $feature) +
  • + + {{ $feature }} +
  • + @endforeach +
+ +
+ @if ($isPro) +
+ @if ($planExpiresAt) + Pro is active until {{ $planExpiresAt->format('d M Y') }}. + @else + Your organization is on Frontdesk Pro. + @endif +
+

SMS host alerts still bill at Ladill platform rates from your wallet.

+ @elseif ($canManage) +
+ @csrf + +
+

Billed monthly from your Ladill wallet. Host SMS remains pay-as-you-go on every plan.

+ @else +

Ask an organization administrator to upgrade this workspace to Pro.

+ @endif +
+
+
+
+
diff --git a/resources/views/frontdesk/settings/edit.blade.php b/resources/views/frontdesk/settings/edit.blade.php index 1350166..7cb833b 100644 --- a/resources/views/frontdesk/settings/edit.blade.php +++ b/resources/views/frontdesk/settings/edit.blade.php @@ -20,10 +20,7 @@

@if (! $isPro) -
- @csrf - -
+ Upgrade to Pro (GHS {{ number_format($proPriceMinor / 100, 0) }}/mo) @endif
diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index ce3d9f4..df0c0e1 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -104,11 +104,24 @@ diff --git a/routes/web.php b/routes/web.php index c3a3b18..ea8a02d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,7 +16,7 @@ use App\Http\Controllers\Frontdesk\KioskController; use App\Http\Controllers\Frontdesk\KioskDeviceController; use App\Http\Controllers\Frontdesk\MemberController; use App\Http\Controllers\Frontdesk\OnboardingController; -use App\Http\Controllers\Frontdesk\QrScanController; +use App\Http\Controllers\Frontdesk\ProController; use App\Http\Controllers\Frontdesk\ReceptionDeskController; use App\Http\Controllers\Frontdesk\ReportController; use App\Http\Controllers\Frontdesk\SecurityController; @@ -143,9 +143,12 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::post('/compliance/visitors/{visitorId}/restore', [ComplianceController::class, 'restoreVisitor'])->name('frontdesk.compliance.visitors.restore'); Route::post('/compliance/visits/{visitId}/restore', [ComplianceController::class, 'restoreVisit'])->name('frontdesk.compliance.visits.restore'); + Route::get('/pro', [ProController::class, 'index'])->name('frontdesk.pro.index'); + Route::post('/pro/subscribe', [ProController::class, 'subscribe'])->name('frontdesk.pro.subscribe'); + Route::get('/settings', [SettingsController::class, 'edit'])->name('frontdesk.settings'); Route::put('/settings', [SettingsController::class, 'update'])->name('frontdesk.settings.update'); - Route::post('/settings/upgrade', [WalletController::class, 'upgrade'])->name('frontdesk.settings.upgrade'); + Route::post('/settings/upgrade', [ProController::class, 'subscribe'])->name('frontdesk.settings.upgrade'); Route::get('/branches', [BranchController::class, 'index'])->name('frontdesk.branches.index'); Route::get('/branches/create', [BranchController::class, 'create'])->name('frontdesk.branches.create'); diff --git a/tests/Feature/FrontdeskProTest.php b/tests/Feature/FrontdeskProTest.php new file mode 100644 index 0000000..4a46bab --- /dev/null +++ b/tests/Feature/FrontdeskProTest.php @@ -0,0 +1,104 @@ +withoutMiddleware(EnsurePlatformSession::class); + config(['billing.api_url' => 'https://billing.test']); + + $this->owner = User::create([ + 'public_id' => 'pro-owner-001', + 'name' => 'Owner', + 'email' => 'owner@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->owner->public_id, + 'name' => 'Pro Org', + 'slug' => 'pro-org', + 'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8], + ]); + + Member::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->owner->public_id, + 'role' => 'org_admin', + ]); + + Branch::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'HQ', + 'is_active' => true, + ]); + } + + public function test_pro_page_renders_for_free_organization(): void + { + $this->actingAs($this->owner) + ->get(route('frontdesk.pro.index')) + ->assertOk() + ->assertSee('Ladill Frontdesk Pro') + ->assertSee('GHS 100') + ->assertSee('Upgrade to Pro'); + } + + public function test_sidebar_shows_upgrade_to_pro_on_dashboard(): void + { + $this->actingAs($this->owner) + ->get(route('frontdesk.dashboard')) + ->assertOk() + ->assertSee('Upgrade to Pro'); + } + + public function test_subscribe_upgrades_organization(): void + { + Http::fake([ + 'billing.test/can-afford*' => Http::response(['affordable' => true]), + 'billing.test/debit' => Http::response(['ok' => true]), + ]); + + $this->actingAs($this->owner) + ->post(route('frontdesk.pro.subscribe')) + ->assertRedirect(route('frontdesk.pro.index')) + ->assertSessionHas('success'); + + $this->assertSame('pro', $this->organization->fresh()->settings['plan']); + } + + public function test_pro_sidebar_shows_active_label_after_upgrade(): void + { + $this->organization->update([ + 'settings' => array_merge($this->organization->settings ?? [], [ + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + ]), + ]); + + $this->actingAs($this->owner) + ->get(route('frontdesk.dashboard')) + ->assertOk() + ->assertSee('Frontdesk Pro') + ->assertDontSee('Upgrade to Pro'); + } +}