Files
ladill-servers/tests/Feature/HostingUpsellRecommendationTest.php
T
isaaccladandCursor b6c8ac343f Extract Ladill Servers as standalone app at servers.ladill.com.
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>
2026-06-06 19:18:30 +00:00

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(),
]);
}
}