VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
284 lines
10 KiB
PHP
284 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\CustomerHostingOrder;
|
|
use App\Models\Domain;
|
|
use App\Models\HostingAccount;
|
|
use App\Models\HostingProduct;
|
|
use App\Models\Mailbox;
|
|
use App\Models\User;
|
|
use App\Services\Hosting\Providers\SharedNodeProvider;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Tests\TestCase;
|
|
|
|
class HostingUpsellRecommendationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_api_returns_plan_upgrade_recommendation_when_storage_usage_is_high(): void
|
|
{
|
|
$user = User::factory()->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(),
|
|
]);
|
|
}
|
|
}
|