diff --git a/app/Http/Controllers/Care/ProController.php b/app/Http/Controllers/Care/ProController.php index 51c806b..70f9157 100644 --- a/app/Http/Controllers/Care/ProController.php +++ b/app/Http/Controllers/Care/ProController.php @@ -62,12 +62,17 @@ class ProController extends Controller ->with('success', 'Your organization already has an active paid plan.'); } - $branches = max(1, $plans->activeBranchCount($organization)); + $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, - $plans->proPriceTotalMinor($organization), + $amountMinor, 'pro', 'care_pro', 'care-pro-'.$organization->id.'-'.now()->format('Y-m-d-His'), @@ -94,12 +99,17 @@ class ProController extends Controller ->with('error', "Enterprise requires at least {$min} active branch. Contact sales for custom multi-site onboarding."); } - $branches = max(1, $plans->activeBranchCount($organization)); + $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, - $plans->enterprisePriceMinor($organization), + $amountMinor, 'enterprise', 'care_enterprise', 'care-enterprise-'.$organization->id.'-'.now()->format('Y-m-d-His'), @@ -115,6 +125,7 @@ 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'], ]); $organization = $this->organization($request); @@ -133,8 +144,8 @@ class ProController extends Controller ->with('error', "Enterprise requires at least {$min} active branch."); } - $branches = max(1, $plans->activeBranchCount($organization)); - $monthlyMinor = $plans->monthlyPriceMinor($organization, $plan); + $branches = $plans->resolveCheckoutBranches($organization, (int) $validated['branches']); + $monthlyMinor = $plans->priceForBranches($plan, $branches); $amountMinor = $monthlyMinor * $months; try { diff --git a/app/Services/Care/PlanService.php b/app/Services/Care/PlanService.php index f161942..2a50a64 100644 --- a/app/Services/Care/PlanService.php +++ b/app/Services/Care/PlanService.php @@ -117,20 +117,38 @@ class PlanService /** Total monthly amount for Pro = active branches × per-branch rate (min 1). */ public function proPriceTotalMinor(Organization $organization): int { - return max(1, $this->activeBranchCount($organization)) * $this->proPricePerBranchMinor(); + return $this->priceForBranches('pro', $this->activeBranchCount($organization)); } /** Total monthly amount for Enterprise = active branches × per-branch rate (min 1). */ public function enterprisePriceMinor(Organization $organization): int { - return max(1, $this->activeBranchCount($organization)) * $this->enterprisePricePerBranchMinor(); + return $this->priceForBranches('enterprise', $this->activeBranchCount($organization)); } public function monthlyPriceMinor(Organization $organization, string $plan): int { - return $plan === 'enterprise' - ? $this->enterprisePriceMinor($organization) - : $this->proPriceTotalMinor($organization); + return $this->priceForBranches($plan, $this->activeBranchCount($organization)); + } + + /** Fixed branch count × per-branch rate (for checkout when the user picks branch seats). */ + public function priceForBranches(string $plan, int $branches): int + { + $branches = max(1, $branches); + $rate = $plan === 'enterprise' + ? $this->enterprisePricePerBranchMinor() + : $this->proPricePerBranchMinor(); + + return $branches * $rate; + } + + public function resolveCheckoutBranches(Organization $organization, int $requested): int + { + $branches = max(1, $requested); + // Don't under-bill vs sites already configured in the account. + $active = max(1, $this->activeBranchCount($organization)); + + return max($branches, $active); } public function canSubscribeEnterprise(Organization $organization): bool diff --git a/resources/views/care/pro/index.blade.php b/resources/views/care/pro/index.blade.php index 1f7f3e0..b328ed9 100644 --- a/resources/views/care/pro/index.blade.php +++ b/resources/views/care/pro/index.blade.php @@ -1,26 +1,48 @@ @php $proPerBranch = number_format($proPricePerBranchMinor / 100, 0); - $proTotal = number_format($proPriceMinor / 100, 0); $enterprisePerBranch = number_format($enterprisePricePerBranchMinor / 100, 0); - $enterpriseTotal = number_format($enterprisePriceMinor / 100, 0); + $initialBranches = max(1, (int) $branchCount); @endphp -
+
@if (session('success'))
{{ session('success') }}
@endif @if (session('error'))
{{ session('error') }}
@endif + @if (session('upsell')) +
{{ session('upsell') }}
+ @endif

