From 33b607020713eff69db0a0908a20ea7759469358 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 28 Jun 2026 02:00:26 +0000 Subject: [PATCH] Add freemium plans, wallet-billed host notifications, and Pro upgrades. Host alerts use email/phone on the host record without requiring a Ladill account link; SMS and over-quota emails debit the org wallet at platform rates. Co-authored-by: Cursor --- .../Frontdesk/BranchController.php | 8 + .../Frontdesk/DeviceController.php | 4 + .../Frontdesk/IntegrationController.php | 13 + .../Controllers/Frontdesk/KioskController.php | 1 + .../Frontdesk/KioskDeviceController.php | 1 + .../Frontdesk/SettingsController.php | 18 +- .../Frontdesk/WalletController.php | 82 +++++ app/Models/NotificationUsage.php | 21 ++ .../Frontdesk/NotificationBillingService.php | 110 +++++++ .../Frontdesk/NotificationDispatcher.php | 53 ++- .../Frontdesk/NotificationPricingService.php | 26 ++ .../Frontdesk/NotificationUsageService.php | 47 +++ app/Services/Frontdesk/PlanService.php | 83 +++++ .../Frontdesk/VisitCheckInService.php | 2 +- config/frontdesk.php | 46 ++- ...ate_frontdesk_notification_usage_table.php | 29 ++ .../views/frontdesk/hosts/create.blade.php | 2 + .../views/frontdesk/hosts/edit.blade.php | 3 +- .../frontdesk/integrations/upgrade.blade.php | 22 ++ .../views/frontdesk/kiosk/index.blade.php | 3 +- .../views/frontdesk/settings/edit.blade.php | 45 +++ routes/web.php | 4 +- .../FrontdeskNotificationBillingTest.php | 309 ++++++++++++++++++ 23 files changed, 912 insertions(+), 20 deletions(-) create mode 100644 app/Http/Controllers/Frontdesk/WalletController.php create mode 100644 app/Models/NotificationUsage.php create mode 100644 app/Services/Frontdesk/NotificationBillingService.php create mode 100644 app/Services/Frontdesk/NotificationPricingService.php create mode 100644 app/Services/Frontdesk/NotificationUsageService.php create mode 100644 app/Services/Frontdesk/PlanService.php create mode 100644 database/migrations/2026_06_28_020000_create_frontdesk_notification_usage_table.php create mode 100644 resources/views/frontdesk/integrations/upgrade.blade.php create mode 100644 tests/Feature/FrontdeskNotificationBillingTest.php diff --git a/app/Http/Controllers/Frontdesk/BranchController.php b/app/Http/Controllers/Frontdesk/BranchController.php index 4f1a35d..40e664d 100644 --- a/app/Http/Controllers/Frontdesk/BranchController.php +++ b/app/Http/Controllers/Frontdesk/BranchController.php @@ -38,6 +38,14 @@ class BranchController extends Controller $this->authorizeAbility($request, 'admin.branches.manage'); $organization = $this->organization($request); + $currentCount = Branch::owned($this->ownerRef($request)) + ->where('organization_id', $organization->id) + ->count(); + + if (! app(\App\Services\Frontdesk\PlanService::class)->canAddBranch($organization, $currentCount)) { + return back()->withInput()->with('error', 'Your plan allows one branch. Upgrade to Frontdesk Pro for unlimited branches.'); + } + $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'code' => ['nullable', 'string', 'max:50'], diff --git a/app/Http/Controllers/Frontdesk/DeviceController.php b/app/Http/Controllers/Frontdesk/DeviceController.php index 3e242b4..52f5d90 100644 --- a/app/Http/Controllers/Frontdesk/DeviceController.php +++ b/app/Http/Controllers/Frontdesk/DeviceController.php @@ -61,6 +61,10 @@ class DeviceController extends Controller 'reception_desk_id' => ['nullable', 'integer'], ]); + if ($validated['type'] === 'kiosk' && ! app(\App\Services\Frontdesk\PlanService::class)->canAddKiosk($organization)) { + return back()->withInput()->with('error', 'Your plan allows one kiosk device. Upgrade to Frontdesk Pro for unlimited kiosks.'); + } + $token = in_array($validated['type'], config('frontdesk.device_token_types', []), true) ? $devices->generateToken() : null; diff --git a/app/Http/Controllers/Frontdesk/IntegrationController.php b/app/Http/Controllers/Frontdesk/IntegrationController.php index 25fab2b..26ef122 100644 --- a/app/Http/Controllers/Frontdesk/IntegrationController.php +++ b/app/Http/Controllers/Frontdesk/IntegrationController.php @@ -17,6 +17,15 @@ class IntegrationController extends Controller { $this->authorizeAbility($request, 'settings.view'); $organization = $this->organization($request); + $plans = app(\App\Services\Frontdesk\PlanService::class); + + if (! $plans->hasFeature($organization, 'integrations')) { + return view('frontdesk.integrations.upgrade', [ + 'organization' => $organization, + 'proPriceMinor' => $plans->proPriceMinor(), + ]); + } + $canManage = app(\App\Services\Frontdesk\FrontdeskPermissions::class) ->isAdmin($this->member($request)); @@ -39,6 +48,10 @@ class IntegrationController extends Controller $this->authorizeAbility($request, 'settings.manage'); $organization = $this->organization($request); + if (! app(\App\Services\Frontdesk\PlanService::class)->hasFeature($organization, 'integrations')) { + return back()->with('error', 'Webhooks and calendar feeds require Frontdesk Pro.'); + } + $validated = $request->validate([ 'webhook_url' => ['nullable', 'url', 'max:500'], 'webhook_secret' => ['nullable', 'string', 'max:128'], diff --git a/app/Http/Controllers/Frontdesk/KioskController.php b/app/Http/Controllers/Frontdesk/KioskController.php index 891b175..c74f80c 100644 --- a/app/Http/Controllers/Frontdesk/KioskController.php +++ b/app/Http/Controllers/Frontdesk/KioskController.php @@ -77,6 +77,7 @@ class KioskController extends Controller 'awaiting_approval' => $visit->awaitingApproval(), 'checked_in_at' => $visit->checked_in_at?->toIso8601String(), 'badge_url' => $visit->isInside() ? route('frontdesk.visits.badge', $visit) : null, + 'host_notified' => (bool) ($visit->host_notified ?? false), ], ]); } diff --git a/app/Http/Controllers/Frontdesk/KioskDeviceController.php b/app/Http/Controllers/Frontdesk/KioskDeviceController.php index 422a0b0..17770f4 100644 --- a/app/Http/Controllers/Frontdesk/KioskDeviceController.php +++ b/app/Http/Controllers/Frontdesk/KioskDeviceController.php @@ -84,6 +84,7 @@ class KioskDeviceController extends Controller 'awaiting_approval' => $visit->awaitingApproval(), 'checked_in_at' => $visit->checked_in_at?->toIso8601String(), 'badge_url' => $visit->isInside() ? route('frontdesk.visits.badge', $visit) : null, + 'host_notified' => (bool) ($visit->host_notified ?? false), ], ]); } diff --git a/app/Http/Controllers/Frontdesk/SettingsController.php b/app/Http/Controllers/Frontdesk/SettingsController.php index 05a2fc6..a24c43c 100644 --- a/app/Http/Controllers/Frontdesk/SettingsController.php +++ b/app/Http/Controllers/Frontdesk/SettingsController.php @@ -6,6 +6,9 @@ use App\Http\Controllers\Controller; use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount; use App\Models\Branch; use App\Services\Frontdesk\FrontdeskPermissions; +use App\Services\Frontdesk\NotificationPricingService; +use App\Services\Frontdesk\NotificationUsageService; +use App\Services\Frontdesk\PlanService; use App\Support\OrganizationBranding; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -15,7 +18,7 @@ class SettingsController extends Controller { use ScopesToAccount; - public function edit(Request $request): View + public function edit(Request $request, PlanService $plans, NotificationUsageService $usage, NotificationPricingService $pricing): View { $this->authorizeAbility($request, 'settings.view'); $organization = $this->organization($request); @@ -25,6 +28,10 @@ class SettingsController extends Controller ->where('organization_id', $organization->id) ->count(); + $monthlyUsage = $usage->forOrganization($organization); + $plan = $plans->plan($organization); + $freeEmailAllowance = $plans->freeEmailsPerMonth($organization); + return view('frontdesk.settings.edit', [ 'organization' => $organization, 'canManage' => $canManage, @@ -33,6 +40,15 @@ class SettingsController extends Controller 'deviceTypes' => config('frontdesk.device_types'), 'notificationChannels' => config('frontdesk.notification_channels'), 'notificationEvents' => config('frontdesk.notification_events'), + 'plan' => $plan, + 'isPro' => $plans->isPro($organization), + 'proPriceMinor' => $plans->proPriceMinor(), + 'monthlyUsage' => $monthlyUsage, + 'freeEmailAllowance' => $freeEmailAllowance, + 'emailCostFormatted' => $pricing->formatMinor($pricing->emailCostMinor()), + 'smsCostFormatted' => $pricing->formatMinor( + $pricing->smsCostMinor('Sample SMS message for pricing display.'), + ), ]); } diff --git a/app/Http/Controllers/Frontdesk/WalletController.php b/app/Http/Controllers/Frontdesk/WalletController.php new file mode 100644 index 0000000..a34bf32 --- /dev/null +++ b/app/Http/Controllers/Frontdesk/WalletController.php @@ -0,0 +1,82 @@ +user()->public_id; + + $minor = Cache::remember("frontdesk_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), + ]); + } + + 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/Models/NotificationUsage.php b/app/Models/NotificationUsage.php new file mode 100644 index 0000000..e09d2dd --- /dev/null +++ b/app/Models/NotificationUsage.php @@ -0,0 +1,21 @@ +belongsTo(Organization::class, 'organization_id'); + } +} diff --git a/app/Services/Frontdesk/NotificationBillingService.php b/app/Services/Frontdesk/NotificationBillingService.php new file mode 100644 index 0000000..97387b8 --- /dev/null +++ b/app/Services/Frontdesk/NotificationBillingService.php @@ -0,0 +1,110 @@ +plans->freeEmailsPerMonth($organization); + if ($allowance === null || $this->usage->emailCountThisMonth($organization) < $allowance) { + return 0; + } + + return $this->pricing->emailCostMinor(); + } + + public function canAffordEmail(Organization $organization): bool + { + $cost = $this->emailCostMinor($organization); + if ($cost <= 0) { + return true; + } + + return $this->canAfford($organization->owner_ref, $cost); + } + + public function canAffordSms(Organization $organization, string $message): bool + { + $cost = $this->pricing->smsCostMinor($message); + if ($cost <= 0) { + return true; + } + + return $this->canAfford($organization->owner_ref, $cost); + } + + /** + * Debit after a successful send. Returns false if the wallet could not be charged. + */ + public function chargeEmail(Organization $organization, string $reference, string $description): bool + { + $cost = $this->emailCostMinor($organization); + if ($cost <= 0) { + $this->usage->recordEmail($organization, 0); + + return true; + } + + if (! $this->debit($organization->owner_ref, $cost, 'email', $reference, $description)) { + return false; + } + + $this->usage->recordEmail($organization, $cost); + + return true; + } + + public function chargeSms(Organization $organization, string $message, string $reference, string $description): bool + { + $cost = $this->pricing->smsCostMinor($message); + if ($cost <= 0) { + $this->usage->recordSms($organization, 0); + + return true; + } + + if (! $this->debit($organization->owner_ref, $cost, 'sms', $reference, $description)) { + return false; + } + + $this->usage->recordSms($organization, $cost); + + return true; + } + + private function canAfford(string $ownerRef, int $costMinor): bool + { + try { + return $this->billing->canAfford($ownerRef, $costMinor); + } catch (\Throwable) { + return true; + } + } + + private function debit(string $ownerRef, int $costMinor, string $channel, string $reference, string $description): bool + { + try { + return $this->billing->debit( + $ownerRef, + $costMinor, + 'notification_'.$channel, + 'frontdesk-notify-'.$reference.'-'.Str::uuid(), + $description, + ); + } catch (\Throwable) { + return false; + } + } +} diff --git a/app/Services/Frontdesk/NotificationDispatcher.php b/app/Services/Frontdesk/NotificationDispatcher.php index 615dab5..ab53a44 100644 --- a/app/Services/Frontdesk/NotificationDispatcher.php +++ b/app/Services/Frontdesk/NotificationDispatcher.php @@ -19,11 +19,12 @@ class NotificationDispatcher protected EmailService $email, protected SmsService $sms, protected NotificationPreferenceService $preferences, + protected NotificationBillingService $billing, ) {} - public function visitorArrived(Visit $visit): void + public function visitorArrived(Visit $visit): bool { - $this->dispatchVisitEvent( + return $this->dispatchVisitEvent( $visit, 'visitor_arrived', 'Visitor has arrived', @@ -149,15 +150,15 @@ class NotificationDispatcher string $title, string $message, array $meta = [], - ): void { + ): bool { $visit->load(['visitor', 'host', 'organization']); $channels = $this->preferences->channelsForEvent($visit->organization, $event); if ($channels === [] || ! $visit->host) { - return; + return false; } - $this->notifyHost($visit->host, $visit->organization, $channels, $title, $message, $visit, $event, $meta); + return $this->notifyHost($visit->host, $visit->organization, $channels, $title, $message, $visit, $event, $meta); } /** @param list $channels */ @@ -170,17 +171,45 @@ class NotificationDispatcher ?Visit $visit = null, ?string $event = null, array $meta = [], - ): void { + ): bool { + $notified = false; + $reference = $visit ? 'visit-'.$visit->id.'-'.($event ?? 'alert') : 'host-'.$host->id; + if (in_array('email', $channels, true) && $host->email) { - try { - $this->email->send($host->email, $title, $message); - } catch (\Throwable $e) { - Log::warning('Frontdesk host email failed', ['host_id' => $host->id, 'error' => $e->getMessage()]); + if (! $this->billing->canAffordEmail($organization)) { + Log::info('Frontdesk host email skipped — insufficient wallet balance', [ + 'host_id' => $host->id, + 'organization_id' => $organization->id, + ]); + } else { + try { + $this->email->send($host->email, $title, $message); + if ($this->billing->chargeEmail($organization, $reference, "Host alert: {$title}")) { + $notified = true; + } + } catch (\Throwable $e) { + Log::warning('Frontdesk host email failed', ['host_id' => $host->id, 'error' => $e->getMessage()]); + } } } if (in_array('sms', $channels, true) && $host->phone) { - $this->sms->send($host->phone, "{$title}: {$message}"); + $smsBody = "{$title}: {$message}"; + if (! $this->billing->canAffordSms($organization, $smsBody)) { + Log::info('Frontdesk host SMS skipped — insufficient wallet balance', [ + 'host_id' => $host->id, + 'organization_id' => $organization->id, + ]); + } else { + try { + $this->sms->send($host->phone, $smsBody); + if ($this->billing->chargeSms($organization, $smsBody, $reference, "Host SMS: {$title}")) { + $notified = true; + } + } catch (\Throwable $e) { + Log::warning('Frontdesk host SMS failed', ['host_id' => $host->id, 'error' => $e->getMessage()]); + } + } } if ($host->user_ref) { @@ -193,6 +222,8 @@ class NotificationDispatcher ]))); } } + + return $notified; } protected function notifyStaffUsers( diff --git a/app/Services/Frontdesk/NotificationPricingService.php b/app/Services/Frontdesk/NotificationPricingService.php new file mode 100644 index 0000000..1c60e98 --- /dev/null +++ b/app/Services/Frontdesk/NotificationPricingService.php @@ -0,0 +1,26 @@ +format('Y-m'); + } + + public function forOrganization(Organization $organization, ?string $period = null): NotificationUsage + { + $period ??= $this->currentPeriod(); + + return NotificationUsage::firstOrCreate( + ['organization_id' => $organization->id, 'period' => $period], + ['email_count' => 0, 'sms_count' => 0, 'email_charged_minor' => 0, 'sms_charged_minor' => 0], + ); + } + + public function emailCountThisMonth(Organization $organization): int + { + return (int) $this->forOrganization($organization)->email_count; + } + + public function recordEmail(Organization $organization, int $chargedMinor = 0): void + { + $usage = $this->forOrganization($organization); + $usage->increment('email_count'); + if ($chargedMinor > 0) { + $usage->increment('email_charged_minor', $chargedMinor); + } + } + + public function recordSms(Organization $organization, int $chargedMinor): void + { + $usage = $this->forOrganization($organization); + $usage->increment('sms_count'); + if ($chargedMinor > 0) { + $usage->increment('sms_charged_minor', $chargedMinor); + } + } +} diff --git a/app/Services/Frontdesk/PlanService.php b/app/Services/Frontdesk/PlanService.php new file mode 100644 index 0000000..679f572 --- /dev/null +++ b/app/Services/Frontdesk/PlanService.php @@ -0,0 +1,83 @@ +settings ?? []; + $plan = (string) ($settings['plan'] ?? 'free'); + + if ($plan === 'pro' && ! empty($settings['plan_expires_at'])) { + $expires = Carbon::parse($settings['plan_expires_at']); + if ($expires->isPast()) { + return 'free'; + } + } + + return array_key_exists($plan, config('frontdesk.plans', [])) ? $plan : 'free'; + } + + public function isPro(Organization $organization): bool + { + return $this->planKey($organization) === 'pro'; + } + + /** @return array */ + public function plan(Organization $organization): array + { + $key = $this->planKey($organization); + + return [ + 'key' => $key, + ...(array) config('frontdesk.plans.'.$key, []), + ]; + } + + /** Null means unlimited included host emails (Pro). */ + public function freeEmailsPerMonth(Organization $organization): ?int + { + $value = config('frontdesk.plans.'.$this->planKey($organization).'.free_emails_per_month', 100); + + return $value === null ? null : (int) $value; + } + + public function canAddBranch(Organization $organization, int $currentCount): bool + { + $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_branches'); + + return $limit === null || $currentCount < (int) $limit; + } + + public function canAddKiosk(Organization $organization): bool + { + $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_kiosk_devices'); + if ($limit === null) { + return true; + } + + $count = Device::query() + ->where('organization_id', $organization->id) + ->where('type', 'kiosk') + ->count(); + + return $count < (int) $limit; + } + + public function hasFeature(Organization $organization, string $feature): bool + { + $features = config('frontdesk.plans.'.$this->planKey($organization).'.features', []); + + return in_array($feature, $features, true); + } + + public function proPriceMinor(): int + { + return (int) config('frontdesk.plans.pro.price_minor', 9900); + } +} diff --git a/app/Services/Frontdesk/VisitCheckInService.php b/app/Services/Frontdesk/VisitCheckInService.php index b68c679..29cd015 100644 --- a/app/Services/Frontdesk/VisitCheckInService.php +++ b/app/Services/Frontdesk/VisitCheckInService.php @@ -144,7 +144,7 @@ class VisitCheckInService ['visitor' => $visitor->full_name, 'badge_code' => $visit->badge_code], ); - $this->notifications->visitorArrived($visit); + $visit->host_notified = $this->notifications->visitorArrived($visit); $this->webhooks->dispatch('visit.checked_in', $visit); } diff --git a/config/frontdesk.php b/config/frontdesk.php index ac120e9..9549520 100644 --- a/config/frontdesk.php +++ b/config/frontdesk.php @@ -106,11 +106,49 @@ return [ ], 'notification_channels' => [ - 'email' => 'Email', + 'email' => 'Email (platform SMTP)', 'sms' => 'SMS', - 'push' => 'Push Notification', - 'teams' => 'Microsoft Teams', - 'slack' => 'Slack', + ], + + 'billing' => [ + 'email_cost_ghs' => (float) env('FRONTDESK_EMAIL_COST_GHS', 0.0135), + 'sms_cost_per_segment_ghs' => (float) env('FRONTDESK_SMS_COST_PER_SEGMENT_GHS', 0.05), + 'sms_segment_length' => (int) env('FRONTDESK_SMS_SEGMENT_LENGTH', 160), + ], + + 'plans' => [ + 'free' => [ + 'label' => 'Free', + 'price_minor' => 0, + 'max_branches' => 1, + 'max_kiosk_devices' => 1, + 'free_emails_per_month' => (int) env('FRONTDESK_FREE_EMAILS_PER_MONTH', 100), + 'features' => [ + 'check_in', + 'hosts', + 'badges', + 'watchlist', + ], + ], + 'pro' => [ + 'label' => 'Pro', + 'price_minor' => (int) env('FRONTDESK_PRO_PRICE_MINOR', 9900), + 'max_branches' => null, + 'max_kiosk_devices' => null, + 'free_emails_per_month' => env('FRONTDESK_PRO_FREE_EMAILS_PER_MONTH') !== null + ? (int) env('FRONTDESK_PRO_FREE_EMAILS_PER_MONTH') + : null, + 'features' => [ + 'check_in', + 'hosts', + 'badges', + 'watchlist', + 'integrations', + 'webhooks', + 'scheduled_reports', + 'custom_badge_templates', + ], + ], ], 'notification_events' => [ diff --git a/database/migrations/2026_06_28_020000_create_frontdesk_notification_usage_table.php b/database/migrations/2026_06_28_020000_create_frontdesk_notification_usage_table.php new file mode 100644 index 0000000..bad9c7e --- /dev/null +++ b/database/migrations/2026_06_28_020000_create_frontdesk_notification_usage_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('organization_id')->constrained('frontdesk_organizations')->cascadeOnDelete(); + $table->string('period', 7); + $table->unsignedInteger('email_count')->default(0); + $table->unsignedInteger('sms_count')->default(0); + $table->unsignedInteger('email_charged_minor')->default(0); + $table->unsignedInteger('sms_charged_minor')->default(0); + $table->timestamps(); + + $table->unique(['organization_id', 'period']); + }); + } + + public function down(): void + { + Schema::dropIfExists('frontdesk_notification_usage'); + } +}; diff --git a/resources/views/frontdesk/hosts/create.blade.php b/resources/views/frontdesk/hosts/create.blade.php index 7500894..9c79612 100644 --- a/resources/views/frontdesk/hosts/create.blade.php +++ b/resources/views/frontdesk/hosts/create.blade.php @@ -21,10 +21,12 @@
+

Used for arrival alerts when email notifications are enabled.

+

Used for SMS alerts when SMS is enabled in settings.

diff --git a/resources/views/frontdesk/hosts/edit.blade.php b/resources/views/frontdesk/hosts/edit.blade.php index c6e742f..ae7a99a 100644 --- a/resources/views/frontdesk/hosts/edit.blade.php +++ b/resources/views/frontdesk/hosts/edit.blade.php @@ -24,7 +24,8 @@
- + +

Optional — enables the host portal and in-app bell. Email and phone above are used for alerts.

When set, this user can access the host self-service portal.

diff --git a/resources/views/frontdesk/integrations/upgrade.blade.php b/resources/views/frontdesk/integrations/upgrade.blade.php new file mode 100644 index 0000000..7f3690f --- /dev/null +++ b/resources/views/frontdesk/integrations/upgrade.blade.php @@ -0,0 +1,22 @@ + +
+

Integrations

+

Webhooks, calendar feeds, and third-party connectors are included with Frontdesk Pro.

+ +
+

Upgrade to Pro

+

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

+
+ @csrf + +
+

+ Top up your wallet if you need more balance. +

+
+ + ← Back to settings +
+
diff --git a/resources/views/frontdesk/kiosk/index.blade.php b/resources/views/frontdesk/kiosk/index.blade.php index 1b3de3f..7a9787a 100644 --- a/resources/views/frontdesk/kiosk/index.blade.php +++ b/resources/views/frontdesk/kiosk/index.blade.php @@ -243,7 +243,8 @@ -

+

diff --git a/resources/views/frontdesk/settings/edit.blade.php b/resources/views/frontdesk/settings/edit.blade.php index 137f6e9..1350166 100644 --- a/resources/views/frontdesk/settings/edit.blade.php +++ b/resources/views/frontdesk/settings/edit.blade.php @@ -6,6 +6,50 @@

Settings

{{ $organization->name }}

+ @if ($canManage) +
+
+
+

Plan

+

+ @if ($isPro) + {{ $plan['label'] ?? 'Pro' }} — unlimited branches & kiosks, integrations, and unlimited host emails (SMS still billed per segment). + @else + Free — 1 branch, 1 kiosk, core check-in & badges. Host alerts billed after {{ number_format($freeEmailAllowance) }} free emails/month. + @endif +

+
+ @if (! $isPro) +
+ @csrf + +
+ @endif +
+
+
+
Host emails this month
+
+ @if ($freeEmailAllowance === null) + {{ number_format($monthlyUsage->email_count) }} sent (unlimited on Pro) + @else + {{ number_format($monthlyUsage->email_count) }} / {{ number_format($freeEmailAllowance) }} free + @endif +
+
+
+
Host SMS this month
+
{{ number_format($monthlyUsage->sms_count) }} sent
+
+
+

+ Email {{ $emailCostFormatted }} each after free allowance · SMS from {{ $smsCostFormatted }} per segment — charged to your + Ladill wallet. + Hosts are notified via email or phone on their profile; linking a Ladill account is optional (in-app bell only). +

+
+ @endif +
@csrf @method('PUT') @@ -70,6 +114,7 @@

Notification channels

+

Alerts go to each host's email and/or phone — no Ladill account required.

@foreach ($notificationChannels as $key => $label)