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