withoutMiddleware(EnsurePlatformSession::class); config(['billing.api_url' => 'https://billing.test']); $this->owner = User::create([ 'public_id' => 'care-owner-001', 'name' => 'Owner', 'email' => 'owner@example.com', ]); $this->organization = Organization::create([ 'owner_ref' => $this->owner->public_id, 'name' => 'Care Org', 'slug' => 'care-org', 'settings' => ['onboarded' => true, 'facility_type' => 'clinic'], ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, 'role' => 'hospital_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('care.pro.index')) ->assertOk() ->assertSee('Choose your Care plan') ->assertSee('1990') ->assertSee('4990') ->assertSee('/branch/mo') ->assertSee('Unlimited branches') ->assertSee('Core patient records') ->assertSee('Queue integration') ->assertSee('Custom multi-department workflow') ->assertSee('AI-assisted healthcare integration') ->assertSee('Upgrade to Pro') ->assertSee('Enterprise'); } public function test_free_plan_cannot_enable_queue_integration(): void { $this->actingAs($this->owner) ->put(route('care.settings.update'), [ 'name' => 'Care Org', 'timezone' => 'Africa/Accra', 'facility_type' => 'clinic', 'queue_integration_enabled' => '1', ]) ->assertRedirect(route('care.pro.index')) ->assertSessionHas('upsell'); $this->organization->refresh(); $this->assertFalse((bool) data_get($this->organization->settings, 'queue_integration_enabled')); } public function test_free_plan_is_blocked_from_lab_module(): void { $this->actingAs($this->owner) ->get(route('care.lab.queue.index')) ->assertRedirect(route('care.pro.index')) ->assertSessionHas('upsell'); } public function test_free_plan_is_blocked_from_branch_management(): void { $this->actingAs($this->owner) ->get(route('care.branches.index')) ->assertOk() ->assertSee('Upgrade your plan') ->assertDontSee('All branches'); $this->actingAs($this->owner) ->post(route('care.branches.store'), [ 'name' => 'Second site', ]) ->assertRedirect(route('care.pro.index')) ->assertSessionHas('upsell'); } public function test_pro_plan_can_manage_branches(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); $this->actingAs($this->owner) ->get(route('care.branches.index')) ->assertOk() ->assertSee('All branches') ->assertSee('HQ'); } public function test_pro_plan_allows_unlimited_branches(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'pro', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); $this->assertNull(app(\App\Services\Care\PlanService::class)->maxBranches($this->organization->fresh())); $this->assertTrue(app(\App\Services\Care\PlanService::class)->canAddBranch($this->organization->fresh(), 50)); } public function test_enterprise_plan_allows_unlimited_branches(): void { $this->organization->update([ 'settings' => array_merge($this->organization->settings ?? [], [ 'plan' => 'enterprise', 'plan_expires_at' => now()->addMonth()->toIso8601String(), ]), ]); $this->assertNull(app(\App\Services\Care\PlanService::class)->maxBranches($this->organization->fresh())); $this->assertTrue(app(\App\Services\Care\PlanService::class)->canAddBranch($this->organization->fresh(), 50)); } public function test_subscribe_enterprise_no_longer_requires_multiple_branches(): void { Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->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 { $this->actingAs($this->owner) ->get(route('care.dashboard')) ->assertOk() ->assertSee('Upgrade to Pro') ->assertSee('Unlock Care Pro or Enterprise') ->assertSee('View plans'); } public function test_team_member_does_not_see_upgrade_banner(): void { $member = User::create([ 'public_id' => 'care-doctor-001', 'name' => 'Doctor', 'email' => 'doctor@example.com', ]); Member::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, 'user_ref' => $member->public_id, 'role' => 'doctor', ]); $this->actingAs($member); \App\Support\StaffUx::remember([ 'full_access' => false, 'apps' => ['care'], 'show_hub' => false, 'show_billing' => false, ]); $this->get(route('care.dashboard')) ->assertOk() ->assertDontSee('Unlock Care Pro or Enterprise') ->assertDontSee('View plans') ->assertDontSee('Upgrade to Pro') ->assertSee('Report Issue'); } public function test_subscribe_upgrades_organization_to_pro(): void { Http::fake([ 'billing.test/can-afford*' => Http::response(['affordable' => true]), 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->actingAs($this->owner) ->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 { Http::fake([ 'billing.test/plan-checkout' => Http::response([ 'checkout_url' => 'https://checkout.paystack.com/test', 'reference' => 'ref-123', ]), ]); $this->actingAs($this->owner) ->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 { Http::fake([ 'billing.test/plan-checkout/verify' => Http::response([ 'paid' => true, 'plan' => 'pro', 'months' => 12, 'metadata' => ['organization_id' => $this->organization->id], ]), ]); $this->actingAs($this->owner) ->get(route('care.pro.paystack.callback', ['reference' => 'ref-123'])) ->assertRedirect(route('care.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']); } public function test_pro_renew_extends_subscription_when_wallet_charges(): void { $this->organization->update([ 'settings' => [ 'onboarded' => true, 'facility_type' => 'clinic', 'plan' => 'pro', 'auto_renew' => true, 'plan_expires_at' => now()->subDay()->toIso8601String(), ], ]); Http::fake([ 'billing.test/debit' => Http::response(['ok' => true]), ]); $this->artisan('care: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_enterprise_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' => 'enterprise', 'auto_renew' => true, 'plan_expires_at' => now()->subDay()->toIso8601String(), ]), ]); $perBranch = (int) config('care.plans.enterprise.price_minor_per_branch'); $expected = $perBranch * 2; Http::fake([ 'billing.test/debit' => function ($request) use ($expected) { $this->assertSame($expected, (int) $request['amount_minor']); return Http::response(['ok' => true]); }, ]); $this->artisan('care:pro-renew')->assertSuccessful(); $settings = $this->organization->fresh()->settings; $this->assertSame('enterprise', $settings['plan']); $this->assertSame(2, $settings['billed_branches']); } 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, 'plan_expires_at' => now()->subDay()->toIso8601String(), ]), ]); $perBranch = (int) config('care.plans.pro.price_minor_per_branch'); $expected = $perBranch * 2; Http::fake([ 'billing.test/debit' => function ($request) use ($expected) { $this->assertSame($expected, (int) $request['amount_minor']); return Http::response(['ok' => true]); }, ]); $this->artisan('care:pro-renew')->assertSuccessful(); $settings = $this->organization->fresh()->settings; $this->assertSame('pro', $settings['plan']); $this->assertSame(2, $settings['billed_branches']); } }