Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
274 lines
9.9 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|