withoutMiddleware(EnsurePlatformSession::class); config(['billing.api_url' => 'https://billing.test']); $this->owner = User::create([ 'public_id' => 'pro-owner-001', 'name' => 'Owner', 'email' => 'owner@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Pro Org', 'slug' => 'pro-org', 'settings' => ['onboarded' => true, 'badge_expiry_hours' => 8], ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, 'role' => 'org_admin', ]); Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'HQ', 'is_active' => true, ]); } public function test_pro_page_renders_three_tier_pricing(): void { $this->actingAs($this->owner) ->get(route('frontdesk.pro.index')) ->assertOk() ->assertSee('Choose your Frontdesk plan') ->assertSee('GHS 990') ->assertSee('/branch/mo') ->assertSee('GHS 1,990') ->assertSee('Enterprise') ->assertSee('Branches'); } public function test_sidebar_shows_upgrade_to_pro_on_dashboard(): void { $this->actingAs($this->owner) ->get(route('frontdesk.dashboard')) ->assertOk() ->assertSee('Upgrade to Pro') ->assertSee('Unlock Frontdesk Pro or Enterprise') ->assertSee('View plans'); } public function test_subscribe_upgrades_organization(): void { Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.pro.subscribe'), [ 'branches' => 1, ]) ->assertRedirect(route('frontdesk.pro.index')) ->assertSessionHas('success'); $settings = $this->organization->fresh()->settings; $this->assertSame('pro', $settings['plan']); $this->assertSame('wallet_monthly', $settings['billing_method']); $this->assertSame(1, (int) ($settings['billed_branches'] ?? 0)); } public function test_subscribe_defaults_branches_when_omitted(): void { $rate = app(PlanService::class)->proPricePerBranchMinor(); Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.settings.upgrade')) ->assertRedirect(route('frontdesk.pro.index')) ->assertSessionHas('success'); Http::assertSent(function ($request) use ($rate) { return $request->url() === 'https://billing.test/debit' && (int) $request['amount_minor'] === $rate; }); $this->assertSame(1, (int) ($this->organization->fresh()->settings['billed_branches'] ?? 0)); } public function test_subscribe_charges_per_branch_for_multiple_seats(): void { Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'East', 'is_active' => true, ]); Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'West', 'is_active' => true, ]); $rate = app(PlanService::class)->proPricePerBranchMinor(); $expected = $rate * 3; Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.pro.subscribe'), [ 'branches' => 3, ]) ->assertRedirect(route('frontdesk.pro.index')) ->assertSessionHas('success'); Http::assertSent(function ($request) use ($expected) { return $request->url() === 'https://billing.test/debit' && (int) $request['amount_minor'] === $expected && str_contains((string) $request['description'], '3 branch'); }); $this->assertSame(3, (int) ($this->organization->fresh()->settings['billed_branches'] ?? 0)); } public function test_subscribe_enterprise_charges_per_branch(): void { $rate = app(PlanService::class)->enterprisePricePerBranchMinor(); $expected = $rate * 2; Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.pro.subscribe-enterprise'), [ 'branches' => 2, ]) ->assertRedirect(route('frontdesk.pro.index')) ->assertSessionHas('success'); Http::assertSent(function ($request) use ($expected) { return $request->url() === 'https://billing.test/debit' && (int) $request['amount_minor'] === $expected && (string) $request['source'] === 'frontdesk_enterprise'; }); $settings = $this->organization->fresh()->settings; $this->assertSame('enterprise', $settings['plan']); $this->assertSame(2, (int) ($settings['billed_branches'] ?? 0)); } public function test_subscribe_prepaid_redirects_to_paystack(): void { Http::fake([ 'billing.test/plan-checkout' => Http::response([ 'checkout_url' => 'https://checkout.paystack.com/test', 'reference' => 'ref-123', ]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.pro.subscribe-prepaid'), [ 'plan' => 'pro', 'months' => 12, 'branches' => 1, ]) ->assertRedirect('https://checkout.paystack.com/test'); } public function test_subscribe_prepaid_sends_multi_branch_amount(): void { $rate = app(PlanService::class)->proPricePerBranchMinor(); $expected = $rate * 4 * 6; Http::fake([ 'billing.test/plan-checkout' => Http::response([ 'checkout_url' => 'https://checkout.paystack.com/multi', 'reference' => 'ref-multi', ]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.pro.subscribe-prepaid'), [ 'plan' => 'pro', 'months' => 6, 'branches' => 4, ]) ->assertRedirect('https://checkout.paystack.com/multi'); Http::assertSent(function ($request) use ($expected) { return str_ends_with($request->url(), '/plan-checkout') && (int) $request['amount_minor'] === $expected && (int) data_get($request, 'metadata.billed_branches') === 4; }); } public function test_paystack_callback_activates_prepaid_pro(): void { Http::fake([ 'billing.test/plan-checkout/verify' => Http::response([ 'paid' => true, 'plan' => 'pro', 'months' => 12, 'metadata' => [ 'organization_id' => $this->organization->id, 'billed_branches' => 3, ], ]), ]); $this->actingAs($this->owner) ->get(route('frontdesk.pro.paystack.callback', ['reference' => 'ref-123'])) ->assertRedirect(route('frontdesk.pro.index')) ->assertSessionHas('success'); $settings = $this->organization->fresh()->settings; $this->assertSame('pro', $settings['plan']); $this->assertFalse($settings['auto_renew']); $this->assertSame('paystack_prepaid', $settings['billing_method']); $this->assertSame(3, (int) ($settings['billed_branches'] ?? 0)); } public function test_pro_renew_extends_subscription_when_wallet_charges(): void { $this->organization->update([ 'settings' => [ 'onboarded' => true, 'plan' => 'pro', 'auto_renew' => true, 'billed_branches' => 1, 'plan_expires_at' => now()->subDay()->toIso8601String(), ], ]); Http::fake([ 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->artisan('frontdesk:pro-renew')->assertSuccessful(); $settings = $this->organization->fresh()->settings; $this->assertSame('pro', $settings['plan']); $this->assertTrue(now()->parse($settings['plan_expires_at'])->isFuture()); } public function test_pro_renew_charges_max_of_billed_and_active_branches(): void { Branch::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'name' => 'East', 'is_active' => true, ]); // 2 active branches; billed was 1 — true-up to 2. $rate = app(PlanService::class)->proPricePerBranchMinor(); $expected = $rate * 2; $this->organization->update([ 'settings' => [ 'onboarded' => true, 'plan' => 'pro', 'auto_renew' => true, 'billed_branches' => 1, 'plan_expires_at' => now()->subDay()->toIso8601String(), ], ]); Http::fake([ 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->artisan('frontdesk:pro-renew')->assertSuccessful(); Http::assertSent(function ($request) use ($expected) { return $request->url() === 'https://billing.test/debit' && (int) $request['amount_minor'] === $expected && str_contains((string) $request['description'], '2 branch'); }); $this->assertSame(2, (int) ($this->organization->fresh()->settings['billed_branches'] ?? 0)); } public function test_prepaid_pro_skipped_by_renewal_command(): void { $this->organization->update([ 'settings' => [ 'onboarded' => true, 'plan' => 'pro', 'auto_renew' => false, 'billing_method' => 'paystack_prepaid', 'plan_expires_at' => now()->subDay()->toIso8601String(), ], ]); Http::fake(); $this->artisan('frontdesk:pro-renew')->assertSuccessful(); Http::assertNothingSent(); } public function test_pro_sidebar_shows_active_label_after_upgrade(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); $this->actingAs($this->owner) ->get(route('frontdesk.dashboard')) ->assertOk() ->assertSee('Frontdesk Pro') ->assertDontSee('Upgrade to Pro'); } public function test_enterprise_sidebar_shows_enterprise_label(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'enterprise', 'plan_expires_at' => now()->addYear()->toIso8601String(), ]), ]); $this->actingAs($this->owner) ->get(route('frontdesk.dashboard')) ->assertOk() ->assertSee('Frontdesk Enterprise') ->assertDontSee('Upgrade to Pro'); } public function test_settings_shows_enterprise_as_paid_not_free(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'enterprise', 'billed_branches' => 2, 'plan_expires_at' => now()->addYear()->toIso8601String(), ]), ]); $this->actingAs($this->owner) ->get(route('frontdesk.settings')) ->assertOk() ->assertSee('Enterprise') ->assertSee('Billed for 2') ->assertSee('Manage plan') ->assertDontSee('Upgrade to Pro'); } public function test_paid_plan_blocks_branch_beyond_allotment(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'billed_branches' => 1, 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.branches.store'), [ 'name' => 'Overflow Branch', ]) ->assertRedirect() ->assertSessionHas('error'); $this->assertDatabaseMissing('frontdesk_branches', ['name' => 'Overflow Branch']); } public function test_expand_branches_charges_extra_seats(): void { $rate = app(PlanService::class)->proPricePerBranchMinor(); $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'billed_branches' => 1, 'billing_method' => 'wallet_monthly', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.pro.expand-branches'), [ 'branches' => 3, ]) ->assertRedirect(route('frontdesk.pro.index')) ->assertSessionHas('success'); Http::assertSent(function ($request) use ($rate) { return $request->url() === 'https://billing.test/debit' && (int) $request['amount_minor'] === $rate * 2 && str_contains((string) $request['source'], 'branch_expand'); }); $this->assertSame(3, (int) ($this->organization->fresh()->settings['billed_branches'] ?? 0)); } public function test_after_expand_can_create_additional_branch(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'billed_branches' => 2, 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); $this->actingAs($this->owner) ->post(route('frontdesk.branches.store'), [ 'name' => 'Second Branch', ]) ->assertRedirect(route('frontdesk.branches.index')) ->assertSessionHas('success'); $this->assertDatabaseHas('frontdesk_branches', ['name' => 'Second Branch']); } }