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>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Local mirror of the platform identity, keyed by public_id (OIDC sub).
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('public_id')->unique();
|
||||
$table->string('name')->nullable();
|
||||
$table->string('email')->unique();
|
||||
$table->string('avatar_url')->nullable();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Registry of domains linked to hosting accounts (no website FK — hosting app
|
||||
* does not own the site builder). Must run before hosting_orders.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('domains', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->unsignedBigInteger('website_id')->nullable();
|
||||
$table->unsignedBigInteger('hosting_account_id')->nullable();
|
||||
$table->unsignedBigInteger('email_domain_id')->nullable();
|
||||
$table->string('host')->unique();
|
||||
$table->string('type')->default('custom');
|
||||
$table->string('source')->default('hosting');
|
||||
$table->string('onboarding_mode')->default('ns_auto');
|
||||
$table->string('verification_token')->nullable();
|
||||
$table->timestamp('verified_at')->nullable();
|
||||
$table->string('status')->default('pending');
|
||||
$table->string('ssl_status')->default('pending');
|
||||
$table->string('onboarding_state')->default('pending_ns');
|
||||
$table->string('dns_mode')->nullable();
|
||||
$table->json('ns_expected')->nullable();
|
||||
$table->json('ns_observed')->nullable();
|
||||
$table->timestamp('ns_checked_at')->nullable();
|
||||
$table->timestamp('connected_at')->nullable();
|
||||
$table->timestamp('mail_ready_at')->nullable();
|
||||
$table->timestamp('active_at')->nullable();
|
||||
$table->timestamp('manual_dns_verified_at')->nullable();
|
||||
$table->json('verification_meta')->nullable();
|
||||
$table->timestamp('ssl_provisioned_at')->nullable();
|
||||
$table->timestamp('ssl_expires_at')->nullable();
|
||||
$table->string('registrar')->nullable();
|
||||
$table->string('registrar_order_id')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'host']);
|
||||
$table->index(['source']);
|
||||
$table->index(['hosting_account_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('domains');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('hosting_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('domain_name');
|
||||
$table->string('rc_order_id')->nullable()->index();
|
||||
$table->string('rc_customer_id')->nullable();
|
||||
$table->string('plan_name')->nullable();
|
||||
$table->string('hosting_type')->default('single_domain');
|
||||
$table->string('status')->default('pending');
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('cpanel_url')->nullable();
|
||||
$table->string('cpanel_username')->nullable();
|
||||
$table->unsignedInteger('bandwidth_mb')->nullable();
|
||||
$table->unsignedInteger('disk_space_mb')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('provisioned_at')->nullable();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('hosting_orders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rc_service_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('category');
|
||||
$table->string('domain_name');
|
||||
$table->string('rc_order_id')->nullable()->index();
|
||||
$table->string('rc_customer_id')->nullable();
|
||||
$table->string('plan_id')->nullable();
|
||||
$table->string('plan_name')->nullable();
|
||||
$table->string('status')->default('pending');
|
||||
$table->string('server_location')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('access_url')->nullable();
|
||||
$table->string('access_username')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('provisioned_at')->nullable();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rc_service_orders');
|
||||
}
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('rc_service_orders', function (Blueprint $table) {
|
||||
$table->string('order_type')->default('service')->after('category')->index();
|
||||
$table->string('payment_status')->default('unpaid')->after('status')->index();
|
||||
$table->string('fulfillment_status')->default('cart')->after('payment_status')->index();
|
||||
$table->string('payment_reference')->nullable()->after('fulfillment_status')->index();
|
||||
$table->unsignedInteger('amount_minor')->nullable()->after('payment_reference');
|
||||
$table->string('currency', 3)->default('GHS')->after('amount_minor');
|
||||
$table->unsignedSmallInteger('term_months')->nullable()->after('currency');
|
||||
$table->unsignedSmallInteger('term_years')->nullable()->after('term_months');
|
||||
$table->timestamp('paid_at')->nullable()->after('term_years');
|
||||
$table->timestamp('submitted_at')->nullable()->after('paid_at');
|
||||
$table->timestamp('last_synced_at')->nullable()->after('submitted_at');
|
||||
});
|
||||
|
||||
DB::table('rc_service_orders')
|
||||
->orderBy('id')
|
||||
->chunkById(200, function ($rows): void {
|
||||
foreach ($rows as $row) {
|
||||
$status = strtolower((string) ($row->status ?? 'pending'));
|
||||
|
||||
DB::table('rc_service_orders')
|
||||
->where('id', (int) $row->id)
|
||||
->update([
|
||||
'order_type' => in_array((string) ($row->category ?? ''), ['domain-registration', 'domain-transfer'], true)
|
||||
? ((string) $row->category === 'domain-transfer' ? 'domain_transfer' : 'domain_registration')
|
||||
: 'service',
|
||||
'payment_status' => 'paid',
|
||||
'fulfillment_status' => match ($status) {
|
||||
'active', 'suspended', 'cancelled', 'expired' => 'fulfilled',
|
||||
'failed' => 'failed',
|
||||
default => 'awaiting_vendor',
|
||||
},
|
||||
'currency' => 'GHS',
|
||||
'submitted_at' => $row->created_at,
|
||||
'last_synced_at' => $row->updated_at,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('rc_service_orders', function (Blueprint $table) {
|
||||
$table->dropIndex(['order_type']);
|
||||
$table->dropIndex(['payment_status']);
|
||||
$table->dropIndex(['fulfillment_status']);
|
||||
$table->dropIndex(['payment_reference']);
|
||||
$table->dropColumn([
|
||||
'order_type',
|
||||
'payment_status',
|
||||
'fulfillment_status',
|
||||
'payment_reference',
|
||||
'amount_minor',
|
||||
'currency',
|
||||
'term_months',
|
||||
'term_years',
|
||||
'paid_at',
|
||||
'submitted_at',
|
||||
'last_synced_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Hosting Nodes - Physical/virtual servers that host accounts
|
||||
Schema::create('hosting_nodes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('hostname');
|
||||
$table->string('ip_address');
|
||||
$table->string('ipv6_address')->nullable();
|
||||
$table->enum('type', ['shared', 'vps', 'dedicated'])->default('shared');
|
||||
$table->enum('provider', ['contabo', 'manual', 'legacy_rc', 'local'])->default('contabo');
|
||||
$table->string('provider_instance_id')->nullable();
|
||||
$table->string('region')->nullable();
|
||||
$table->string('datacenter')->nullable();
|
||||
$table->unsignedInteger('cpu_cores')->nullable();
|
||||
$table->unsignedInteger('ram_mb')->nullable();
|
||||
$table->unsignedInteger('disk_gb')->nullable();
|
||||
$table->unsignedInteger('bandwidth_tb')->nullable();
|
||||
$table->unsignedInteger('max_accounts')->nullable();
|
||||
$table->unsignedInteger('current_accounts')->default(0);
|
||||
$table->enum('status', ['provisioning', 'active', 'maintenance', 'full', 'decommissioned'])->default('provisioning');
|
||||
$table->json('features')->nullable();
|
||||
$table->json('installed_software')->nullable();
|
||||
$table->string('ssh_port')->default('22');
|
||||
$table->text('ssh_private_key')->nullable();
|
||||
$table->string('control_panel')->nullable();
|
||||
$table->timestamp('last_health_check_at')->nullable();
|
||||
$table->json('health_status')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['type', 'status']);
|
||||
$table->index('provider_instance_id');
|
||||
});
|
||||
|
||||
// Hosting Plans - Product offerings
|
||||
Schema::create('hosting_plans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->enum('type', ['shared', 'vps', 'dedicated', 'email'])->default('shared');
|
||||
$table->decimal('price_monthly', 10, 2);
|
||||
$table->decimal('price_yearly', 10, 2)->nullable();
|
||||
$table->string('currency', 3)->default('USD');
|
||||
$table->unsignedInteger('disk_gb');
|
||||
$table->unsignedInteger('bandwidth_gb')->nullable();
|
||||
$table->unsignedInteger('cpu_cores')->nullable();
|
||||
$table->unsignedInteger('ram_mb')->nullable();
|
||||
$table->unsignedInteger('max_domains')->nullable();
|
||||
$table->unsignedInteger('max_databases')->nullable();
|
||||
$table->unsignedInteger('max_email_accounts')->nullable();
|
||||
$table->unsignedInteger('max_ftp_accounts')->nullable();
|
||||
$table->boolean('ssl_included')->default(true);
|
||||
$table->boolean('backups_included')->default(true);
|
||||
$table->unsignedInteger('backup_retention_days')->default(7);
|
||||
$table->json('features')->nullable();
|
||||
$table->json('php_versions')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->string('contabo_product_id')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['type', 'is_active']);
|
||||
});
|
||||
|
||||
// Hosting Accounts - Customer hosting instances
|
||||
Schema::create('hosting_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('hosting_plan_id')->constrained()->restrictOnDelete();
|
||||
$table->foreignId('hosting_node_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('username', 32)->unique();
|
||||
$table->string('primary_domain')->nullable();
|
||||
$table->enum('type', ['shared', 'vps', 'dedicated'])->default('shared');
|
||||
$table->enum('status', ['pending', 'provisioning', 'active', 'suspended', 'terminated', 'failed'])->default('pending');
|
||||
$table->string('provider_account_id')->nullable();
|
||||
$table->string('home_directory')->nullable();
|
||||
$table->unsignedBigInteger('disk_used_bytes')->default(0);
|
||||
$table->unsignedBigInteger('bandwidth_used_bytes')->default(0);
|
||||
$table->timestamp('bandwidth_reset_at')->nullable();
|
||||
$table->string('php_version')->default('8.2');
|
||||
$table->json('features_enabled')->nullable();
|
||||
$table->json('resource_limits')->nullable();
|
||||
$table->text('suspension_reason')->nullable();
|
||||
$table->timestamp('suspended_at')->nullable();
|
||||
$table->timestamp('provisioned_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['user_id', 'status']);
|
||||
$table->index('hosting_node_id');
|
||||
$table->index('primary_domain');
|
||||
});
|
||||
|
||||
// Hosted Sites - Individual websites on hosting accounts
|
||||
Schema::create('hosted_sites', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('domain');
|
||||
$table->string('document_root');
|
||||
$table->enum('type', ['primary', 'addon', 'subdomain', 'parked'])->default('addon');
|
||||
$table->enum('status', ['pending', 'active', 'suspended', 'disabled'])->default('pending');
|
||||
$table->string('php_version')->nullable();
|
||||
$table->boolean('ssl_enabled')->default(false);
|
||||
$table->timestamp('ssl_expires_at')->nullable();
|
||||
$table->string('installed_app')->nullable();
|
||||
$table->string('installed_app_version')->nullable();
|
||||
$table->json('app_config')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['hosting_account_id', 'domain']);
|
||||
$table->index('domain');
|
||||
});
|
||||
|
||||
// Hosted Databases
|
||||
Schema::create('hosted_databases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('hosted_site_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('username');
|
||||
$table->string('password_encrypted')->nullable();
|
||||
$table->enum('type', ['mysql', 'mariadb', 'postgresql'])->default('mysql');
|
||||
$table->unsignedBigInteger('size_bytes')->default(0);
|
||||
$table->enum('status', ['active', 'suspended', 'deleted'])->default('active');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['hosting_account_id', 'name']);
|
||||
});
|
||||
|
||||
// Provisioning Jobs - Track async provisioning operations
|
||||
Schema::create('provisioning_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->string('type');
|
||||
$table->morphs('provisionable');
|
||||
$table->enum('provider', ['contabo', 'shared_node', 'mail', 'dns', 'ssl']);
|
||||
$table->enum('status', ['pending', 'running', 'completed', 'failed', 'cancelled'])->default('pending');
|
||||
$table->json('payload')->nullable();
|
||||
$table->json('result')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->unsignedInteger('attempts')->default(0);
|
||||
$table->unsignedInteger('max_attempts')->default(3);
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamp('next_retry_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['status', 'next_retry_at']);
|
||||
});
|
||||
|
||||
// VPS Instances - For managed VPS products
|
||||
Schema::create('vps_instances', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('hosting_plan_id')->constrained()->restrictOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('hostname');
|
||||
$table->string('provider')->default('contabo');
|
||||
$table->string('provider_instance_id')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('ipv6_address')->nullable();
|
||||
$table->string('region')->nullable();
|
||||
$table->string('datacenter')->nullable();
|
||||
$table->string('image')->nullable();
|
||||
$table->unsignedInteger('cpu_cores')->nullable();
|
||||
$table->unsignedInteger('ram_mb')->nullable();
|
||||
$table->unsignedInteger('disk_gb')->nullable();
|
||||
$table->enum('status', ['pending', 'provisioning', 'running', 'stopped', 'suspended', 'terminated', 'failed'])->default('pending');
|
||||
$table->enum('power_status', ['on', 'off', 'unknown'])->default('unknown');
|
||||
$table->string('root_password_encrypted')->nullable();
|
||||
$table->text('ssh_public_key')->nullable();
|
||||
$table->json('tags')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamp('provisioned_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['user_id', 'status']);
|
||||
$table->index('provider_instance_id');
|
||||
});
|
||||
|
||||
// VPS Snapshots
|
||||
Schema::create('vps_snapshots', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('vps_instance_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('provider_snapshot_id')->nullable();
|
||||
$table->unsignedBigInteger('size_bytes')->nullable();
|
||||
$table->enum('status', ['creating', 'available', 'deleting', 'failed'])->default('creating');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('provider_snapshot_id');
|
||||
});
|
||||
|
||||
// App Installations - Track installed applications
|
||||
Schema::create('app_installations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('hosted_site_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('app_type');
|
||||
$table->string('app_version');
|
||||
$table->string('admin_path')->nullable();
|
||||
$table->string('admin_username')->nullable();
|
||||
$table->string('admin_email')->nullable();
|
||||
$table->json('config')->nullable();
|
||||
$table->enum('status', ['installing', 'active', 'updating', 'failed', 'removed'])->default('installing');
|
||||
$table->timestamp('installed_at')->nullable();
|
||||
$table->timestamp('last_updated_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['app_type', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('app_installations');
|
||||
Schema::dropIfExists('vps_snapshots');
|
||||
Schema::dropIfExists('vps_instances');
|
||||
Schema::dropIfExists('provisioning_jobs');
|
||||
Schema::dropIfExists('hosted_databases');
|
||||
Schema::dropIfExists('hosted_sites');
|
||||
Schema::dropIfExists('hosting_accounts');
|
||||
Schema::dropIfExists('hosting_plans');
|
||||
Schema::dropIfExists('hosting_nodes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Hosting Products - Admin-managed pricing for all hosting types
|
||||
Schema::create('hosting_products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->enum('category', ['shared', 'vps', 'dedicated', 'email']);
|
||||
$table->enum('type', ['single_domain', 'multi_domain', 'wordpress', 'vps', 'dedicated', 'email_standalone']);
|
||||
$table->decimal('price_monthly', 10, 2);
|
||||
$table->decimal('price_quarterly', 10, 2)->nullable();
|
||||
$table->decimal('price_yearly', 10, 2)->nullable();
|
||||
$table->decimal('price_biennial', 10, 2)->nullable();
|
||||
$table->decimal('setup_fee', 10, 2)->default(0);
|
||||
$table->string('currency', 3)->default('USD');
|
||||
|
||||
// Resource limits
|
||||
$table->integer('disk_gb')->nullable();
|
||||
$table->integer('bandwidth_gb')->nullable();
|
||||
$table->integer('cpu_cores')->nullable();
|
||||
$table->integer('ram_mb')->nullable();
|
||||
$table->integer('max_domains')->default(1);
|
||||
$table->integer('max_databases')->nullable();
|
||||
$table->integer('max_email_accounts')->nullable();
|
||||
$table->integer('max_ftp_accounts')->nullable();
|
||||
|
||||
// Features
|
||||
$table->boolean('ssl_included')->default(true);
|
||||
$table->boolean('backups_included')->default(false);
|
||||
$table->integer('backup_retention_days')->nullable();
|
||||
$table->json('features')->nullable();
|
||||
$table->json('php_versions')->nullable();
|
||||
|
||||
// Contabo mapping for VPS/Dedicated
|
||||
$table->string('contabo_product_id')->nullable();
|
||||
$table->string('contabo_region')->nullable();
|
||||
|
||||
// Display
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->boolean('is_visible')->default(true);
|
||||
$table->integer('sort_order')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['category', 'is_active']);
|
||||
$table->index(['type', 'is_active']);
|
||||
});
|
||||
|
||||
// Customer Hosting Orders - New unified order system
|
||||
Schema::create('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('hosting_product_id')->constrained()->onDelete('restrict');
|
||||
$table->foreignId('domain_id')->nullable()->constrained()->onDelete('set null');
|
||||
|
||||
$table->string('domain_name');
|
||||
$table->enum('billing_cycle', ['monthly', 'quarterly', 'yearly', 'biennial'])->default('yearly');
|
||||
$table->decimal('amount_paid', 10, 2);
|
||||
$table->string('currency', 3)->default('USD');
|
||||
$table->string('payment_reference')->nullable();
|
||||
|
||||
// Order status with approval workflow
|
||||
$table->enum('status', [
|
||||
'pending_payment',
|
||||
'pending_approval',
|
||||
'approved',
|
||||
'provisioning',
|
||||
'active',
|
||||
'suspended',
|
||||
'cancelled',
|
||||
'expired',
|
||||
'failed'
|
||||
])->default('pending_payment');
|
||||
|
||||
// Approval tracking
|
||||
$table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->text('approval_notes')->nullable();
|
||||
|
||||
// Provisioning details
|
||||
$table->foreignId('hosting_node_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->foreignId('hosting_account_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->foreignId('vps_instance_id')->nullable()->constrained()->onDelete('set null');
|
||||
|
||||
$table->string('server_username')->nullable();
|
||||
$table->string('server_ip')->nullable();
|
||||
$table->string('control_panel_url')->nullable();
|
||||
|
||||
$table->timestamp('provisioned_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('suspended_at')->nullable();
|
||||
$table->timestamp('cancelled_at')->nullable();
|
||||
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['user_id', 'status']);
|
||||
$table->index(['status', 'created_at']);
|
||||
$table->index('domain_name');
|
||||
});
|
||||
|
||||
// Node Capacity Alerts - Track when new shared hosting nodes are needed
|
||||
Schema::create('node_capacity_alerts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('hosting_node_id')->constrained()->onDelete('cascade');
|
||||
$table->enum('alert_type', ['capacity_warning', 'capacity_critical', 'resource_high', 'needs_expansion']);
|
||||
$table->integer('current_accounts');
|
||||
$table->integer('max_accounts');
|
||||
$table->decimal('capacity_percent', 5, 2);
|
||||
$table->json('resource_usage')->nullable();
|
||||
$table->text('message');
|
||||
$table->boolean('is_resolved')->default(false);
|
||||
$table->foreignId('resolved_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->timestamp('resolved_at')->nullable();
|
||||
$table->text('resolution_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['is_resolved', 'created_at']);
|
||||
$table->index(['hosting_node_id', 'is_resolved']);
|
||||
});
|
||||
|
||||
// Provisioning Queue - Track all provisioning jobs
|
||||
Schema::create('provisioning_queue', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_hosting_order_id')->constrained()->onDelete('cascade');
|
||||
$table->enum('status', ['queued', 'processing', 'completed', 'failed', 'cancelled'])->default('queued');
|
||||
$table->enum('provider', ['shared_node', 'contabo_vps', 'contabo_dedicated']);
|
||||
$table->integer('attempts')->default(0);
|
||||
$table->integer('max_attempts')->default(3);
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->json('provisioning_log')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['status', 'created_at']);
|
||||
});
|
||||
|
||||
// Add approval fields to existing hosting_orders table for legacy RC orders
|
||||
if (Schema::hasTable('hosting_orders')) {
|
||||
Schema::table('hosting_orders', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('hosting_orders', 'is_legacy_rc')) {
|
||||
$table->boolean('is_legacy_rc')->default(true)->after('meta');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('provisioning_queue');
|
||||
Schema::dropIfExists('node_capacity_alerts');
|
||||
Schema::dropIfExists('customer_hosting_orders');
|
||||
Schema::dropIfExists('hosting_products');
|
||||
|
||||
if (Schema::hasTable('hosting_orders') && Schema::hasColumn('hosting_orders', 'is_legacy_rc')) {
|
||||
Schema::table('hosting_orders', function (Blueprint $table) {
|
||||
$table->dropColumn('is_legacy_rc');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('hosting_accounts', function (Blueprint $table) {
|
||||
$table->foreignId('hosting_product_id')->nullable()->after('user_id')->constrained()->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('hosting_accounts', function (Blueprint $table) {
|
||||
$table->dropForeign(['hosting_product_id']);
|
||||
$table->dropColumn('hosting_product_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('hosting_accounts', function (Blueprint $table) {
|
||||
$table->dropForeign(['hosting_plan_id']);
|
||||
$table->dropColumn('hosting_plan_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('hosting_accounts', function (Blueprint $table) {
|
||||
$table->foreignId('hosting_plan_id')->nullable()->after('hosting_product_id')->constrained()->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_installations', function (Blueprint $table) {
|
||||
$table->text('admin_password_encrypted')->nullable()->after('admin_email');
|
||||
$table->string('database_name')->nullable()->after('admin_password_encrypted');
|
||||
$table->string('database_username')->nullable()->after('database_name');
|
||||
$table->text('database_password_encrypted')->nullable()->after('database_username');
|
||||
$table->string('install_url')->nullable()->after('database_password_encrypted');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('app_installations', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'admin_password_encrypted',
|
||||
'database_name',
|
||||
'database_username',
|
||||
'database_password_encrypted',
|
||||
'install_url',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('app_installations', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('progress')->default(0)->after('status');
|
||||
$table->string('progress_message')->nullable()->after('progress');
|
||||
$table->string('current_step')->nullable()->after('progress_message');
|
||||
$table->unsignedTinyInteger('total_steps')->default(5)->after('current_step');
|
||||
$table->text('error_message')->nullable()->after('total_steps');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('app_installations', function (Blueprint $table) {
|
||||
$table->dropColumn(['progress', 'progress_message', 'current_step', 'total_steps', 'error_message']);
|
||||
});
|
||||
}
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->string('billing_cycle', 20)->default('yearly')->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('customer_hosting_orders')
|
||||
->where('billing_cycle', 'semiannual')
|
||||
->update(['billing_cycle' => 'quarterly']);
|
||||
|
||||
Schema::table('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->enum('billing_cycle', ['monthly', 'quarterly', 'yearly', 'biennial'])->default('yearly')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('server_agents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_hosting_order_id')->constrained()->cascadeOnDelete();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->string('status', 40)->default('pending_registration');
|
||||
$table->string('hostname')->nullable();
|
||||
$table->string('agent_version', 120)->nullable();
|
||||
$table->string('platform', 120)->nullable();
|
||||
$table->json('capabilities')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->string('bootstrap_token_hash', 64)->nullable();
|
||||
$table->string('access_token_hash', 64)->nullable();
|
||||
$table->timestamp('access_token_issued_at')->nullable();
|
||||
$table->timestamp('registered_at')->nullable();
|
||||
$table->timestamp('last_heartbeat_at')->nullable();
|
||||
$table->string('last_seen_ip', 64)->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('customer_hosting_order_id');
|
||||
$table->index(['status', 'last_heartbeat_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('server_agents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('server_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('customer_hosting_order_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('server_agent_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('type', 80);
|
||||
$table->string('status', 40)->default('queued');
|
||||
$table->json('payload');
|
||||
$table->json('result')->nullable();
|
||||
$table->unsignedTinyInteger('progress')->default(0);
|
||||
$table->unsignedInteger('attempt_count')->default(0);
|
||||
$table->timestamp('queued_at')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamp('failed_at')->nullable();
|
||||
$table->timestamp('last_heartbeat_at')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['customer_hosting_order_id', 'status']);
|
||||
$table->index(['server_agent_id', 'status']);
|
||||
$table->index(['type', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('server_tasks');
|
||||
}
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->string('guest_email')->nullable()->after('user_id');
|
||||
$table->index('guest_email');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->dropIndex(['guest_email']);
|
||||
$table->dropColumn('guest_email');
|
||||
});
|
||||
}
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('customer_hosting_orders', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->upsertStarterPlan();
|
||||
$this->reorderExistingSharedPlans();
|
||||
$this->updateProPlan();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'starter-plan')
|
||||
->delete();
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'basic-plan')
|
||||
->update([
|
||||
'sort_order' => 1,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'plus-plan')
|
||||
->update([
|
||||
'sort_order' => 2,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'growth-plan')
|
||||
->update([
|
||||
'sort_order' => 3,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'pro-plan')
|
||||
->update([
|
||||
'description' => 'Unlimited websites with unmetered SSD storage and bandwidth, 25 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
|
||||
'price_monthly' => 75.00,
|
||||
'price_yearly' => 900.00,
|
||||
'disk_gb' => null,
|
||||
'sort_order' => 4,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function upsertStarterPlan(): void
|
||||
{
|
||||
$existing = DB::table('hosting_products')->where('slug', 'starter-plan')->first();
|
||||
|
||||
$payload = [
|
||||
'name' => 'Starter Plan',
|
||||
'description' => 'Entry-level single-domain hosting with 5 GB SSD storage, unmetered bandwidth, 1 free email account, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 2 FTP users, and 2 MySQL databases.',
|
||||
'category' => 'shared',
|
||||
'type' => 'single_domain',
|
||||
'price_monthly' => 1.00,
|
||||
'price_quarterly' => null,
|
||||
'price_yearly' => 12.00,
|
||||
'price_biennial' => null,
|
||||
'setup_fee' => 0.00,
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 5,
|
||||
'bandwidth_gb' => null,
|
||||
'cpu_cores' => null,
|
||||
'ram_mb' => null,
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 2,
|
||||
'max_email_accounts' => 1,
|
||||
'max_ftp_accounts' => 2,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 7,
|
||||
'features' => json_encode([
|
||||
'LiteSpeed servers',
|
||||
'AI Website Builder',
|
||||
'Softaculous installer',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Weekly backups',
|
||||
], JSON_THROW_ON_ERROR),
|
||||
'php_versions' => null,
|
||||
'contabo_product_id' => null,
|
||||
'contabo_region' => null,
|
||||
'is_active' => true,
|
||||
'is_featured' => false,
|
||||
'is_visible' => true,
|
||||
'sort_order' => 1,
|
||||
'deleted_at' => null,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'starter-plan')
|
||||
->update($payload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('hosting_products')->insert($payload + [
|
||||
'slug' => 'starter-plan',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function reorderExistingSharedPlans(): void
|
||||
{
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'basic-plan')
|
||||
->update([
|
||||
'sort_order' => 2,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'plus-plan')
|
||||
->update([
|
||||
'sort_order' => 3,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'growth-plan')
|
||||
->update([
|
||||
'sort_order' => 4,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateProPlan(): void
|
||||
{
|
||||
DB::table('hosting_products')
|
||||
->where('slug', 'pro-plan')
|
||||
->update([
|
||||
'description' => 'Unlimited websites with 150 GB SSD storage and unmetered bandwidth, 25 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
|
||||
'price_monthly' => 175.00,
|
||||
'price_yearly' => 2100.00,
|
||||
'disk_gb' => 150,
|
||||
'sort_order' => 5,
|
||||
'deleted_at' => null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
};
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private const BYTES_PER_GB = 1073741824;
|
||||
private const HOSTING_NODES_SEGMENT_STATUS_INDEX = 'hosting_nodes_segment_status_index';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$this->addColumnIfMissing('hosting_nodes', 'segment', function (Blueprint $table): void {
|
||||
$table->string('segment')->default('general')->after('type');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_nodes', 'oversell_ratio', function (Blueprint $table): void {
|
||||
$table->decimal('oversell_ratio', 5, 2)->default(3)->after('disk_gb');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_nodes', 'allocated_disk_gb', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('allocated_disk_gb')->default(0)->after('oversell_ratio');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_nodes', 'used_disk_gb', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('used_disk_gb')->default(0)->after('allocated_disk_gb');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_nodes', 'current_load_percent', function (Blueprint $table): void {
|
||||
$table->decimal('current_load_percent', 5, 2)->default(0)->after('current_accounts');
|
||||
});
|
||||
$this->addIndexIfMissing('hosting_nodes', ['segment', 'status'], self::HOSTING_NODES_SEGMENT_STATUS_INDEX);
|
||||
|
||||
$this->addColumnIfMissing('hosting_accounts', 'allocated_disk_gb', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('allocated_disk_gb')->default(0)->after('home_directory');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'inode_count', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('inode_count')->default(0)->after('disk_used_bytes');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'cpu_usage_percent', function (Blueprint $table): void {
|
||||
$table->decimal('cpu_usage_percent', 8, 2)->nullable()->after('inode_count');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'last_usage_sync_at', function (Blueprint $table): void {
|
||||
$table->timestamp('last_usage_sync_at')->nullable()->after('cpu_usage_percent');
|
||||
});
|
||||
|
||||
$this->backfillAccountAllocations();
|
||||
$this->backfillNodeCapacities();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->dropColumnsIfPresent('hosting_accounts', [
|
||||
'allocated_disk_gb',
|
||||
'inode_count',
|
||||
'cpu_usage_percent',
|
||||
'last_usage_sync_at',
|
||||
]);
|
||||
|
||||
$this->dropIndexIfPresent('hosting_nodes', self::HOSTING_NODES_SEGMENT_STATUS_INDEX);
|
||||
$this->dropColumnsIfPresent('hosting_nodes', [
|
||||
'segment',
|
||||
'oversell_ratio',
|
||||
'allocated_disk_gb',
|
||||
'used_disk_gb',
|
||||
'current_load_percent',
|
||||
]);
|
||||
}
|
||||
|
||||
private function backfillAccountAllocations(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_accounts') || ! Schema::hasColumn('hosting_accounts', 'allocated_disk_gb')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$productDiskById = Schema::hasTable('hosting_products')
|
||||
? DB::table('hosting_products')->pluck('disk_gb', 'id')
|
||||
: collect();
|
||||
$planDiskById = Schema::hasTable('hosting_plans')
|
||||
? DB::table('hosting_plans')->pluck('disk_gb', 'id')
|
||||
: collect();
|
||||
|
||||
$select = ['id', 'disk_used_bytes'];
|
||||
|
||||
if (Schema::hasColumn('hosting_accounts', 'hosting_product_id')) {
|
||||
$select[] = 'hosting_product_id';
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('hosting_accounts', 'hosting_plan_id')) {
|
||||
$select[] = 'hosting_plan_id';
|
||||
}
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->select($select)
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($accounts) use ($productDiskById, $planDiskById): void {
|
||||
$hasLastUsageSyncAt = Schema::hasColumn('hosting_accounts', 'last_usage_sync_at');
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$allocated = (int) ($productDiskById[$account->hosting_product_id] ?? 0);
|
||||
|
||||
if ($allocated <= 0) {
|
||||
$allocated = (int) ($planDiskById[$account->hosting_plan_id] ?? 0);
|
||||
}
|
||||
|
||||
$updates = [
|
||||
'allocated_disk_gb' => $allocated,
|
||||
];
|
||||
|
||||
if ($hasLastUsageSyncAt) {
|
||||
$updates['last_usage_sync_at'] = now();
|
||||
}
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->where('id', $account->id)
|
||||
->update($updates);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function backfillNodeCapacities(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_nodes') || ! Schema::hasTable('hosting_accounts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('hosting_nodes')
|
||||
->select(['id', 'disk_gb', 'max_accounts', 'status'])
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($nodes): void {
|
||||
foreach ($nodes as $node) {
|
||||
$aggregate = DB::table('hosting_accounts')
|
||||
->where('hosting_node_id', $node->id)
|
||||
->selectRaw('COUNT(*) as account_count')
|
||||
->selectRaw('COALESCE(SUM(allocated_disk_gb), 0) as allocated_disk_gb')
|
||||
->selectRaw('COALESCE(SUM(disk_used_bytes), 0) as used_disk_bytes')
|
||||
->first();
|
||||
|
||||
$currentAccounts = (int) ($aggregate->account_count ?? 0);
|
||||
$allocatedDiskGb = (int) ($aggregate->allocated_disk_gb ?? 0);
|
||||
$usedDiskGb = (int) ceil(((int) ($aggregate->used_disk_bytes ?? 0)) / self::BYTES_PER_GB);
|
||||
$logicalCapacity = $node->disk_gb ? ((float) $node->disk_gb * 3) : 0;
|
||||
$accountPercent = $node->max_accounts ? ($currentAccounts / $node->max_accounts) * 100 : 0;
|
||||
$logicalPercent = $logicalCapacity > 0 ? ($allocatedDiskGb / $logicalCapacity) * 100 : 0;
|
||||
$physicalPercent = $node->disk_gb ? ($usedDiskGb / $node->disk_gb) * 100 : 0;
|
||||
$loadPercent = round(max($accountPercent, $logicalPercent, $physicalPercent), 2);
|
||||
|
||||
DB::table('hosting_nodes')
|
||||
->where('id', $node->id)
|
||||
->update([
|
||||
'allocated_disk_gb' => $allocatedDiskGb,
|
||||
'used_disk_gb' => $usedDiskGb,
|
||||
'current_accounts' => $currentAccounts,
|
||||
'current_load_percent' => $loadPercent,
|
||||
'status' => in_array($node->status, ['active', 'full'], true)
|
||||
? (($logicalCapacity > 0 && $allocatedDiskGb >= $logicalCapacity) || ($node->max_accounts && $currentAccounts >= $node->max_accounts) ? 'full' : 'active')
|
||||
: $node->status,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function addColumnIfMissing(string $table, string $column, callable $callback): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || Schema::hasColumn($table, $column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, $callback);
|
||||
}
|
||||
|
||||
private function addIndexIfMissing(string $table, array $columns, string $indexName): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || $this->hasIndex($table, $indexName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $table) use ($columns, $indexName): void {
|
||||
$table->index($columns, $indexName);
|
||||
});
|
||||
}
|
||||
|
||||
private function dropColumnsIfPresent(string $table, array $columns): void
|
||||
{
|
||||
if (! Schema::hasTable($table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$presentColumns = array_values(array_filter($columns, fn (string $column): bool => Schema::hasColumn($table, $column)));
|
||||
|
||||
if ($presentColumns === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $table) use ($presentColumns): void {
|
||||
$table->dropColumn($presentColumns);
|
||||
});
|
||||
}
|
||||
|
||||
private function dropIndexIfPresent(string $table, string $indexName): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! $this->hasIndex($table, $indexName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $table) use ($indexName): void {
|
||||
$table->dropIndex($indexName);
|
||||
});
|
||||
}
|
||||
|
||||
private function hasIndex(string $table, string $indexName): bool
|
||||
{
|
||||
if (! Schema::hasTable($table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
$rows = DB::select(sprintf("PRAGMA index_list('%s')", str_replace("'", "''", $table)));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (($row->name ?? null) === $indexName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
return DB::table('information_schema.statistics')
|
||||
->where('table_schema', DB::getDatabaseName())
|
||||
->where('table_name', $table)
|
||||
->where('index_name', $indexName)
|
||||
->exists();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private const HOSTING_ACCOUNTS_RESOURCE_STATUS_INDEX = 'hosting_accounts_resource_status_status_index';
|
||||
private const HOSTING_ACCOUNTS_FLAGGED_WARNING_INDEX = 'hosting_accounts_is_flagged_warning_count_index';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$this->addColumnIfMissing('hosting_accounts', 'memory_used_mb', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('memory_used_mb')->default(0)->after('cpu_usage_percent');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'process_count', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('process_count')->default(0)->after('memory_used_mb');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'io_usage_mb', function (Blueprint $table): void {
|
||||
$table->decimal('io_usage_mb', 8, 2)->nullable()->after('process_count');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'cpu_limit_percent', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('cpu_limit_percent')->nullable()->after('php_version');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'memory_limit_mb', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('memory_limit_mb')->nullable()->after('cpu_limit_percent');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'process_limit', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('process_limit')->nullable()->after('memory_limit_mb');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'io_limit_mb', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('io_limit_mb')->nullable()->after('process_limit');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'inode_limit', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('inode_limit')->nullable()->after('io_limit_mb');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'resource_status', function (Blueprint $table): void {
|
||||
$table->string('resource_status')->default('active')->after('inode_limit');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'uploads_restricted', function (Blueprint $table): void {
|
||||
$table->boolean('uploads_restricted')->default(false)->after('resource_status');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'is_flagged', function (Blueprint $table): void {
|
||||
$table->boolean('is_flagged')->default(false)->after('uploads_restricted');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'warning_count', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('warning_count')->default(0)->after('is_flagged');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'cpu_breach_streak', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('cpu_breach_streak')->default(0)->after('warning_count');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'memory_breach_streak', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('memory_breach_streak')->default(0)->after('cpu_breach_streak');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'process_breach_streak', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('process_breach_streak')->default(0)->after('memory_breach_streak');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'io_breach_streak', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('io_breach_streak')->default(0)->after('process_breach_streak');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'inode_breach_streak', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('inode_breach_streak')->default(0)->after('io_breach_streak');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'last_warning_at', function (Blueprint $table): void {
|
||||
$table->timestamp('last_warning_at')->nullable()->after('inode_breach_streak');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'last_resource_breach_at', function (Blueprint $table): void {
|
||||
$table->timestamp('last_resource_breach_at')->nullable()->after('last_warning_at');
|
||||
});
|
||||
$this->addColumnIfMissing('hosting_accounts', 'throttled_at', function (Blueprint $table): void {
|
||||
$table->timestamp('throttled_at')->nullable()->after('last_resource_breach_at');
|
||||
});
|
||||
$this->addIndexIfMissing('hosting_accounts', ['resource_status', 'status'], self::HOSTING_ACCOUNTS_RESOURCE_STATUS_INDEX);
|
||||
$this->addIndexIfMissing('hosting_accounts', ['is_flagged', 'warning_count'], self::HOSTING_ACCOUNTS_FLAGGED_WARNING_INDEX);
|
||||
|
||||
if (! Schema::hasTable('hosting_account_alerts')) {
|
||||
Schema::create('hosting_account_alerts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('alert_type');
|
||||
$table->string('severity')->default('warning');
|
||||
$table->text('message');
|
||||
$table->json('resource_usage')->nullable();
|
||||
$table->boolean('is_resolved')->default(false);
|
||||
$table->timestamp('resolved_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['hosting_account_id', 'is_resolved']);
|
||||
$table->index(['alert_type', 'is_resolved']);
|
||||
});
|
||||
}
|
||||
|
||||
$this->backfillResourcePolicyDefaults();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('hosting_account_alerts');
|
||||
|
||||
$this->dropIndexIfPresent('hosting_accounts', self::HOSTING_ACCOUNTS_RESOURCE_STATUS_INDEX);
|
||||
$this->dropIndexIfPresent('hosting_accounts', self::HOSTING_ACCOUNTS_FLAGGED_WARNING_INDEX);
|
||||
$this->dropColumnsIfPresent('hosting_accounts', [
|
||||
'memory_used_mb',
|
||||
'process_count',
|
||||
'io_usage_mb',
|
||||
'cpu_limit_percent',
|
||||
'memory_limit_mb',
|
||||
'process_limit',
|
||||
'io_limit_mb',
|
||||
'inode_limit',
|
||||
'resource_status',
|
||||
'uploads_restricted',
|
||||
'is_flagged',
|
||||
'warning_count',
|
||||
'cpu_breach_streak',
|
||||
'memory_breach_streak',
|
||||
'process_breach_streak',
|
||||
'io_breach_streak',
|
||||
'inode_breach_streak',
|
||||
'last_warning_at',
|
||||
'last_resource_breach_at',
|
||||
'throttled_at',
|
||||
]);
|
||||
}
|
||||
|
||||
private function backfillResourcePolicyDefaults(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_accounts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$products = Schema::hasTable('hosting_products')
|
||||
? DB::table('hosting_products')->get()->keyBy('id')
|
||||
: collect();
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->select(['id', 'hosting_product_id', 'type', 'status'])
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($accounts) use ($products): void {
|
||||
foreach ($accounts as $account) {
|
||||
$product = $products->get($account->hosting_product_id);
|
||||
$type = $product->type ?? null;
|
||||
|
||||
$defaults = match ($type) {
|
||||
'wordpress' => [75, 768, 15, 8, 100000],
|
||||
'multi_domain' => [100, 1024, 20, 10, 150000],
|
||||
default => [50, 512, 10, 5, 50000],
|
||||
};
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->where('id', $account->id)
|
||||
->update([
|
||||
'cpu_limit_percent' => $defaults[0],
|
||||
'memory_limit_mb' => $defaults[1],
|
||||
'process_limit' => $defaults[2],
|
||||
'io_limit_mb' => $defaults[3],
|
||||
'inode_limit' => $defaults[4],
|
||||
'resource_status' => $account->status === 'suspended' ? 'suspended' : 'active',
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function addColumnIfMissing(string $table, string $column, callable $callback): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || Schema::hasColumn($table, $column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, $callback);
|
||||
}
|
||||
|
||||
private function addIndexIfMissing(string $table, array $columns, string $indexName): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || $this->hasIndex($table, $indexName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $table) use ($columns, $indexName): void {
|
||||
$table->index($columns, $indexName);
|
||||
});
|
||||
}
|
||||
|
||||
private function dropColumnsIfPresent(string $table, array $columns): void
|
||||
{
|
||||
if (! Schema::hasTable($table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$presentColumns = array_values(array_filter($columns, fn (string $column): bool => Schema::hasColumn($table, $column)));
|
||||
|
||||
if ($presentColumns === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $table) use ($presentColumns): void {
|
||||
$table->dropColumn($presentColumns);
|
||||
});
|
||||
}
|
||||
|
||||
private function dropIndexIfPresent(string $table, string $indexName): void
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! $this->hasIndex($table, $indexName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table($table, function (Blueprint $table) use ($indexName): void {
|
||||
$table->dropIndex($indexName);
|
||||
});
|
||||
}
|
||||
|
||||
private function hasIndex(string $table, string $indexName): bool
|
||||
{
|
||||
if (! Schema::hasTable($table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
$rows = DB::select(sprintf("PRAGMA index_list('%s')", str_replace("'", "''", $table)));
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (($row->name ?? null) === $indexName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
return DB::table('information_schema.statistics')
|
||||
->where('table_schema', DB::getDatabaseName())
|
||||
->where('table_name', $table)
|
||||
->where('index_name', $indexName)
|
||||
->exists();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_plan_changes')) {
|
||||
Schema::create('hosting_plan_changes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('from_hosting_product_id')->constrained('hosting_products')->restrictOnDelete();
|
||||
$table->foreignId('to_hosting_product_id')->constrained('hosting_products')->restrictOnDelete();
|
||||
$table->string('direction');
|
||||
$table->string('billing_cycle')->default('monthly');
|
||||
$table->unsignedSmallInteger('cycle_months')->default(1);
|
||||
$table->unsignedSmallInteger('remaining_days')->default(0);
|
||||
$table->decimal('proration_ratio', 8, 4)->default(0);
|
||||
$table->decimal('old_cycle_price', 10, 2)->default(0);
|
||||
$table->decimal('new_cycle_price', 10, 2)->default(0);
|
||||
$table->decimal('charge_amount', 10, 2)->default(0);
|
||||
$table->decimal('credit_amount', 10, 2)->default(0);
|
||||
$table->decimal('net_amount', 10, 2)->default(0);
|
||||
$table->string('currency', 3)->default('GHS');
|
||||
$table->string('status')->default('applied');
|
||||
$table->timestamp('applied_at')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['hosting_account_id', 'created_at']);
|
||||
$table->index(['user_id', 'created_at']);
|
||||
$table->index(['direction', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('hosting_billing_invoices')) {
|
||||
Schema::create('hosting_billing_invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('hosting_plan_change_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('invoice_type');
|
||||
$table->string('status')->default('pending');
|
||||
$table->string('currency', 3)->default('GHS');
|
||||
$table->decimal('subtotal_amount', 10, 2)->default(0);
|
||||
$table->decimal('credit_amount', 10, 2)->default(0);
|
||||
$table->decimal('total_amount', 10, 2)->default(0);
|
||||
$table->string('description')->nullable();
|
||||
$table->string('provider_reference')->nullable();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->json('line_items')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['hosting_account_id', 'invoice_type']);
|
||||
$table->index(['user_id', 'status']);
|
||||
$table->index(['invoice_type', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasTable('mailboxes') && ! Schema::hasColumn('mailboxes', 'hosting_account_id')) {
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->foreignId('hosting_account_id')->nullable()->after('website_id')->constrained()->nullOnDelete();
|
||||
$table->index(['hosting_account_id', 'status']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('mailboxes') && Schema::hasColumn('mailboxes', 'hosting_account_id')) {
|
||||
Schema::table('mailboxes', function (Blueprint $table) {
|
||||
$table->dropIndex(['hosting_account_id', 'status']);
|
||||
$table->dropConstrainedForeignId('hosting_account_id');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::dropIfExists('hosting_billing_invoices');
|
||||
Schema::dropIfExists('hosting_plan_changes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('hosting_account_members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('hosting_account_id')->constrained('hosting_accounts')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('invited_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('role')->default('developer');
|
||||
$table->timestamp('invited_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['hosting_account_id', 'user_id']);
|
||||
$table->index(['user_id', 'role']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('hosting_account_members');
|
||||
}
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('hosting_account_members', function (Blueprint $table) {
|
||||
$table->text('ssh_public_key')->nullable()->after('invited_at');
|
||||
$table->timestamp('ssh_key_installed_at')->nullable()->after('ssh_public_key');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('hosting_account_members', function (Blueprint $table) {
|
||||
$table->dropColumn(['ssh_public_key', 'ssh_key_installed_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('hosted_sites', function (Blueprint $table) {
|
||||
$table->string('ssl_status')->nullable()->after('ssl_enabled');
|
||||
$table->text('ssl_error')->nullable()->after('ssl_status');
|
||||
$table->timestamp('ssl_provisioned_at')->nullable()->after('ssl_expires_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('hosted_sites', function (Blueprint $table) {
|
||||
$table->dropColumn(['ssl_status', 'ssl_error', 'ssl_provisioned_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_products') || ! Schema::hasTable('hosting_accounts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targets = [
|
||||
'starter-plan' => 150000,
|
||||
'basic-plan' => 300000,
|
||||
'plus-plan' => 300000,
|
||||
'growth-plan' => 500000,
|
||||
'pro-plan' => 800000,
|
||||
'wp-starter' => 300000,
|
||||
'wp-business' => 300000,
|
||||
];
|
||||
|
||||
foreach ($targets as $slug => $inodeLimit) {
|
||||
$productId = (int) (DB::table('hosting_products')->where('slug', $slug)->value('id') ?? 0);
|
||||
|
||||
if ($productId > 0) {
|
||||
$this->setAccountsForProduct($productId, $inodeLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_products') || ! Schema::hasTable('hosting_accounts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fallbacks = [
|
||||
'starter-plan' => 50000,
|
||||
'basic-plan' => 100000,
|
||||
'plus-plan' => 100000,
|
||||
'growth-plan' => 300000,
|
||||
'pro-plan' => 600000,
|
||||
'wp-starter' => 100000,
|
||||
'wp-business' => 100000,
|
||||
];
|
||||
|
||||
foreach ($fallbacks as $slug => $inodeLimit) {
|
||||
$productId = (int) (DB::table('hosting_products')->where('slug', $slug)->value('id') ?? 0);
|
||||
|
||||
if ($productId > 0) {
|
||||
$this->setAccountsForProduct($productId, $inodeLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setAccountsForProduct(int $productId, int $inodeLimit): void
|
||||
{
|
||||
DB::table('hosting_accounts')
|
||||
->where('hosting_product_id', $productId)
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($accounts) use ($inodeLimit): void {
|
||||
foreach ($accounts as $account) {
|
||||
$resourceLimits = json_decode((string) ($account->resource_limits ?? '[]'), true);
|
||||
if (! is_array($resourceLimits)) {
|
||||
$resourceLimits = [];
|
||||
}
|
||||
|
||||
$resourceLimits['inode_limit'] = $inodeLimit;
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->where('id', $account->id)
|
||||
->update([
|
||||
'inode_limit' => $inodeLimit,
|
||||
'resource_limits' => json_encode($resourceLimits, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private array $plans = [
|
||||
'starter-plan' => [
|
||||
'description' => 'Entry-level single-domain hosting with 8 GB SSD storage, unmetered bandwidth, 2 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 3 FTP users, and 3 MySQL databases.',
|
||||
'disk_gb' => 8,
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 3,
|
||||
'max_email_accounts' => 2,
|
||||
'max_ftp_accounts' => 3,
|
||||
'limits' => [75, 768, 15, 8, 225000],
|
||||
],
|
||||
'basic-plan' => [
|
||||
'description' => 'Perfect for individuals & small business websites. Includes 15 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 15 FTP users, and 15 MySQL databases.',
|
||||
'disk_gb' => 15,
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 15,
|
||||
'max_email_accounts' => 8,
|
||||
'max_ftp_accounts' => 15,
|
||||
'limits' => [75, 768, 15, 8, 450000],
|
||||
],
|
||||
'plus-plan' => [
|
||||
'description' => 'Enhanced single-domain hosting with 30 GB SSD storage, unmetered bandwidth, 15 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder + WordPress AI, Softaculous installer, twice-weekly backups, SSH access, 30 FTP users, and 30 MySQL databases.',
|
||||
'disk_gb' => 30,
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 30,
|
||||
'max_email_accounts' => 15,
|
||||
'max_ftp_accounts' => 30,
|
||||
'limits' => [75, 768, 15, 8, 450000],
|
||||
],
|
||||
'growth-plan' => [
|
||||
'description' => 'For businesses managing multiple websites. Host up to 8 websites with 45 GB SSD storage, unmetered bandwidth, 23 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, 75 FTP users, and 75 MySQL databases.',
|
||||
'disk_gb' => 45,
|
||||
'max_domains' => 8,
|
||||
'max_databases' => 75,
|
||||
'max_email_accounts' => 23,
|
||||
'max_ftp_accounts' => 75,
|
||||
'limits' => [150, 1536, 30, 15, 750000],
|
||||
],
|
||||
'pro-plan' => [
|
||||
'description' => 'Unlimited websites with 225 GB SSD storage and unmetered bandwidth, 38 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
|
||||
'disk_gb' => 225,
|
||||
'max_domains' => -1,
|
||||
'max_databases' => -1,
|
||||
'max_email_accounts' => 38,
|
||||
'max_ftp_accounts' => -1,
|
||||
'limits' => [150, 1536, 30, 15, 1200000],
|
||||
],
|
||||
'wp-starter' => [
|
||||
'description' => 'Optimized WordPress hosting with 23 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed + WordPress optimization, AI for WordPress, 1-click WordPress install, automatic updates, daily backups, and SSH access.',
|
||||
'disk_gb' => 23,
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 2,
|
||||
'max_email_accounts' => 8,
|
||||
'max_ftp_accounts' => null,
|
||||
'limits' => [113, 1152, 23, 12, 450000],
|
||||
],
|
||||
'wp-business' => [
|
||||
'description' => 'Premium WordPress hosting with unlimited WordPress sites, 75 GB SSD storage, unmetered bandwidth, 38 free email accounts, advanced security (Imunify360-level), free SSL + CDN, LiteSpeed + advanced caching, AI for WordPress, staging environment, daily backups + restore points, SSH access, and priority performance.',
|
||||
'disk_gb' => 75,
|
||||
'max_domains' => -1,
|
||||
'max_databases' => -1,
|
||||
'max_email_accounts' => 38,
|
||||
'max_ftp_accounts' => null,
|
||||
'limits' => [113, 1152, 23, 12, 450000],
|
||||
],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_products')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->plans as $slug => $plan) {
|
||||
$product = DB::table('hosting_products')->where('slug', $slug)->first();
|
||||
|
||||
if (! $product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('hosting_products')
|
||||
->where('id', $product->id)
|
||||
->update(array_filter([
|
||||
'description' => $plan['description'],
|
||||
'disk_gb' => $plan['disk_gb'],
|
||||
'max_domains' => $plan['max_domains'],
|
||||
'max_databases' => $plan['max_databases'],
|
||||
'max_email_accounts' => $plan['max_email_accounts'],
|
||||
'max_ftp_accounts' => $plan['max_ftp_accounts'],
|
||||
'updated_at' => now(),
|
||||
], fn ($value) => $value !== null));
|
||||
|
||||
$this->updateAccountsForProduct((int) $product->id, $plan);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Do not reduce customer quotas automatically on rollback.
|
||||
}
|
||||
|
||||
private function updateAccountsForProduct(int $productId, array $plan): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_accounts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$cpuLimit, $memoryLimit, $processLimit, $ioLimit, $inodeLimit] = $plan['limits'];
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->where('hosting_product_id', $productId)
|
||||
->orderBy('id')
|
||||
->chunkById(100, function ($accounts) use ($plan, $cpuLimit, $memoryLimit, $processLimit, $ioLimit, $inodeLimit): void {
|
||||
foreach ($accounts as $account) {
|
||||
$resourceLimits = json_decode((string) ($account->resource_limits ?? '[]'), true);
|
||||
if (! is_array($resourceLimits)) {
|
||||
$resourceLimits = [];
|
||||
}
|
||||
|
||||
$resourceLimits['disk_gb'] = max((int) ($resourceLimits['disk_gb'] ?? 0), (int) $plan['disk_gb']);
|
||||
$resourceLimits['cpu_limit_percent'] = max((int) ($resourceLimits['cpu_limit_percent'] ?? 0), $cpuLimit);
|
||||
$resourceLimits['memory_limit_mb'] = max((int) ($resourceLimits['memory_limit_mb'] ?? 0), $memoryLimit);
|
||||
$resourceLimits['process_limit'] = max((int) ($resourceLimits['process_limit'] ?? 0), $processLimit);
|
||||
$resourceLimits['io_limit_mb'] = max((int) ($resourceLimits['io_limit_mb'] ?? 0), $ioLimit);
|
||||
$resourceLimits['inode_limit'] = max((int) ($resourceLimits['inode_limit'] ?? 0), $inodeLimit);
|
||||
$this->raiseFiniteResourceLimit($resourceLimits, 'max_domains', $plan['max_domains'] ?? null);
|
||||
$this->raiseFiniteResourceLimit($resourceLimits, 'max_databases', $plan['max_databases'] ?? null);
|
||||
$this->raiseFiniteResourceLimit($resourceLimits, 'max_email_accounts', $plan['max_email_accounts'] ?? null);
|
||||
$this->raiseFiniteResourceLimit($resourceLimits, 'max_ftp_accounts', $plan['max_ftp_accounts'] ?? null);
|
||||
|
||||
$updates = [
|
||||
'allocated_disk_gb' => max((int) ($account->allocated_disk_gb ?? 0), (int) $plan['disk_gb']),
|
||||
'cpu_limit_percent' => max((int) ($account->cpu_limit_percent ?? 0), $cpuLimit),
|
||||
'memory_limit_mb' => max((int) ($account->memory_limit_mb ?? 0), $memoryLimit),
|
||||
'process_limit' => max((int) ($account->process_limit ?? 0), $processLimit),
|
||||
'io_limit_mb' => max((int) ($account->io_limit_mb ?? 0), $ioLimit),
|
||||
'inode_limit' => max((int) ($account->inode_limit ?? 0), $inodeLimit),
|
||||
'resource_limits' => json_encode($resourceLimits, JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
DB::table('hosting_accounts')
|
||||
->where('id', $account->id)
|
||||
->update($updates);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function raiseFiniteResourceLimit(array &$resourceLimits, string $key, mixed $newValue): void
|
||||
{
|
||||
if ($newValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) $newValue === -1 || (int) ($resourceLimits[$key] ?? 0) === -1) {
|
||||
$resourceLimits[$key] = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
$resourceLimits[$key] = max((int) ($resourceLimits[$key] ?? 0), (int) $newValue);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_products')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->serverProducts() as $slug => $payload) {
|
||||
DB::table('hosting_products')
|
||||
->where('slug', $slug)
|
||||
->update($payload + [
|
||||
'currency' => 'GHS',
|
||||
'price_monthly' => 0,
|
||||
'price_quarterly' => 0,
|
||||
'price_yearly' => 0,
|
||||
'price_biennial' => 0,
|
||||
'deleted_at' => null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->clearPricingCaches();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('hosting_products')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$legacy = [
|
||||
'vps-s' => ['contabo_product_id' => 'V10', 'price_monthly' => 75, 'price_quarterly' => 215, 'price_yearly' => 720],
|
||||
'vps-m' => ['contabo_product_id' => 'V20', 'price_monthly' => 90, 'price_quarterly' => 255, 'price_yearly' => 865],
|
||||
'vps-l' => ['contabo_product_id' => 'V30', 'price_monthly' => 120, 'price_quarterly' => 340, 'price_yearly' => 1150],
|
||||
'vps-xl' => ['contabo_product_id' => 'V40', 'price_monthly' => 150, 'price_quarterly' => 430, 'price_yearly' => 1440],
|
||||
];
|
||||
|
||||
foreach ($legacy as $slug => $payload) {
|
||||
DB::table('hosting_products')
|
||||
->where('slug', $slug)
|
||||
->update($payload + [
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->clearPricingCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function serverProducts(): array
|
||||
{
|
||||
return [
|
||||
'vps-s' => [
|
||||
'description' => 'Entry-level VPS with 3 vCPU cores, 8 GB RAM, 75 GB NVMe SSD, and unlimited bandwidth. Perfect for small projects and development.',
|
||||
'disk_gb' => 75,
|
||||
'cpu_cores' => 3,
|
||||
'ram_mb' => 8192,
|
||||
'contabo_product_id' => 'V91',
|
||||
'contabo_region' => 'EU',
|
||||
'sort_order' => 10,
|
||||
],
|
||||
'vps-m' => [
|
||||
'description' => 'Mid-range VPS with 6 vCPU cores, 12 GB RAM, 100 GB NVMe SSD, and unlimited bandwidth. Ideal for growing applications and medium traffic sites.',
|
||||
'disk_gb' => 100,
|
||||
'cpu_cores' => 6,
|
||||
'ram_mb' => 12288,
|
||||
'contabo_product_id' => 'V94',
|
||||
'contabo_region' => 'EU',
|
||||
'sort_order' => 11,
|
||||
],
|
||||
'vps-l' => [
|
||||
'description' => 'High-performance VPS with 8 vCPU cores, 24 GB RAM, 200 GB NVMe SSD, and unlimited bandwidth. Built for demanding workloads and high-traffic applications.',
|
||||
'disk_gb' => 200,
|
||||
'cpu_cores' => 8,
|
||||
'ram_mb' => 24576,
|
||||
'contabo_product_id' => 'V97',
|
||||
'contabo_region' => 'EU',
|
||||
'sort_order' => 12,
|
||||
],
|
||||
'vps-xl' => [
|
||||
'description' => 'Enterprise VPS with 12 vCPU cores, 48 GB RAM, 250 GB NVMe SSD, and unlimited bandwidth. Maximum performance for resource-intensive applications.',
|
||||
'disk_gb' => 250,
|
||||
'cpu_cores' => 12,
|
||||
'ram_mb' => 49152,
|
||||
'contabo_product_id' => 'V100',
|
||||
'contabo_region' => 'EU',
|
||||
'sort_order' => 13,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function clearPricingCaches(): void
|
||||
{
|
||||
Cache::forget('contabo_products');
|
||||
|
||||
foreach (['V10', 'V20', 'V30', 'V40', 'V91', 'V94', 'V97', 'V100'] as $productId) {
|
||||
Cache::forget("contabo_product_price_{$productId}");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('hosting_nodes', 'role')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('hosting_nodes', function (Blueprint $table) {
|
||||
$table->string('role', 20)->default('primary')->after('type');
|
||||
$table->foreignId('primary_hosting_node_id')
|
||||
->nullable()
|
||||
->after('role')
|
||||
->constrained('hosting_nodes')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index(['role', 'primary_hosting_node_id']);
|
||||
});
|
||||
|
||||
DB::table('hosting_nodes')->update(['role' => 'primary']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('hosting_nodes', function (Blueprint $table) {
|
||||
$table->dropForeign(['primary_hosting_node_id']);
|
||||
$table->dropIndex(['role', 'primary_hosting_node_id']);
|
||||
$table->dropColumn(['role', 'primary_hosting_node_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('contabo_infrastructure_provisions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('contabo_infrastructure_provisions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('purpose');
|
||||
$table->string('status')->default('pending');
|
||||
$table->string('name');
|
||||
$table->string('contabo_product_id');
|
||||
$table->string('region');
|
||||
$table->string('image_id')->nullable();
|
||||
$table->string('contabo_instance_id')->nullable();
|
||||
$table->string('completion_token', 64);
|
||||
$table->json('payload')->nullable();
|
||||
$table->json('log')->nullable();
|
||||
$table->text('ssh_public_key')->nullable();
|
||||
$table->text('ssh_private_key')->nullable();
|
||||
$table->text('generated_mail_db_password')->nullable();
|
||||
$table->foreignId('hosting_node_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->unsignedBigInteger('email_server_id')->nullable();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamp('instance_created_at')->nullable();
|
||||
$table->timestamp('ip_assigned_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamp('failed_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['status', 'purpose']);
|
||||
$table->index('completion_token');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('contabo_infrastructure_provisions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('hosting_nodes')
|
||||
->where('provider', 'local')
|
||||
->where('disk_gb', 100)
|
||||
->update(['disk_gb' => 150]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('hosting_nodes')
|
||||
->where('provider', 'local')
|
||||
->where('disk_gb', 150)
|
||||
->update(['disk_gb' => 100]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Team membership: links a user to an account (owner) whose mailboxes &
|
||||
// domains they can manage. Invited by email; linked on first SSO login.
|
||||
Schema::create('email_team_members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('account_id')->index(); // owner user id
|
||||
$table->unsignedBigInteger('user_id')->nullable()->index(); // member user (set on accept)
|
||||
$table->string('email');
|
||||
$table->string('role', 20)->default('member'); // admin|member
|
||||
$table->string('status', 20)->default('invited'); // invited|active
|
||||
$table->string('token', 64)->nullable();
|
||||
$table->timestamp('accepted_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['account_id', 'email']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('email_team_members');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Per-account preferences for the Email product.
|
||||
Schema::create('email_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('default_quota_mb')->nullable(); // default new-mailbox quota
|
||||
$table->string('notify_email')->nullable(); // where billing/alerts go
|
||||
$table->boolean('product_updates')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('email_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->text('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('hosting_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
|
||||
$table->string('notify_email')->nullable();
|
||||
$table->boolean('product_updates')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('hosting_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Team membership: links a user to an account (owner) whose mailboxes &
|
||||
// domains they can manage. Invited by email; linked on first SSO login.
|
||||
Schema::create('hosting_team_members', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('account_id')->index(); // owner user id
|
||||
$table->unsignedBigInteger('user_id')->nullable()->index(); // member user (set on accept)
|
||||
$table->string('email');
|
||||
$table->string('role', 20)->default('member'); // admin|member
|
||||
$table->string('status', 20)->default('invited'); // invited|active
|
||||
$table->string('token', 64)->nullable();
|
||||
$table->timestamp('accepted_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['account_id', 'email']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('hosting_team_members');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
foreach ([
|
||||
'hosting_accounts',
|
||||
'customer_hosting_orders',
|
||||
'hosting_orders',
|
||||
'hosting_billing_invoices',
|
||||
'hosting_plan_changes',
|
||||
] as $table) {
|
||||
if (Schema::hasTable($table) && ! Schema::hasColumn($table, 'platform_id')) {
|
||||
Schema::table($table, function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('platform_id')->nullable()->unique()->after('id');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach ([
|
||||
'hosting_accounts',
|
||||
'customer_hosting_orders',
|
||||
'hosting_orders',
|
||||
'hosting_billing_invoices',
|
||||
'hosting_plan_changes',
|
||||
] as $table) {
|
||||
if (Schema::hasTable($table) && Schema::hasColumn($table, 'platform_id')) {
|
||||
Schema::table($table, function (Blueprint $table): void {
|
||||
$table->dropColumn('platform_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
$table->morphs('notifiable');
|
||||
$table->text('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Database\Seeders\HostingProductSeeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
HostingProductSeeder::class,
|
||||
]);
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\HostingNode;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class HostingNodeSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
HostingNode::firstOrCreate(
|
||||
['provider' => 'local', 'ip_address' => '127.0.0.1'],
|
||||
[
|
||||
'name' => 'Primary Server',
|
||||
'hostname' => gethostname() ?: 'localhost',
|
||||
'ip_address' => '127.0.0.1',
|
||||
'ipv6_address' => '::1',
|
||||
'type' => 'shared',
|
||||
'segment' => 'general',
|
||||
'provider' => 'local',
|
||||
'cpu_cores' => 4,
|
||||
'ram_mb' => 8192,
|
||||
'disk_gb' => 150,
|
||||
'oversell_ratio' => 3,
|
||||
'bandwidth_tb' => 10,
|
||||
'max_accounts' => 100,
|
||||
'current_accounts' => 0,
|
||||
'status' => 'active',
|
||||
'ssh_port' => 22,
|
||||
'features' => ['php', 'mysql', 'nginx', 'ssl'],
|
||||
'installed_software' => [
|
||||
'php' => ['8.1', '8.2', '8.3'],
|
||||
'mysql' => '8.0',
|
||||
'nginx' => '1.24',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\HostingProduct;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class HostingProductSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$products = [
|
||||
// ============================================
|
||||
// SINGLE DOMAIN HOSTING
|
||||
// Perfect for individuals & small business websites
|
||||
// ============================================
|
||||
[
|
||||
'name' => 'Starter Plan',
|
||||
'slug' => 'starter-plan',
|
||||
'description' => 'Entry-level single-domain hosting with 8 GB SSD storage, unmetered bandwidth, 2 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 3 FTP users, and 3 MySQL databases.',
|
||||
'category' => 'shared',
|
||||
'type' => 'single_domain',
|
||||
'price_monthly' => 1.00,
|
||||
'price_yearly' => 12.00, // 1 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 8,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 3,
|
||||
'max_email_accounts' => 2,
|
||||
'max_ftp_accounts' => 3,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 7, // Weekly backups
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'AI Website Builder',
|
||||
'Softaculous installer',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Weekly backups',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => false,
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'name' => 'Basic Plan',
|
||||
'slug' => 'basic-plan',
|
||||
'description' => 'Perfect for individuals & small business websites. Includes 15 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 15 FTP users, and 15 MySQL databases.',
|
||||
'category' => 'shared',
|
||||
'type' => 'single_domain',
|
||||
'price_monthly' => 25.00,
|
||||
'price_yearly' => 300.00, // 25 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 15,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 15,
|
||||
'max_email_accounts' => 8,
|
||||
'max_ftp_accounts' => 15,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 7, // Weekly backups
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'AI Website Builder',
|
||||
'Softaculous installer',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Weekly backups',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => false,
|
||||
'sort_order' => 2,
|
||||
],
|
||||
[
|
||||
'name' => 'Plus Plan',
|
||||
'slug' => 'plus-plan',
|
||||
'description' => 'Enhanced single-domain hosting with 30 GB SSD storage, unmetered bandwidth, 15 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder + WordPress AI, Softaculous installer, twice-weekly backups, SSH access, 30 FTP users, and 30 MySQL databases.',
|
||||
'category' => 'shared',
|
||||
'type' => 'single_domain',
|
||||
'price_monthly' => 35.00,
|
||||
'price_yearly' => 420.00, // 35 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 30,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 30,
|
||||
'max_email_accounts' => 15,
|
||||
'max_ftp_accounts' => 30,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 4, // Twice-weekly backups
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'AI Website Builder',
|
||||
'WordPress AI',
|
||||
'Softaculous installer',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Twice-weekly backups',
|
||||
'SSH access',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => true,
|
||||
'sort_order' => 3,
|
||||
],
|
||||
|
||||
// ============================================
|
||||
// MULTI-DOMAIN HOSTING
|
||||
// For businesses managing multiple websites
|
||||
// ============================================
|
||||
[
|
||||
'name' => 'Growth Plan',
|
||||
'slug' => 'growth-plan',
|
||||
'description' => 'For businesses managing multiple websites. Host up to 8 websites with 45 GB SSD storage, unmetered bandwidth, 23 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, 75 FTP users, and 75 MySQL databases.',
|
||||
'category' => 'shared',
|
||||
'type' => 'multi_domain',
|
||||
'price_monthly' => 55.00,
|
||||
'price_yearly' => 660.00, // 55 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 45,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => 8,
|
||||
'max_databases' => 75,
|
||||
'max_email_accounts' => 23,
|
||||
'max_ftp_accounts' => 75,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 7, // Daily backups
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'AI tools',
|
||||
'Site builders',
|
||||
'Softaculous installer',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Daily backups',
|
||||
'SSH access',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => true,
|
||||
'sort_order' => 4,
|
||||
],
|
||||
[
|
||||
'name' => 'Pro Plan',
|
||||
'slug' => 'pro-plan',
|
||||
'description' => 'Unlimited websites with 225 GB SSD storage and unmetered bandwidth, 38 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
|
||||
'category' => 'shared',
|
||||
'type' => 'multi_domain',
|
||||
'price_monthly' => 175.00,
|
||||
'price_yearly' => 2100.00, // 175 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 225,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => -1, // Unlimited
|
||||
'max_databases' => -1, // Unlimited
|
||||
'max_email_accounts' => 38,
|
||||
'max_ftp_accounts' => -1, // Unlimited
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 7, // Daily backups
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'AI tools',
|
||||
'Site builders',
|
||||
'Softaculous installer',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Daily backups',
|
||||
'SSH access',
|
||||
'Unlimited websites',
|
||||
'Unlimited FTP users',
|
||||
'Unlimited MySQL databases',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => false,
|
||||
'sort_order' => 5,
|
||||
],
|
||||
|
||||
// ============================================
|
||||
// WORDPRESS HOSTING
|
||||
// Optimized for WordPress performance & automation
|
||||
// ============================================
|
||||
[
|
||||
'name' => 'WP Starter',
|
||||
'slug' => 'wp-starter',
|
||||
'description' => 'Optimized WordPress hosting with 23 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed + WordPress optimization, AI for WordPress, 1-click WordPress install, automatic updates, daily backups, and SSH access.',
|
||||
'category' => 'shared',
|
||||
'type' => 'wordpress',
|
||||
'price_monthly' => 40.00,
|
||||
'price_yearly' => 480.00, // 40 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 23,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => 1,
|
||||
'max_databases' => 2,
|
||||
'max_email_accounts' => 8,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 7, // Daily backups
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'WordPress optimization',
|
||||
'AI for WordPress',
|
||||
'1-click WordPress install',
|
||||
'Automatic updates',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Daily backups',
|
||||
'SSH access',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => true,
|
||||
'sort_order' => 5,
|
||||
],
|
||||
[
|
||||
'name' => 'WP Business',
|
||||
'slug' => 'wp-business',
|
||||
'description' => 'Premium WordPress hosting with unlimited WordPress sites, 75 GB SSD storage, unmetered bandwidth, 38 free email accounts, advanced security (Imunify360-level), free SSL + CDN, LiteSpeed + advanced caching, AI for WordPress, staging environment, daily backups + restore points, SSH access, and priority performance.',
|
||||
'category' => 'shared',
|
||||
'type' => 'wordpress',
|
||||
'price_monthly' => 85.00,
|
||||
'price_yearly' => 1020.00, // 85 × 12
|
||||
'currency' => 'GHS',
|
||||
'disk_gb' => 75,
|
||||
'bandwidth_gb' => null, // Unmetered
|
||||
'max_domains' => -1, // Unlimited WordPress sites
|
||||
'max_databases' => -1, // Unlimited
|
||||
'max_email_accounts' => 38,
|
||||
'ssl_included' => true,
|
||||
'backups_included' => true,
|
||||
'backup_retention_days' => 30, // Daily backups + restore points
|
||||
'features' => [
|
||||
'LiteSpeed servers',
|
||||
'Advanced caching',
|
||||
'WordPress optimization',
|
||||
'AI for WordPress',
|
||||
'Staging environment',
|
||||
'Advanced security (Imunify360-level)',
|
||||
'Free SSL',
|
||||
'Free CDN',
|
||||
'Daily backups',
|
||||
'Restore points',
|
||||
'SSH access',
|
||||
'Priority performance',
|
||||
'Unlimited WordPress sites',
|
||||
],
|
||||
'is_active' => true,
|
||||
'is_featured' => false,
|
||||
'sort_order' => 6,
|
||||
],
|
||||
|
||||
// VPS & dedicated → Ladill Servers (servers.ladill.com). See ServerProductSeeder there.
|
||||
];
|
||||
|
||||
foreach ($products as $product) {
|
||||
HostingProduct::updateOrCreate(
|
||||
['slug' => $product['slug']],
|
||||
$product
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up old hosting products that are no longer in the new pricing structure
|
||||
$newSlugs = array_column($products, 'slug');
|
||||
$oldSlugsToRemove = [
|
||||
'starter-hosting',
|
||||
'basic-hosting',
|
||||
'business-hosting',
|
||||
'pro-hosting',
|
||||
'wordpress-starter',
|
||||
'wordpress-business',
|
||||
];
|
||||
|
||||
HostingProduct::whereIn('slug', $oldSlugsToRemove)
|
||||
->whereNotIn('slug', $newSlugs)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user