Files
ladill-queue/app/Http/Controllers/Qms/ProController.php
T
isaacclad 8be3eaeb4b
Deploy Ladill Queue / deploy (push) Successful in 33s
Add branch count control to Queue plans page.
Match Care/Frontdesk: branches stepper next to billing period, live
totals, and checkout charges for the selected seat count.
2026-07-16 09:36:36 +00:00

319 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers\Qms;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Qms\Concerns\ScopesToAccount;
use App\Models\Organization;
use App\Services\Billing\BillingClient;
use App\Services\Qms\PlanService;
use App\Services\Qms\QmsPermissions;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\View\View;
class ProController extends Controller
{
use ScopesToAccount;
public function index(Request $request, PlanService $plans): View
{
$organization = $this->organization($request);
$canManage = app(QmsPermissions::class)->can($this->member($request), 'settings.manage');
$settings = $organization->settings ?? [];
$expiresAt = ! empty($settings['plan_expires_at'])
? Carbon::parse($settings['plan_expires_at'])
: null;
$planKey = $plans->planKey($organization);
$branchCount = $plans->activeBranchCount($organization);
$proPrice = $plans->proPriceMinor($organization);
$enterprisePrice = $plans->enterprisePriceMinor($organization);
$billedBranches = (int) ($settings['pro_billed_branches']
?? $settings['enterprise_billed_branches']
?? $branchCount);
return view('qms.pro.index', [
'organization' => $organization,
'planKey' => $planKey,
'isPro' => $planKey === 'pro',
'isEnterprise' => $planKey === 'enterprise',
'hasPaidPlan' => $plans->hasPaidPlan($organization),
'canManage' => $canManage,
'proPricePerBranchMinor' => $plans->proPricePerBranchMinor(),
'proPriceMinor' => $proPrice,
'enterprisePricePerBranchMinor' => $plans->enterprisePricePerBranchMinor(),
'enterprisePriceMinor' => $enterprisePrice,
'branchCount' => $branchCount,
'canSubscribeEnterprise' => $plans->canSubscribeEnterprise($organization),
'enterpriseMinBranches' => (int) config('qms.enterprise.min_branches', 2),
'prepaidMonths' => (array) config('qms.prepaid_months', [6, 12, 24]),
'currency' => (string) config('billing.currency', 'GHS'),
'planExpiresAt' => $expiresAt,
'proBilledBranches' => (int) ($settings['pro_billed_branches'] ?? $branchCount),
'enterpriseBilledBranches' => (int) ($settings['enterprise_billed_branches'] ?? $branchCount),
'billedBranches' => $billedBranches,
'salesUrl' => ladill_platform_url('contact-sales'),
]);
}
public function subscribe(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.');
}
$validated = $request->validate([
'branches' => ['required', 'integer', 'min:1', 'max:999'],
]);
$branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']);
$amountMinor = $plans->priceForBranches('pro', $branches);
return $this->chargeMonthlyWallet(
$organization,
$billing,
$amountMinor,
'pro',
'queue_pro',
'queue-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'),
'Ladill Queue Pro — '.$branches.' branch(es) monthly',
'Welcome to Queue Pro — active for one month.',
$branches,
);
}
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.");
}
$validated = $request->validate([
'branches' => ['required', 'integer', 'min:1', 'max:999'],
]);
$branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']);
$amountMinor = $plans->priceForBranches('enterprise', $branches);
return $this->chargeMonthlyWallet(
$organization,
$billing,
$amountMinor,
'enterprise',
'queue_enterprise',
'queue-enterprise-'.$organization->id.'-'.now()->format('Y-m-d-His'),
'Ladill Queue Enterprise — '.$branches.' branch(es) monthly',
'Welcome to Queue Enterprise — active for one month.',
$branches,
);
}
public function subscribePrepaid(Request $request, PlanService $plans, BillingClient $billing): RedirectResponse
{
$this->authorizeAbility($request, 'settings.manage');
$validated = $request->validate([
'plan' => ['required', 'in:pro,enterprise'],
'months' => ['required', 'integer', 'in:6,12,24'],
'branches' => ['required', 'integer', 'min:1', 'max:999'],
]);
$organization = $this->organization($request);
if ($plans->hasPaidPlan($organization)) {
return redirect()->route('qms.pro.index')
->with('success', 'Your organization already has an active paid plan.');
}
$plan = $validated['plan'];
$months = (int) $validated['months'];
if ($plan === 'enterprise' && ! $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.");
}
$branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']);
$amountMinor = $plans->priceForBranches($plan, $branches) * $months;
$metadata = [
'organization_id' => $organization->id,
'billed_branches' => $branches,
];
if ($plan === 'enterprise') {
$metadata['enterprise_billed_branches'] = $branches;
} else {
$metadata['pro_billed_branches'] = $branches;
}
try {
$checkout = $billing->initiatePlanCheckout(
$organization->owner_ref,
$plan,
$months,
$amountMinor,
route('qms.pro.paystack.callback'),
$metadata,
);
} catch (\Throwable) {
return redirect()->route('qms.pro.index')
->with('error', 'Could not start Paystack checkout. Please try again.');
}
$url = trim((string) ($checkout['checkout_url'] ?? ''));
if ($url === '') {
return redirect()->route('qms.pro.index')
->with('error', 'Paystack did not return a checkout URL.');
}
return redirect()->away($url);
}
public function paystackCallback(Request $request, BillingClient $billing, PlanService $plans): RedirectResponse
{
$reference = (string) $request->query('reference', '');
if ($reference === '') {
return redirect()->route('qms.pro.index')->with('error', 'Missing payment reference.');
}
try {
$result = $billing->verifyPlanCheckout($reference);
} catch (\Throwable) {
return redirect()->route('qms.pro.index')
->with('error', 'Could not verify payment. Contact support with reference: '.$reference);
}
if (! ($result['paid'] ?? false)) {
return redirect()->route('qms.pro.index')
->with('error', 'Payment was not completed.');
}
$metadata = (array) ($result['metadata'] ?? []);
$organization = Organization::query()->find((int) ($metadata['organization_id'] ?? 0));
if (! $organization) {
return redirect()->route('qms.pro.index')
->with('error', 'Organization not found for this payment.');
}
$this->authorizeAbility($request, 'settings.manage');
if ($this->organization($request)->id !== $organization->id) {
abort(403);
}
$plan = (string) ($result['plan'] ?? 'pro');
$months = (int) ($result['months'] ?? 0);
$branches = isset($metadata['billed_branches'])
? (int) $metadata['billed_branches']
: (isset($metadata['pro_billed_branches'])
? (int) $metadata['pro_billed_branches']
: (isset($metadata['enterprise_billed_branches'])
? (int) $metadata['enterprise_billed_branches']
: null));
$this->activatePrepaidPlan($organization, $plan, $months, $branches);
$label = $plan === 'enterprise' ? 'Queue Enterprise' : 'Queue Pro';
return redirect()->route('qms.pro.index')
->with('success', "{$label} is active for {$months} months.");
}
private function chargeMonthlyWallet(
Organization $organization,
BillingClient $billing,
int $priceMinor,
string $plan,
string $billingSource,
string $reference,
string $description,
string $successMessage,
?int $billedBranches = 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.');
}
$charged = $billing->debit(
$organization->owner_ref,
$priceMinor,
$billingSource,
$reference,
$description,
);
} catch (\Throwable) {
return redirect()->route('qms.pro.index')
->with('error', 'Could not process upgrade. Please try again.');
}
if (! $charged) {
return redirect()->route('qms.pro.index')
->with('error', 'Insufficient wallet balance. Top up your Ladill wallet to upgrade.');
}
$settings = $organization->settings ?? [];
$settings['plan'] = $plan;
$settings['auto_renew'] = true;
$settings['billing_method'] = 'wallet_monthly';
$settings['plan_expires_at'] = now()
->addDays((int) config('qms.pro.period_days', 30))
->toIso8601String();
$this->applyBilledBranches($settings, $plan, $billedBranches);
unset($settings['plan_renewal_error']);
$organization->update(['settings' => $settings]);
return redirect()->route('qms.pro.index')->with('success', $successMessage);
}
private function activatePrepaidPlan(
Organization $organization,
string $plan,
int $months,
?int $billedBranches = null,
): void {
$settings = $organization->settings ?? [];
$settings['plan'] = $plan;
$settings['auto_renew'] = false;
$settings['billing_method'] = 'paystack_prepaid';
$settings['plan_expires_at'] = now()->addMonths($months)->toIso8601String();
$this->applyBilledBranches($settings, $plan, $billedBranches);
unset($settings['plan_renewal_error']);
$organization->update(['settings' => $settings]);
}
/**
* @param array<string, mixed> $settings
*/
private function applyBilledBranches(array &$settings, string $plan, ?int $billedBranches): void
{
if ($billedBranches === null) {
return;
}
if ($plan === 'enterprise') {
$settings['enterprise_billed_branches'] = $billedBranches;
unset($settings['pro_billed_branches']);
} else {
$settings['pro_billed_branches'] = $billedBranches;
unset($settings['enterprise_billed_branches']);
}
}
}