diff --git a/app/Http/Controllers/Frontdesk/BranchController.php b/app/Http/Controllers/Frontdesk/BranchController.php index c5ac166..8bedcf5 100644 --- a/app/Http/Controllers/Frontdesk/BranchController.php +++ b/app/Http/Controllers/Frontdesk/BranchController.php @@ -43,13 +43,14 @@ class BranchController extends Controller { $this->authorizeAbility($request, 'admin.branches.manage'); $organization = $this->organization($request); + $plans = app(\App\Services\Frontdesk\PlanService::class); $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.'); + if (! $plans->canAddBranch($organization, $currentCount)) { + return back()->withInput()->with('error', $plans->branchLimitMessage($organization)); } $validated = $request->validate([ @@ -90,6 +91,15 @@ class BranchController extends Controller 'is_active' => ['boolean'], ]); + $activating = $request->boolean('is_active') && ! $branch->is_active; + if ($activating) { + $plans = app(\App\Services\Frontdesk\PlanService::class); + $organization = $this->organization($request); + if (! $plans->canActivateBranch($organization)) { + return back()->withInput()->with('error', $plans->branchLimitMessage($organization)); + } + } + $branch->update([ ...$validated, 'is_active' => $request->boolean('is_active'), diff --git a/app/Http/Controllers/Frontdesk/ProController.php b/app/Http/Controllers/Frontdesk/ProController.php index 03326ec..c31dbb4 100644 --- a/app/Http/Controllers/Frontdesk/ProController.php +++ b/app/Http/Controllers/Frontdesk/ProController.php @@ -27,6 +27,10 @@ class ProController extends Controller : null; $planKey = $plans->planKey($organization); $branchCount = max(1, $plans->activeBranchCount($organization)); + $billedBranches = max( + $plans->billedBranches($organization), + $branchCount, + ); return view('frontdesk.pro.index', [ 'organization' => $organization, @@ -45,7 +49,7 @@ class ProController extends Controller 'prepaidMonths' => (array) config('frontdesk.prepaid_months', [6, 12, 24]), 'currency' => (string) config('billing.currency', 'GHS'), 'planExpiresAt' => $expiresAt, - 'billedBranches' => (int) ($settings['billed_branches'] ?? $branchCount), + 'billedBranches' => $billedBranches, 'salesUrl' => ladill_platform_url('contact-sales'), ]); } @@ -57,14 +61,13 @@ class ProController extends Controller if ($plans->hasPaidPlan($organization)) { return redirect()->route('frontdesk.pro.index') - ->with('success', 'Your organization already has an active paid plan.'); + ->with('success', 'Your organization already has an active paid plan. Use "Add seats" on this page to expand your allotment.'); } - $validated = $request->validate([ - 'branches' => ['required', 'integer', 'min:1', 'max:999'], - ]); - - $branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']); + $branches = $plans->resolveCheckoutBranches( + $organization, + $this->requestedBranches($request, $plans, $organization), + ); $amountMinor = $plans->priceForBranches('pro', $branches); return $this->chargeMonthlyWallet( @@ -87,7 +90,7 @@ class ProController extends Controller if ($plans->hasPaidPlan($organization)) { return redirect()->route('frontdesk.pro.index') - ->with('success', 'Your organization already has an active paid plan.'); + ->with('success', 'Your organization already has an active paid plan. Use "Add seats" on this page to expand your allotment.'); } if (! $plans->canSubscribeEnterprise($organization)) { @@ -97,11 +100,10 @@ class ProController extends Controller ->with('error', "Enterprise requires at least {$min} active branch. Add a branch or contact sales."); } - $validated = $request->validate([ - 'branches' => ['required', 'integer', 'min:1', 'max:999'], - ]); - - $branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']); + $branches = $plans->resolveCheckoutBranches( + $organization, + $this->requestedBranches($request, $plans, $organization), + ); $amountMinor = $plans->priceForBranches('enterprise', $branches); return $this->chargeMonthlyWallet( @@ -123,13 +125,13 @@ class ProController extends Controller $validated = $request->validate([ 'plan' => ['required', 'in:pro,enterprise'], 'months' => ['required', 'integer', 'in:6,12,24'], - 'branches' => ['required', 'integer', 'min:1', 'max:999'], + 'branches' => ['nullable', 'integer', 'min:1', 'max:999'], ]); $organization = $this->organization($request); if ($plans->hasPaidPlan($organization)) { return redirect()->route('frontdesk.pro.index') - ->with('success', 'Your organization already has an active plan.'); + ->with('success', 'Your organization already has an active plan. Use "Add seats" on this page to expand your allotment.'); } $plan = $validated['plan']; @@ -142,7 +144,10 @@ class ProController extends Controller ->with('error', "Enterprise requires at least {$min} active branch."); } - $branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']); + $branches = $plans->resolveCheckoutBranches( + $organization, + (int) ($validated['branches'] ?? $plans->activeBranchCount($organization) ?: 1), + ); $amountMinor = $plans->priceForBranches($plan, $branches) * $months; try { @@ -171,6 +176,76 @@ class ProController extends Controller return redirect()->away($url); } + /** + * Mid-cycle expansion: charge for additional seats and raise billed_branches. + */ + public function expandBranches(Request $request, PlanService $plans, BillingClient $billing): RedirectResponse + { + $this->authorizeAbility($request, 'settings.manage'); + $organization = $this->organization($request); + + if (! $plans->hasPaidPlan($organization)) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Subscribe to a paid plan before adding branch seats.'); + } + + $validated = $request->validate([ + 'branches' => ['required', 'integer', 'min:1', 'max:999'], + ]); + + $plan = $plans->planKey($organization); + $currentBilled = max( + 1, + $plans->billedBranches($organization), + $plans->activeBranchCount($organization), + ); + $newTotal = max((int) $validated['branches'], $currentBilled); + + if ($newTotal <= $currentBilled) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Choose a branch count higher than your current allotment ('.$currentBilled.').'); + } + + $extra = $newTotal - $currentBilled; + $rate = $plan === 'enterprise' + ? $plans->enterprisePricePerBranchMinor() + : $plans->proPricePerBranchMinor(); + $amountMinor = $extra * $rate; + $label = $plan === 'enterprise' ? 'Enterprise' : 'Pro'; + $reference = 'frontdesk-'.$plan.'-expand-'.$organization->id.'-'.now()->format('Y-m-d-His'); + + try { + if (! $billing->canAfford($organization->owner_ref, $amountMinor)) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to add branches.'); + } + + $charged = $billing->debit( + $organization->owner_ref, + $amountMinor, + 'frontdesk_'.$plan.'_branch_expand', + $reference, + 'Ladill Frontdesk '.$label.' — +'.$extra.' branch seat(s)', + ); + } catch (\Throwable) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Could not process branch expansion. Please try again.'); + } + + if (! $charged) { + return redirect()->route('frontdesk.pro.index') + ->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to add branches.'); + } + + $settings = $organization->settings ?? []; + $settings['billed_branches'] = $newTotal; + unset($settings['plan_renewal_error']); + $organization->update(['settings' => $settings]); + + return redirect()->route('frontdesk.pro.index') + ->with('success', "Branch allotment updated to {$newTotal}. Charged for {$extra} additional seat(s)."); + } + public function paystackCallback(Request $request, BillingClient $billing, PlanService $plans): RedirectResponse { $reference = (string) $request->query('reference', ''); @@ -216,6 +291,15 @@ class ProController extends Controller ->with('success', "{$label} is active for {$months} months."); } + private function requestedBranches(Request $request, PlanService $plans, Organization $organization): int + { + $validated = $request->validate([ + 'branches' => ['nullable', 'integer', 'min:1', 'max:999'], + ]); + + return (int) ($validated['branches'] ?? max(1, $plans->activeBranchCount($organization))); + } + private function chargeMonthlyWallet( Organization $organization, BillingClient $billing, diff --git a/app/Http/Controllers/Frontdesk/SettingsController.php b/app/Http/Controllers/Frontdesk/SettingsController.php index 983c01c..42e1606 100644 --- a/app/Http/Controllers/Frontdesk/SettingsController.php +++ b/app/Http/Controllers/Frontdesk/SettingsController.php @@ -43,7 +43,11 @@ class SettingsController extends Controller 'notificationEvents' => config('frontdesk.notification_events'), 'plan' => $plan, 'isPro' => $plans->isPro($organization), - 'proPriceMinor' => $plans->proPriceMinor(), + 'isEnterprise' => $plans->isEnterprise($organization), + 'hasPaidPlan' => $plans->hasPaidPlan($organization), + 'billedBranches' => $plans->billedBranches($organization), + 'activeBranchCount' => $plans->activeBranchCount($organization), + 'proPriceMinor' => $plans->proPricePerBranchMinor(), 'monthlyUsage' => $monthlyUsage, 'freeEmailAllowance' => $freeEmailAllowance, 'emailCostFormatted' => $pricing->formatMinor($pricing->emailCostMinor()), diff --git a/app/Services/Frontdesk/PlanService.php b/app/Services/Frontdesk/PlanService.php index 31bf6ff..5bc0b7c 100644 --- a/app/Services/Frontdesk/PlanService.php +++ b/app/Services/Frontdesk/PlanService.php @@ -65,11 +65,53 @@ class PlanService ->count(); } + /** Seat allotment purchased for the current paid period; 0 when free or unset. */ + public function billedBranches(Organization $organization): int + { + return max(0, (int) (($organization->settings ?? [])['billed_branches'] ?? 0)); + } + public function canAddBranch(Organization $organization, int $currentCount): bool { $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_branches'); + if ($limit !== null) { + return $currentCount < (int) $limit; + } - return $limit === null || $currentCount < (int) $limit; + // Paid plans with a purchased seat allotment cannot exceed it until they expand. + $billed = $this->billedBranches($organization); + if ($this->hasPaidPlan($organization) && $billed > 0) { + return $this->activeBranchCount($organization) < $billed; + } + + return true; + } + + public function canActivateBranch(Organization $organization): bool + { + $limit = config('frontdesk.plans.'.$this->planKey($organization).'.max_branches'); + if ($limit !== null) { + return $this->activeBranchCount($organization) < (int) $limit; + } + + $billed = $this->billedBranches($organization); + if ($this->hasPaidPlan($organization) && $billed > 0) { + return $this->activeBranchCount($organization) < $billed; + } + + return true; + } + + public function branchLimitMessage(Organization $organization): string + { + if ($this->hasPaidPlan($organization) && $this->billedBranches($organization) > 0) { + $n = $this->billedBranches($organization); + + return "Your plan is billed for {$n} active ".str('branch')->plural($n) + .'. Increase your branch allotment on the Plans page before adding more.'; + } + + return 'Your plan allows one branch. Upgrade to Frontdesk Pro for more branches.'; } public function canAddKiosk(Organization $organization): bool diff --git a/resources/views/frontdesk/pro/index.blade.php b/resources/views/frontdesk/pro/index.blade.php index 0b6f885..0349178 100644 --- a/resources/views/frontdesk/pro/index.blade.php +++ b/resources/views/frontdesk/pro/index.blade.php @@ -116,6 +116,19 @@ .
SMS host alerts bill at platform rates from your wallet.
+ @if ($canManage) + + @endif @elseif ($canManage) + @endif @elseif ($canManage) @if ($canSubscribeEnterprise)