create(); $starter = $this->makeProduct([ 'name' => 'Starter Plan', 'slug' => 'starter-upsell-api', 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'price_monthly' => 1, 'disk_gb' => 5, 'max_domains' => 1, 'max_email_accounts' => 5, 'sort_order' => 1, ]); $basic = $this->makeProduct([ 'name' => 'Basic Plan', 'slug' => 'basic-upsell-api', 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'price_monthly' => 15, 'disk_gb' => 25, 'max_domains' => 1, 'max_email_accounts' => 10, 'sort_order' => 2, ]); $account = $this->makeAccount($user, $starter, [ 'primary_domain' => 'starter-upsell.test', 'disk_used_bytes' => (int) round(5 * 0.8 * 1073741824), ]); $this->actingAs($user) ->getJson('/api/upsells/recommendations') ->assertOk() ->assertJsonPath('data.0.type', 'plan_upgrade') ->assertJsonPath('data.0.account_id', $account->id) ->assertJsonPath('data.0.target_product_id', $basic->id) ->assertJsonPath('data.0.target_product_name', 'Basic Plan'); } public function test_api_returns_email_addon_recommendation_when_mailbox_usage_exceeds_allowance(): void { $user = User::factory()->create(); $product = $this->makeProduct([ 'name' => 'Growth Plan', 'slug' => 'growth-upsell-email', 'max_email_accounts' => 1, ]); $account = $this->makeAccount($user, $product, [ 'primary_domain' => 'mailbox-upsell.test', ]); $domain = $this->makeDomain($user, $account, 'mailbox-upsell.test'); Mailbox::create([ 'user_id' => $user->id, 'hosting_account_id' => $account->id, 'domain_id' => $domain->id, 'display_name' => 'Info', 'local_part' => 'info', 'address' => 'info@mailbox-upsell.test', 'password_hash' => Hash::make('password123'), 'quota_mb' => 2048, 'status' => 'active', 'maildir_path' => 'hosting/1/mailbox-upsell.test/info', 'credentials_issued_at' => now(), 'incoming_token' => 'upsell-token-1', 'inbound_enabled' => true, 'outbound_enabled' => true, 'outbound_provider' => 'smtp', 'is_paid' => false, ]); Mailbox::create([ 'user_id' => $user->id, 'hosting_account_id' => $account->id, 'domain_id' => $domain->id, 'display_name' => 'Sales', 'local_part' => 'sales', 'address' => 'sales@mailbox-upsell.test', 'password_hash' => Hash::make('password123'), 'quota_mb' => 2048, 'status' => 'active', 'maildir_path' => 'hosting/1/mailbox-upsell.test/sales', 'credentials_issued_at' => now(), 'incoming_token' => 'upsell-token-2', 'inbound_enabled' => true, 'outbound_enabled' => true, 'outbound_provider' => 'smtp', 'is_paid' => true, ]); $this->actingAs($user) ->getJson('/api/upsells/recommendations') ->assertOk() ->assertJsonFragment([ 'type' => 'email_addon', 'account_id' => $account->id, 'suggested_quantity' => 10, ]) ->assertJsonFragment([ 'estimated_amount' => 50.0, ]); } public function test_account_page_renders_recommendation_and_web_plan_change_route_applies_upgrade(): void { $user = User::factory()->create(); $starter = $this->makeProduct([ 'name' => 'Starter Plan', 'slug' => 'starter-upsell-web', 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'price_monthly' => 1, 'disk_gb' => 5, 'sort_order' => 1, ]); $basic = $this->makeProduct([ 'name' => 'Basic Plan', 'slug' => 'basic-upsell-web', 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'price_monthly' => 15, 'disk_gb' => 25, 'max_email_accounts' => 10, 'sort_order' => 2, ]); $account = $this->makeAccount($user, $starter, [ 'primary_domain' => 'upgrade-web.test', 'disk_used_bytes' => (int) round(5 * 0.82 * 1073741824), 'expires_at' => now()->addMonth(), ]); CustomerHostingOrder::create([ 'user_id' => $user->id, 'hosting_product_id' => $starter->id, 'hosting_account_id' => $account->id, 'domain_name' => $account->primary_domain, 'billing_cycle' => CustomerHostingOrder::CYCLE_MONTHLY, 'amount_paid' => 1, 'currency' => 'GHS', 'status' => CustomerHostingOrder::STATUS_ACTIVE, 'expires_at' => now()->addMonth(), ]); $provider = \Mockery::mock(SharedNodeProvider::class); $provider->shouldReceive('syncAccountPackage')->once()->andReturn(true); $this->app->instance(SharedNodeProvider::class, $provider); $this->actingAs($user) ->get(route('hosting.accounts.show', $account)) ->assertOk() ->assertSeeText('Recommended Next Steps') ->assertSeeText('Upgrade to Basic Plan'); $this->actingAs($user) ->post(route('hosting.accounts.plan-change', $account), [ 'target_product_id' => $basic->id, ]) ->assertRedirect(route('hosting.accounts.show', $account)); $account->refresh(); $this->assertSame($basic->id, $account->hosting_product_id); $this->assertSame(25, $account->allocated_disk_gb); } public function test_dashboard_renders_hosting_recommendations(): void { $user = User::factory()->create(); $starter = $this->makeProduct([ 'name' => 'Starter Plan', 'slug' => 'starter-upsell-dashboard', 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'disk_gb' => 5, 'sort_order' => 1, ]); $this->makeProduct([ 'name' => 'Basic Plan', 'slug' => 'basic-upsell-dashboard', 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'disk_gb' => 25, 'sort_order' => 2, ]); $this->makeAccount($user, $starter, [ 'primary_domain' => 'dashboard-upsell.test', 'disk_used_bytes' => (int) round(5 * 0.75 * 1073741824), ]); $this->actingAs($user) ->get(route('dashboard')) ->assertOk() ->assertSeeText('Recommended Next Steps') ->assertSeeText('Storage usage is high on dashboard-upsell.test'); } private function makeProduct(array $overrides = []): HostingProduct { return HostingProduct::create(array_merge([ 'name' => 'Shared Upsell', 'slug' => 'shared-upsell-' . str()->random(8), 'category' => HostingProduct::CATEGORY_SHARED, 'type' => HostingProduct::TYPE_SINGLE_DOMAIN, 'price_monthly' => 10, 'price_quarterly' => 30, 'price_yearly' => 120, 'price_biennial' => 240, 'currency' => 'GHS', 'disk_gb' => 10, 'max_domains' => 1, 'max_databases' => 2, 'max_email_accounts' => 5, 'bandwidth_gb' => 100, 'is_active' => true, 'is_visible' => true, ], $overrides)); } private function makeAccount(User $user, HostingProduct $product, array $overrides = []): HostingAccount { return HostingAccount::unguarded(function () use ($user, $product, $overrides) { return HostingAccount::create(array_merge([ 'user_id' => $user->id, 'hosting_product_id' => $product->id, 'username' => 'acct' . random_int(1000, 9999), 'primary_domain' => 'upsell.example.test', 'type' => HostingAccount::TYPE_SHARED, 'status' => HostingAccount::STATUS_ACTIVE, 'allocated_disk_gb' => (int) ($product->disk_gb ?? 0), 'disk_used_bytes' => 0, 'bandwidth_used_bytes' => 0, 'php_version' => '8.4', 'cpu_limit_percent' => 50, 'memory_limit_mb' => 512, 'process_limit' => 10, 'io_limit_mb' => 5, 'inode_limit' => 50000, 'resource_status' => HostingAccount::RESOURCE_STATUS_ACTIVE, 'resource_limits' => [ 'disk_gb' => (int) ($product->disk_gb ?? 0), 'bandwidth_gb' => $product->bandwidth_gb, 'max_domains' => $product->max_domains, 'max_databases' => $product->max_databases, 'max_email_accounts' => $product->max_email_accounts, ], ], $overrides)); }); } private function makeDomain(User $user, HostingAccount $account, string $host): Domain { return Domain::create([ 'user_id' => $user->id, 'website_id' => null, 'hosting_account_id' => $account->id, 'host' => $host, 'type' => 'custom', 'source' => 'hosting', 'status' => 'verified', 'onboarding_mode' => Domain::MODE_NS_AUTO, 'onboarding_state' => Domain::STATE_ACTIVE, 'dns_mode' => 'managed', 'verified_at' => now(), 'active_at' => now(), ]); } }