diff --git a/app/Http/Controllers/Qms/ProController.php b/app/Http/Controllers/Qms/ProController.php index 2f6b4d3..43a8807 100644 --- a/app/Http/Controllers/Qms/ProController.php +++ b/app/Http/Controllers/Qms/ProController.php @@ -27,7 +27,11 @@ class ProController extends Controller : 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, @@ -36,7 +40,8 @@ class ProController extends Controller 'isEnterprise' => $planKey === 'enterprise', 'hasPaidPlan' => $plans->hasPaidPlan($organization), 'canManage' => $canManage, - 'proPriceMinor' => $plans->proPriceMinor(), + 'proPricePerBranchMinor' => $plans->proPricePerBranchMinor(), + 'proPriceMinor' => $proPrice, 'enterprisePricePerBranchMinor' => $plans->enterprisePricePerBranchMinor(), 'enterprisePriceMinor' => $enterprisePrice, 'branchCount' => $branchCount, @@ -45,7 +50,9 @@ class ProController extends Controller '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'), ]); } @@ -63,12 +70,13 @@ class ProController extends Controller return $this->chargeMonthlyWallet( $organization, $billing, - $plans->proPriceMinor(), + $plans->proPriceMinor($organization), '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.', + $plans->activeBranchCount($organization), ); } @@ -128,9 +136,19 @@ class ProController extends Controller $monthlyMinor = $plan === 'enterprise' ? $plans->enterprisePriceMinor($organization) - : $plans->proPriceMinor(); + : $plans->proPriceMinor($organization); $amountMinor = $monthlyMinor * $months; - $branches = $plan === 'enterprise' ? $plans->activeBranchCount($organization) : null; + $branches = $plans->activeBranchCount($organization); + + $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( @@ -139,10 +157,7 @@ class ProController extends Controller $months, $amountMinor, route('qms.pro.paystack.callback'), - array_filter([ - 'organization_id' => $organization->id, - 'enterprise_billed_branches' => $branches, - ], static fn ($v) => $v !== null), + $metadata, ); } catch (\Throwable) { return redirect()->route('qms.pro.index') @@ -191,9 +206,13 @@ class ProController extends Controller $plan = (string) ($result['plan'] ?? 'pro'); $months = (int) ($result['months'] ?? 0); - $branches = isset($metadata['enterprise_billed_branches']) - ? (int) $metadata['enterprise_billed_branches'] - : null; + $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); @@ -212,7 +231,7 @@ class ProController extends Controller string $reference, string $description, string $successMessage, - ?int $enterpriseBranches = null, + ?int $billedBranches = null, ): RedirectResponse { try { if (! $billing->canAfford($organization->owner_ref, $priceMinor)) { @@ -244,9 +263,7 @@ class ProController extends Controller $settings['plan_expires_at'] = now() ->addDays((int) config('qms.pro.period_days', 30)) ->toIso8601String(); - if ($enterpriseBranches !== null) { - $settings['enterprise_billed_branches'] = $enterpriseBranches; - } + $this->applyBilledBranches($settings, $plan, $billedBranches); unset($settings['plan_renewal_error']); $organization->update(['settings' => $settings]); @@ -257,17 +274,33 @@ class ProController extends Controller Organization $organization, string $plan, int $months, - ?int $enterpriseBranches = null, + ?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(); - if ($enterpriseBranches !== null) { - $settings['enterprise_billed_branches'] = $enterpriseBranches; - } + $this->applyBilledBranches($settings, $plan, $billedBranches); unset($settings['plan_renewal_error']); $organization->update(['settings' => $settings]); } + + /** + * @param array $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']); + } + } } diff --git a/app/Services/Qms/PlanService.php b/app/Services/Qms/PlanService.php index ada08ac..87ec06c 100644 --- a/app/Services/Qms/PlanService.php +++ b/app/Services/Qms/PlanService.php @@ -74,9 +74,19 @@ class PlanService return $max === null || $currentCount < $max; } - public function proPriceMinor(): int + public function proPricePerBranchMinor(): int { - return (int) config('qms.plans.pro.price_minor', 249000); + return (int) config( + 'qms.plans.pro.price_minor_per_branch', + config('qms.plans.pro.price_minor', 249000) + ); + } + + public function proPriceMinor(Organization $organization): int + { + $branches = max(1, $this->activeBranchCount($organization)); + + return $branches * $this->proPricePerBranchMinor(); } public function enterprisePricePerBranchMinor(): int diff --git a/app/Services/Qms/ProRenewalService.php b/app/Services/Qms/ProRenewalService.php index e527661..5af6783 100644 --- a/app/Services/Qms/ProRenewalService.php +++ b/app/Services/Qms/ProRenewalService.php @@ -52,9 +52,10 @@ class ProRenewalService $isEnterprise = $plan === 'enterprise'; $price = $isEnterprise ? $this->plans->enterprisePriceMinor($organization) - : $this->plans->proPriceMinor(); + : $this->plans->proPriceMinor($organization); $reference = ($isEnterprise ? 'queue-enterprise-' : 'queue-pro-') .$organization->id.'-'.now()->format('YmdHis'); + $branchCount = $this->plans->activeBranchCount($organization); try { $charged = $this->billing->debit( @@ -77,7 +78,11 @@ class ProRenewalService ->addDays((int) config('qms.pro.period_days', 30)) ->toIso8601String(); if ($isEnterprise) { - $settings['enterprise_billed_branches'] = $this->plans->activeBranchCount($organization); + $settings['enterprise_billed_branches'] = $branchCount; + unset($settings['pro_billed_branches']); + } else { + $settings['pro_billed_branches'] = $branchCount; + unset($settings['enterprise_billed_branches']); } unset($settings['plan_renewal_error']); $organization->update(['settings' => $settings]); @@ -89,7 +94,11 @@ class ProRenewalService $graceEnd = $expires->copy()->addDays((int) config('qms.pro.grace_days', 3)); if ($graceEnd->isPast()) { $settings['plan'] = 'free'; - unset($settings['plan_expires_at'], $settings['enterprise_billed_branches']); + unset( + $settings['plan_expires_at'], + $settings['enterprise_billed_branches'], + $settings['pro_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 4eceb31..02f5516 100644 --- a/config/qms.php +++ b/config/qms.php @@ -183,7 +183,10 @@ return [ ], 'pro' => [ 'label' => 'Pro', - 'price_minor' => (int) env('QUEUE_PRO_PRICE_MINOR', 249000), + 'price_minor_per_branch' => (int) env( + 'QUEUE_PRO_PRICE_PER_BRANCH_MINOR', + env('QUEUE_PRO_PRICE_MINOR', 249000) + ), 'max_branches' => null, 'max_queues' => null, ], @@ -249,7 +252,7 @@ return [ 'upgrade_banner' => [ 'title' => 'Unlock Queue Pro or Enterprise', - 'description' => 'Unlimited branches, advanced routing, Care & Frontdesk integrations — from GHS 2490/mo.', + 'description' => 'Unlimited branches, advanced routing, Care & Frontdesk integrations — from GHS 2490/branch/mo.', 'route' => 'qms.pro.index', ], diff --git a/resources/views/qms/pro/index.blade.php b/resources/views/qms/pro/index.blade.php index 9de2239..3e60c7a 100644 --- a/resources/views/qms/pro/index.blade.php +++ b/resources/views/qms/pro/index.blade.php @@ -1,6 +1,7 @@ @php - $proPrice = number_format($proPriceMinor / 100, 0); + $proPerBranch = number_format($proPricePerBranchMinor / 100, 0); + $proTotal = number_format($proPriceMinor / 100, 0); $enterprisePerBranch = number_format($enterprisePricePerBranchMinor / 100, 0); $enterpriseTotal = number_format($enterprisePriceMinor / 100, 0); @endphp @@ -17,6 +18,7 @@

Choose your Queue plan

Pay monthly from your Ladill wallet, or prepay 6/12/24 months via Paystack. + Pro and Enterprise are billed per active branch. Enterprise includes a one-time setup fee — contact sales for onboarding.

@@ -50,8 +52,20 @@ {{-- Pro --}}

Pro

- -

Unlimited branches & queues

+ +

+ @if ($branchCount > 0) + {{ $branchCount }} active {{ str('branch')->plural($branchCount) }} = {{ $currency }} {{ $proTotal }}/mo + @else + Per-branch billing + @endif +

  • Advanced routing rules
  • Unlimited kiosks & displays
  • @@ -66,19 +80,25 @@ @if ($planExpiresAt) until {{ $planExpiresAt->format('d M Y') }} @endif + @if ($proBilledBranches > 0) + · billed for {{ $proBilledBranches }} {{ str('branch')->plural($proBilledBranches) }} + @endif .

    @elseif ($canManage)
    @csrf - +
    @else diff --git a/tests/Feature/QmsProTest.php b/tests/Feature/QmsProTest.php index 0215ee5..ca7e61a 100644 --- a/tests/Feature/QmsProTest.php +++ b/tests/Feature/QmsProTest.php @@ -44,12 +44,16 @@ class QmsProTest extends TestCase public function test_pro_page_renders_three_tier_pricing(): void { + $proPerBranch = number_format((int) config('qms.plans.pro.price_minor_per_branch') / 100, 0); + $enterprisePerBranch = number_format((int) config('qms.plans.enterprise.price_minor_per_branch') / 100, 0); + $this->actingAs($this->owner) ->get(route('qms.pro.index')) ->assertOk() ->assertSee('Choose your Queue plan') - ->assertSee('GHS 99') - ->assertSee('GHS 299') + ->assertSee('GHS '.$proPerBranch) + ->assertSee('GHS '.$enterprisePerBranch) + ->assertSee('/branch/mo') ->assertSee('Upgrade to Pro') ->assertSee('Enterprise'); } @@ -109,7 +113,9 @@ class QmsProTest extends TestCase ->assertRedirect(route('qms.pro.index')) ->assertSessionHas('success'); - $this->assertSame('pro', $this->organization->fresh()->settings['plan']); + $settings = $this->organization->fresh()->settings; + $this->assertSame('pro', $settings['plan']); + $this->assertSame(1, $settings['pro_billed_branches']); } public function test_subscribe_enterprise_requires_multiple_branches(): void @@ -147,21 +153,64 @@ class QmsProTest extends TestCase 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'auto_renew' => true, + 'pro_billed_branches' => 1, 'plan_expires_at' => now()->subDay()->toIso8601String(), ]), ]); + $expected = (int) config('qms.plans.pro.price_minor_per_branch'); + Http::fake([ - 'billing.test/debit' => Http::response(['ok' => true]), + 'billing.test/debit' => function ($request) use ($expected) { + $this->assertSame($expected, (int) $request['amount_minor']); + + return Http::response(['ok' => true]); + }, ]); $this->artisan('qms:pro-renew')->assertSuccessful(); $settings = $this->organization->fresh()->settings; $this->assertSame('pro', $settings['plan']); + $this->assertSame(1, $settings['pro_billed_branches']); $this->assertTrue(now()->parse($settings['plan_expires_at'])->isFuture()); } + public function test_pro_renew_charges_per_active_branch(): void + { + Branch::create([ + 'owner_ref' => $this->organization->owner_ref, + 'organization_id' => $this->organization->id, + 'name' => 'Second', + 'is_active' => true, + ]); + + $this->organization->update([ + 'settings' => array_merge($this->organization->settings ?? [], [ + 'plan' => 'pro', + 'auto_renew' => true, + 'pro_billed_branches' => 1, + 'plan_expires_at' => now()->subDay()->toIso8601String(), + ]), + ]); + + $expected = 2 * (int) config('qms.plans.pro.price_minor_per_branch'); + + Http::fake([ + 'billing.test/debit' => function ($request) use ($expected) { + $this->assertSame($expected, (int) $request['amount_minor']); + + return Http::response(['ok' => true]); + }, + ]); + + $this->artisan('qms:pro-renew')->assertSuccessful(); + + $settings = $this->organization->fresh()->settings; + $this->assertSame('pro', $settings['plan']); + $this->assertSame(2, $settings['pro_billed_branches']); + } + public function test_enterprise_renew_charges_per_active_branch(): void { Branch::create([ @@ -180,9 +229,11 @@ class QmsProTest extends TestCase ]), ]); + $expected = 2 * (int) config('qms.plans.enterprise.price_minor_per_branch'); + Http::fake([ - 'billing.test/debit' => function ($request) { - $this->assertSame(59800, (int) $request['amount_minor']); + 'billing.test/debit' => function ($request) use ($expected) { + $this->assertSame($expected, (int) $request['amount_minor']); return Http::response(['ok' => true]); },