Files
ladill-care/tests/Feature/CareProTest.php
T
isaaccladandCursor 62d42d9157
Deploy Ladill Care / deploy (push) Successful in 1m2s
Show Care Enterprise upgrade banner to Pro users.
Pro dashboards now pitch Enterprise instead of the Free unlock copy, and plan context is shared with the banner so Pro tenants no longer see Free messaging.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 11:08:13 +00:00

440 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Branch;
use App\Models\Member;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CareProTest extends TestCase
{
use RefreshDatabase;
protected User $owner;
protected Organization $organization;
protected function setUp(): void
{
parent::setUp();
$this->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('2490')
->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_pro_dashboard_shows_enterprise_upgrade_banner(): void
{
$settings = $this->organization->settings ?? [];
$settings['plan'] = 'pro';
$settings['plan_expires_at'] = now()->addMonth()->toIso8601String();
$settings['billed_branches'] = 1;
$this->organization->update(['settings' => $settings]);
$this->actingAs($this->owner)
->get(route('care.dashboard'))
->assertOk()
->assertSee('Upgrade to Care Enterprise')
->assertSee('View Enterprise')
->assertDontSee('Unlock Care Pro or Enterprise')
->assertSee('Care Pro');
}
public function test_enterprise_dashboard_hides_upgrade_banner(): void
{
$settings = $this->organization->settings ?? [];
$settings['plan'] = 'enterprise';
$settings['plan_expires_at'] = now()->addMonth()->toIso8601String();
$settings['billed_branches'] = 1;
$this->organization->update(['settings' => $settings]);
$this->actingAs($this->owner)
->get(route('care.dashboard'))
->assertOk()
->assertDontSee('Unlock Care Pro or Enterprise')
->assertDontSee('Upgrade to Care Enterprise')
->assertDontSee('View Enterprise')
->assertSee('Care Enterprise');
}
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('Upgrade to Care 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']);
}
}