diff --git a/app/Http/Controllers/Qms/DashboardController.php b/app/Http/Controllers/Qms/DashboardController.php index a6781ec..27e9ce4 100644 --- a/app/Http/Controllers/Qms/DashboardController.php +++ b/app/Http/Controllers/Qms/DashboardController.php @@ -9,6 +9,7 @@ use App\Models\Member; use App\Models\ServiceQueue; use App\Services\Qms\DashboardStats; use App\Services\Qms\OrganizationResolver; +use App\Services\Qms\PlanService; use Illuminate\Http\Request; use Illuminate\View\View; @@ -42,7 +43,9 @@ class DashboardController extends Controller $operational = $this->stats->forOrganization($owner, $organization->id, $branchScope); $branches = (clone $branchQuery)->withCount('serviceQueues')->orderBy('name')->get(); + $plans = app(PlanService::class); + $hasPaidPlan = $plans->hasPaidPlan($organization); - return view('qms.dashboard', compact('organization', 'orgStats', 'branches', 'operational')); + return view('qms.dashboard', compact('organization', 'orgStats', 'branches', 'operational', 'hasPaidPlan')); } } diff --git a/app/Http/Controllers/Qms/ProController.php b/app/Http/Controllers/Qms/ProController.php index ec3d4ac..68a0413 100644 --- a/app/Http/Controllers/Qms/ProController.php +++ b/app/Http/Controllers/Qms/ProController.php @@ -24,21 +24,27 @@ class ProController extends Controller $expiresAt = ! empty($settings['plan_expires_at']) ? Carbon::parse($settings['plan_expires_at']) : null; + $planKey = $plans->planKey($organization); + $branchCount = $plans->activeBranchCount($organization); + $enterprisePrice = $plans->enterprisePriceMinor($organization); return view('qms.pro.index', [ 'organization' => $organization, - 'isPro' => $plans->isPro($organization), + 'planKey' => $planKey, + 'isPro' => $planKey === 'pro', + 'isEnterprise' => $planKey === 'enterprise', + 'hasPaidPlan' => $plans->hasPaidPlan($organization), 'canManage' => $canManage, - 'priceMinor' => $plans->proPriceMinor(), + 'proPriceMinor' => $plans->proPriceMinor(), + 'enterprisePricePerBranchMinor' => $plans->enterprisePricePerBranchMinor(), + 'enterprisePriceMinor' => $enterprisePrice, + 'branchCount' => $branchCount, + 'canSubscribeEnterprise' => $plans->canSubscribeEnterprise($organization), + 'enterpriseMinBranches' => (int) config('qms.enterprise.min_branches', 2), 'currency' => (string) config('billing.currency', 'GHS'), 'planExpiresAt' => $expiresAt, - 'proFeatures' => [ - 'Unlimited branches & queues', - 'Advanced routing rules', - 'Digital displays & kiosk devices', - 'Appointment reminders', - 'Analytics & report exports', - ], + 'enterpriseBilledBranches' => (int) ($settings['enterprise_billed_branches'] ?? $branchCount), + 'salesUrl' => ladill_platform_url('contact-sales'), ]); } @@ -47,25 +53,78 @@ class ProController extends Controller $this->authorizeAbility($request, 'settings.manage'); $organization = $this->organization($request); - if ($plans->isPro($organization)) { + if ($plans->hasPaidPlan($organization)) { return redirect()->route('qms.pro.index') - ->with('success', 'Your organization is already on Queue Pro.'); + ->with('success', 'Your organization already has an active paid plan.'); } - $priceMinor = $plans->proPriceMinor(); + return $this->chargePlan( + $organization, + $billing, + $plans->proPriceMinor(), + 'pro', + 'queue_pro', + 'queue-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'), + 'Ladill Queue Pro — monthly subscription', + 'Welcome to Queue Pro — active for one month.', + ); + } + public function subscribeEnterprise(Request $request, PlanService $plans, BillingClient $billing): RedirectResponse + { + $this->authorizeAbility($request, 'settings.manage'); + $organization = $this->organization($request); + + if ($plans->hasPaidPlan($organization)) { + return redirect()->route('qms.pro.index') + ->with('success', 'Your organization already has an active paid plan.'); + } + + if (! $plans->canSubscribeEnterprise($organization)) { + $min = (int) config('qms.enterprise.min_branches', 2); + + return redirect()->route('qms.pro.index') + ->with('error', "Enterprise billing requires at least {$min} active branches. Add another branch or choose Pro for a single site."); + } + + $priceMinor = $plans->enterprisePriceMinor($organization); + + return $this->chargePlan( + $organization, + $billing, + $priceMinor, + 'enterprise', + 'queue_enterprise', + 'queue-enterprise-'.$organization->id.'-'.now()->format('Y-m-d-His'), + 'Ladill Queue Enterprise — monthly subscription', + 'Welcome to Queue Enterprise — active for one month.', + $plans->activeBranchCount($organization), + ); + } + + private function chargePlan( + $organization, + BillingClient $billing, + int $priceMinor, + string $plan, + string $billingSource, + string $reference, + string $description, + string $successMessage, + ?int $enterpriseBranches = null, + ): RedirectResponse { try { if (! $billing->canAfford($organization->owner_ref, $priceMinor)) { return redirect()->route('qms.pro.index') - ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade.'); } $charged = $billing->debit( $organization->owner_ref, $priceMinor, - 'queue_pro', - 'queue-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'), - 'Ladill Queue Pro — monthly subscription', + $billingSource, + $reference, + $description, ); } catch (\Throwable) { return redirect()->route('qms.pro.index') @@ -74,19 +133,21 @@ class ProController extends Controller if (! $charged) { return redirect()->route('qms.pro.index') - ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade to Pro.'); + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade.'); } $settings = $organization->settings ?? []; - $settings['plan'] = 'pro'; + $settings['plan'] = $plan; $settings['auto_renew'] = true; $settings['plan_expires_at'] = now() ->addDays((int) config('qms.pro.period_days', 30)) ->toIso8601String(); + if ($enterpriseBranches !== null) { + $settings['enterprise_billed_branches'] = $enterpriseBranches; + } unset($settings['plan_renewal_error']); $organization->update(['settings' => $settings]); - return redirect()->route('qms.pro.index') - ->with('success', 'Welcome to Queue Pro — active for one month.'); + return redirect()->route('qms.pro.index')->with('success', $successMessage); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ce224d4..085a127 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -34,16 +34,28 @@ class AppServiceProvider extends ServiceProvider View::composer('partials.sidebar', function ($view) { /** @var User|null $user */ $user = auth()->user(); + $planKey = 'free'; $isPro = false; + $isEnterprise = false; + $hasPaidPlan = false; if ($user) { $organization = app(OrganizationResolver::class)->resolveForUser($user); if ($organization) { - $isPro = app(PlanService::class)->isPro($organization); + $plans = app(PlanService::class); + $planKey = $plans->planKey($organization); + $isPro = $planKey === 'pro'; + $isEnterprise = $planKey === 'enterprise'; + $hasPaidPlan = $plans->hasPaidPlan($organization); } } - $view->with('isPro', $isPro); + $view->with([ + 'planKey' => $planKey, + 'isPro' => $isPro, + 'isEnterprise' => $isEnterprise, + 'hasPaidPlan' => $hasPaidPlan, + ]); }); View::composer(['partials.topbar'], function ($view) { diff --git a/app/Services/Qms/PlanService.php b/app/Services/Qms/PlanService.php index e80949a..5047f4b 100644 --- a/app/Services/Qms/PlanService.php +++ b/app/Services/Qms/PlanService.php @@ -2,6 +2,7 @@ namespace App\Services\Qms; +use App\Models\Branch; use App\Models\Organization; use Carbon\Carbon; @@ -12,7 +13,7 @@ class PlanService $settings = $organization->settings ?? []; $plan = (string) ($settings['plan'] ?? 'free'); - if ($plan === 'pro' && ! empty($settings['plan_expires_at'])) { + if (in_array($plan, ['pro', 'enterprise'], true) && ! empty($settings['plan_expires_at'])) { if (Carbon::parse($settings['plan_expires_at'])->isPast()) { return 'free'; } @@ -31,6 +32,24 @@ class PlanService return $this->planKey($organization) === 'pro'; } + public function isEnterprise(Organization $organization): bool + { + return $this->planKey($organization) === 'enterprise'; + } + + public function hasPaidPlan(Organization $organization): bool + { + return in_array($this->planKey($organization), ['pro', 'enterprise'], true); + } + + public function activeBranchCount(Organization $organization): int + { + return Branch::query() + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->count(); + } + public function maxBranches(Organization $organization): ?int { return config('qms.plans.'.$this->planKey($organization).'.max_branches'); @@ -59,4 +78,21 @@ class PlanService { return (int) config('qms.plans.pro.price_minor', 9900); } + + public function enterprisePricePerBranchMinor(): int + { + return (int) config('qms.plans.enterprise.price_minor_per_branch', 29900); + } + + public function enterprisePriceMinor(Organization $organization): int + { + $branches = max(1, $this->activeBranchCount($organization)); + + return $branches * $this->enterprisePricePerBranchMinor(); + } + + public function canSubscribeEnterprise(Organization $organization): bool + { + return $this->activeBranchCount($organization) >= (int) config('qms.enterprise.min_branches', 2); + } } diff --git a/app/Services/Qms/ProRenewalService.php b/app/Services/Qms/ProRenewalService.php index 34e75df..e527661 100644 --- a/app/Services/Qms/ProRenewalService.php +++ b/app/Services/Qms/ProRenewalService.php @@ -18,7 +18,7 @@ class ProRenewalService public function dueOrganizations(): Collection { return Organization::query() - ->where('settings->plan', 'pro') + ->whereIn('settings->plan', ['pro', 'enterprise']) ->whereNotNull('settings->plan_expires_at') ->get() ->filter(fn (Organization $organization) => $this->isDue($organization)); @@ -27,7 +27,8 @@ class ProRenewalService public function isDue(Organization $organization): bool { $settings = $organization->settings ?? []; - if (($settings['plan'] ?? '') !== 'pro') { + $plan = (string) ($settings['plan'] ?? 'free'); + if (! in_array($plan, ['pro', 'enterprise'], true)) { return false; } if (array_key_exists('auto_renew', $settings) && $settings['auto_renew'] === false) { @@ -47,27 +48,37 @@ class ProRenewalService } $settings = $organization->settings ?? []; - $price = $this->plans->proPriceMinor(); - $reference = 'queue-pro-'.$organization->id.'-'.now()->format('YmdHis'); + $plan = (string) ($settings['plan'] ?? 'free'); + $isEnterprise = $plan === 'enterprise'; + $price = $isEnterprise + ? $this->plans->enterprisePriceMinor($organization) + : $this->plans->proPriceMinor(); + $reference = ($isEnterprise ? 'queue-enterprise-' : 'queue-pro-') + .$organization->id.'-'.now()->format('YmdHis'); try { $charged = $this->billing->debit( $organization->owner_ref, $price, - 'queue_pro_renewal', + $isEnterprise ? 'queue_enterprise_renewal' : 'queue_pro_renewal', $reference, - 'Ladill Queue Pro — monthly renewal', + $isEnterprise + ? 'Ladill Queue Enterprise — monthly renewal' + : 'Ladill Queue Pro — monthly renewal', ); } catch (\Throwable) { $charged = false; } if ($charged) { - $settings['plan'] = 'pro'; + $settings['plan'] = $plan; $settings['auto_renew'] = true; $settings['plan_expires_at'] = now() ->addDays((int) config('qms.pro.period_days', 30)) ->toIso8601String(); + if ($isEnterprise) { + $settings['enterprise_billed_branches'] = $this->plans->activeBranchCount($organization); + } unset($settings['plan_renewal_error']); $organization->update(['settings' => $settings]); @@ -78,7 +89,7 @@ class ProRenewalService $graceEnd = $expires->copy()->addDays((int) config('qms.pro.grace_days', 3)); if ($graceEnd->isPast()) { $settings['plan'] = 'free'; - unset($settings['plan_expires_at']); + unset($settings['plan_expires_at'], $settings['enterprise_billed_branches']); $settings['plan_renewal_error'] = 'Suspended after failed renewal.'; } else { $settings['plan_renewal_error'] = 'Renewal failed — top up your wallet.'; diff --git a/config/qms.php b/config/qms.php index 1ce4913..d790628 100644 --- a/config/qms.php +++ b/config/qms.php @@ -199,6 +199,10 @@ return [ 'period_days' => (int) env('QUEUE_PRO_PERIOD_DAYS', 30), ], + 'enterprise' => [ + 'min_branches' => (int) env('QUEUE_ENTERPRISE_MIN_BRANCHES', 2), + ], + 'rule_types' => [ 'overflow' => 'Overflow routing', 'priority_boost' => 'Priority boost', diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index aa38f27..d8c488e 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -98,19 +98,21 @@ Settings - - @php $proActive = request()->routeIs('qms.pro.*'); @endphp - @if (! empty($isPro)) - - - Queue Pro - - @else - - - Upgrade to Pro - - @endif @endif + +
diff --git a/resources/views/qms/dashboard.blade.php b/resources/views/qms/dashboard.blade.php index 1b2e76b..7061388 100644 --- a/resources/views/qms/dashboard.blade.php +++ b/resources/views/qms/dashboard.blade.php @@ -68,6 +68,16 @@ @endif + @if (empty($hasPaidPlan)) +Unlock Queue Pro or Enterprise
+Unlimited branches, advanced routing, Care & Frontdesk integrations — from GHS 99/mo.
+Unlimited branches, queues, and advanced queue management.
-{{ $currency }} {{ $price }} / month
+Billed monthly from your Ladill wallet. Enterprise includes a one-time setup fee — contact sales for onboarding.
+Free
+GHS 0
+1 branch · 5 queues
+Current plan
+ @endifPro
+{{ $currency }} {{ $proPrice }}/mo
+Unlimited branches & queues
++ Active @if ($planExpiresAt) - Pro is active until {{ $planExpiresAt->format('d M Y') }}. - @else - Your organization is on Queue Pro. + until {{ $planExpiresAt->format('d M Y') }} @endif -
Billed monthly from your Ladill wallet.
@else -Ask an organization administrator to upgrade this workspace to Pro.
+Ask an admin to upgrade.
+ @endif +Enterprise
+{{ $currency }} {{ $enterprisePerBranch }}/branch/mo
++ @if ($branchCount > 0) + {{ $branchCount }} active {{ str('branch')->plural($branchCount) }} = {{ $currency }} {{ $enterpriseTotal }}/mo + @else + Per-branch billing for multi-site + @endif +
++ Active + @if ($planExpiresAt) + until {{ $planExpiresAt->format('d M Y') }} + @endif + @if ($enterpriseBilledBranches > 0) + · billed for {{ $enterpriseBilledBranches }} {{ str('branch')->plural($enterpriseBilledBranches) }} + @endif + . +
+ @elseif ($canManage) + @if ($canSubscribeEnterprise) + + @else +Add at least {{ $enterpriseMinBranches }} active branches, or choose Pro for a single site.
+ @endif + Contact sales for setup & SLA + @else +Ask an admin to upgrade.
@endif