Choose your Care plan

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

-
+
@@ -29,7 +51,20 @@ :class="billing === '{{ $months }}' ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'" class="rounded-lg px-3 py-1.5 text-sm font-medium transition">{{ $months }} months · Paystack @endforeach + +
+

+ Minimum is your current active branch count ({{ $initialBranches }}). Totals update live for Pro and Enterprise. +

@@ -57,8 +92,9 @@ :monthly-display="$proPerBranch" monthly-suffix="/branch/mo" /> -

- Unlimited branches · {{ $branchCount }} active = {{ $currency }} {{ $proTotal }}/mo +

+ branch(es) × {{ $currency }} {{ $proPerBranch }}/branch + = {{ $currency }} /mo

  • Everything in Free
  • @@ -83,15 +119,18 @@ @elseif ($canManage)
    @csrf - + +
    @else @@ -110,8 +149,9 @@ monthly-suffix="/branch/mo" dark /> -

    - Unlimited branches · {{ $branchCount }} active = {{ $currency }} {{ $enterpriseTotal }}/mo +

    + branch(es) × {{ $currency }} {{ $enterprisePerBranch }}/branch + = {{ $currency }} /mo

    • Everything in Pro
    • @@ -136,17 +176,18 @@ @if ($canSubscribeEnterprise)
      @csrf - + +
      @else diff --git a/tests/Feature/CareProTest.php b/tests/Feature/CareProTest.php index 2c418e5..fccac11 100644 --- a/tests/Feature/CareProTest.php +++ b/tests/Feature/CareProTest.php @@ -129,11 +129,12 @@ class CareProTest extends TestCase ]); $this->actingAs($this->owner) - ->post(route('care.pro.subscribe-enterprise')) + ->post(route('care.pro.subscribe-enterprise'), ['branches' => 1]) ->assertRedirect(route('care.pro.index')) ->assertSessionHas('success'); $this->assertSame('enterprise', $this->organization->fresh()->settings['plan']); + $this->assertSame(1, $this->organization->fresh()->settings['billed_branches']); } public function test_sidebar_shows_upgrade_to_pro_on_dashboard(): void @@ -185,13 +186,35 @@ class CareProTest extends TestCase ]); $this->actingAs($this->owner) - ->post(route('care.pro.subscribe')) + ->post(route('care.pro.subscribe'), ['branches' => 1]) ->assertRedirect(route('care.pro.index')) ->assertSessionHas('success'); $settings = $this->organization->fresh()->settings; $this->assertSame('pro', $settings['plan']); $this->assertSame('wallet_monthly', $settings['billing_method']); + $this->assertSame(1, $settings['billed_branches']); + } + + public function test_subscribe_charges_selected_branch_count(): void + { + $perBranch = (int) config('care.plans.pro.price_minor_per_branch'); + + Http::fake([ + 'billing.test/can-afford*' => Http::response(['affordable' => true]), + 'billing.test/debit' => function ($request) use ($perBranch) { + $this->assertSame($perBranch * 3, (int) $request['amount_minor']); + + return Http::response(['ok' => true]); + }, + ]); + + $this->actingAs($this->owner) + ->post(route('care.pro.subscribe'), ['branches' => 3]) + ->assertRedirect(route('care.pro.index')) + ->assertSessionHas('success'); + + $this->assertSame(3, $this->organization->fresh()->settings['billed_branches']); } public function test_subscribe_prepaid_redirects_to_paystack(): void @@ -207,8 +230,29 @@ class CareProTest extends TestCase ->post(route('care.pro.subscribe-prepaid'), [ 'plan' => 'pro', 'months' => 12, + 'branches' => 2, ]) ->assertRedirect('https://checkout.paystack.com/test'); + + Http::assertSent(function ($request) { + if (! str_contains($request->url(), 'plan-checkout')) { + return false; + } + + $perBranch = (int) config('care.plans.pro.price_minor_per_branch'); + + return (int) $request['amount_minor'] === $perBranch * 2 * 12 + && (int) ($request['metadata']['billed_branches'] ?? 0) === 2; + }); + } + + public function test_plans_page_shows_branch_selector(): void + { + $this->actingAs($this->owner) + ->get(route('care.pro.index')) + ->assertOk() + ->assertSee('Branches') + ->assertSee('x-model.number="branches"', false); } public function test_paystack_callback_activates_prepaid_pro(): void