Files
ladill-servers/tests/Feature/AdminHostingAccountDurationTest.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

274 lines
9.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\HostingAccount;
use App\Models\HostingBillingInvoice;
use App\Models\HostingProduct;
use App\Models\User;
use App\Services\Billing\PaystackService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminHostingAccountDurationTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Carbon::setTestNow();
parent::tearDown();
}
public function test_admin_can_assign_hosting_package_with_duration(): void
{
Carbon::setTestNow('2026-04-05 10:00:00');
$admin = User::factory()->create(['is_admin' => true]);
$user = User::factory()->create();
$product = $this->createHostingProduct();
$response = $this->actingAs($admin)->post(route('admin.users.assign-hosting-package', $user), [
'hosting_product_id' => $product->id,
'duration_months' => 6,
'primary_domain' => 'example.com',
'username' => 'exampleuser',
]);
$response->assertRedirect();
$account = HostingAccount::query()->where('user_id', $user->id)->firstOrFail();
$this->assertSame('exampleuser', $account->username);
$this->assertSame('example.com', $account->primary_domain);
$this->assertSame($product->id, $account->hosting_product_id);
$this->assertSame('active', $account->status);
$this->assertNotNull($account->expires_at);
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2026-10-05 10:00:00')));
$this->assertSame(6, data_get($account->metadata, 'assigned_duration_months'));
}
public function test_admin_can_assign_same_hosting_product_multiple_times_to_one_user(): void
{
Carbon::setTestNow('2026-04-05 10:00:00');
$admin = User::factory()->create(['is_admin' => true]);
$user = User::factory()->create();
$product = $this->createHostingProduct();
$first = $this->actingAs($admin)->post(route('admin.users.assign-hosting-package', $user), [
'hosting_product_id' => $product->id,
'duration_months' => 6,
'primary_domain' => 'first-example.com',
'username' => 'firstacct',
]);
$second = $this->actingAs($admin)->post(route('admin.users.assign-hosting-package', $user), [
'hosting_product_id' => $product->id,
'duration_months' => 12,
'primary_domain' => 'second-example.com',
'username' => 'secondacct',
]);
$first->assertRedirect();
$second->assertRedirect();
$accounts = HostingAccount::query()
->where('user_id', $user->id)
->where('hosting_product_id', $product->id)
->orderBy('username')
->get();
$this->assertCount(2, $accounts);
$this->assertSame(['firstacct', 'secondacct'], $accounts->pluck('username')->all());
$this->assertSame(['first-example.com', 'second-example.com'], $accounts->pluck('primary_domain')->all());
}
public function test_admin_can_update_duration_for_existing_hosting_account(): void
{
Carbon::setTestNow('2026-04-05 10:00:00');
$admin = User::factory()->create(['is_admin' => true]);
$user = User::factory()->create();
$product = $this->createHostingProduct();
$account = HostingAccount::query()->create([
'user_id' => $user->id,
'hosting_product_id' => $product->id,
'username' => 'existinguser',
'type' => 'shared',
'status' => 'active',
'expires_at' => Carbon::parse('2026-05-05 10:00:00'),
'metadata' => [
'assigned_duration_months' => 1,
],
]);
$response = $this->actingAs($admin)->patch(route('admin.users.hosting-accounts.update-duration', [$user, $account]), [
'duration_months' => 12,
]);
$response->assertRedirect();
$account->refresh();
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2027-04-05 10:00:00')));
$this->assertSame(12, data_get($account->metadata, 'assigned_duration_months'));
$this->assertSame($admin->id, data_get($account->metadata, 'duration_updated_by_admin'));
$this->assertNotNull(data_get($account->metadata, 'duration_updated_at'));
}
public function test_admin_can_renew_existing_hosting_account_using_assigned_duration(): void
{
Carbon::setTestNow('2026-04-05 10:00:00');
$admin = User::factory()->create(['is_admin' => true]);
$user = User::factory()->create();
$product = $this->createHostingProduct();
$account = HostingAccount::query()->create([
'user_id' => $user->id,
'hosting_product_id' => $product->id,
'username' => 'renewadmin',
'type' => 'shared',
'status' => 'active',
'expires_at' => Carbon::parse('2026-10-05 10:00:00'),
'metadata' => [
'assigned_duration_months' => 6,
],
]);
$response = $this->actingAs($admin)->post(route('admin.users.hosting-accounts.renew', [$user, $account]));
$response->assertRedirect();
$account->refresh();
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2027-04-05 10:00:00')));
$this->assertSame($admin->id, data_get($account->metadata, 'renewed_by_admin'));
$this->assertNotNull(data_get($account->metadata, 'renewed_at'));
}
public function test_customer_renewal_starts_payment_checkout_instead_of_renewing_immediately(): void
{
Carbon::setTestNow('2026-04-05 10:00:00');
$user = User::factory()->create();
$product = $this->createHostingProduct();
$account = HostingAccount::query()->create([
'user_id' => $user->id,
'hosting_product_id' => $product->id,
'username' => 'renewcustomer',
'type' => 'shared',
'status' => 'active',
'expires_at' => Carbon::parse('2026-05-05 10:00:00'),
'metadata' => [
'assigned_duration_months' => 3,
],
]);
$paystack = \Mockery::mock(PaystackService::class);
$paystack->shouldReceive('initializeTransaction')
->once()
->andReturn([
'authorization_url' => 'https://paystack.test/renew/authorize',
]);
$this->app->instance(PaystackService::class, $paystack);
$response = $this->actingAs($user)->post(route('hosting.accounts.renew', $account));
$response->assertRedirect('https://paystack.test/renew/authorize');
$account->refresh();
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2026-05-05 10:00:00')));
$this->assertNull(data_get($account->metadata, 'renewed_by_user'));
$this->assertDatabaseHas('hosting_billing_invoices', [
'hosting_account_id' => $account->id,
'invoice_type' => HostingBillingInvoice::TYPE_RENEWAL,
'status' => HostingBillingInvoice::STATUS_PENDING,
'total_amount' => 30,
]);
}
public function test_customer_renewal_callback_applies_extension_after_successful_payment(): void
{
Carbon::setTestNow('2026-04-05 10:00:00');
$user = User::factory()->create();
$product = $this->createHostingProduct();
$account = HostingAccount::query()->create([
'user_id' => $user->id,
'hosting_product_id' => $product->id,
'username' => 'renewcallback',
'type' => 'shared',
'status' => 'active',
'expires_at' => Carbon::parse('2026-05-05 10:00:00'),
'metadata' => [
'assigned_duration_months' => 3,
],
]);
$invoice = HostingBillingInvoice::query()->create([
'user_id' => $user->id,
'hosting_account_id' => $account->id,
'invoice_type' => HostingBillingInvoice::TYPE_RENEWAL,
'status' => HostingBillingInvoice::STATUS_PENDING,
'currency' => 'GHS',
'subtotal_amount' => 30,
'credit_amount' => 0,
'total_amount' => 30,
'provider_reference' => 'HOST-REN-CALLBACK',
'metadata' => [
'months' => 3,
'billing_cycle' => 'quarterly',
],
]);
$paystack = \Mockery::mock(PaystackService::class);
$paystack->shouldReceive('verifyTransaction')
->once()
->with('HOST-REN-CALLBACK')
->andReturn([
'reference' => 'HOST-REN-CALLBACK',
'status' => 'success',
]);
$this->app->instance(PaystackService::class, $paystack);
$response = $this->actingAs($user)->get(route('hosting.accounts.renew.callback', $account) . '?reference=HOST-REN-CALLBACK');
$response->assertRedirect(route('hosting.accounts.show', $account));
$account->refresh();
$invoice->refresh();
$this->assertTrue($account->expires_at->equalTo(Carbon::parse('2026-08-05 10:00:00')));
$this->assertSame($user->id, data_get($account->metadata, 'renewed_by_user'));
$this->assertNotNull($invoice->paid_at);
$this->assertSame(HostingBillingInvoice::STATUS_PAID, $invoice->status);
}
private function createHostingProduct(): HostingProduct
{
return HostingProduct::query()->create([
'name' => 'Admin Assigned Shared Hosting',
'slug' => 'admin-assigned-shared-hosting',
'category' => 'shared',
'type' => 'single_domain',
'price_monthly' => 10,
'price_quarterly' => 30,
'price_yearly' => 120,
'price_biennial' => 240,
'currency' => 'GHS',
'max_domains' => 1,
'is_active' => true,
'is_visible' => true,
'sort_order' => 1,
]);
}
}