Files
ladill-hosting/app/Console/Commands/ImportHostingCommand.php
T
isaaccladandCursor e39a47fa1f
Deploy Ladill Hosting / deploy (push) Successful in 25s
Land on account overview and import hosted sites.
Route launcher and home hub entry to the account overview instead of the
control panel, label the sidebar app launcher, and extend hosting:import
to sync hosted_sites with platform_id matching for existing accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 17:13:46 +00:00

332 lines
11 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\CustomerHostingOrder;
use App\Models\HostedSite;
use App\Models\HostingAccount;
use App\Models\HostingAccountMember;
use App\Models\HostingBillingInvoice;
use App\Models\HostingNode;
use App\Models\HostingOrder;
use App\Models\HostingPlanChange;
use App\Models\HostingProduct;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* Zero-loss import of hosting data exported from the platform (`hosting:export`).
* Dry-run by default; pass --commit to write.
*/
class ImportHostingCommand extends Command
{
protected $signature = 'hosting:import
{file : Path to hosting-export.json from the platform}
{--commit : Actually write changes}';
protected $description = 'Import hosting accounts and orders from the platform export';
/** @var array<int,int> platform user_id → local user id */
private array $userMap = [];
/** @var array<int,int> platform account id → local account id */
private array $accountMap = [];
/** @var array<int,int> platform node id → local node id */
private array $nodeMap = [];
public function handle(): int
{
$commit = (bool) $this->option('commit');
if (! $commit) {
$this->warn('DRY RUN — no changes will be written. Re-run with --commit to apply.');
}
$path = $this->argument('file');
if (! is_file($path)) {
$this->error("File not found: {$path}");
return self::FAILURE;
}
$payload = json_decode((string) file_get_contents($path), true);
if (! is_array($payload)) {
$this->error('Invalid JSON export file.');
return self::FAILURE;
}
$apply = function (callable $callback) use ($commit): void {
if ($commit) {
DB::transaction($callback);
} else {
$callback();
}
};
$apply(function () use ($payload, $commit): void {
foreach ($payload['hosting_nodes'] ?? [] as $row) {
$this->importNode($row, $commit);
}
foreach ($payload['hosting_accounts'] ?? [] as $row) {
$this->importAccount($row, $commit);
}
foreach ($payload['hosted_sites'] ?? [] as $row) {
$this->importSite($row, $commit);
}
foreach ($payload['customer_hosting_orders'] ?? [] as $row) {
$this->importCustomerOrder($row, $commit);
}
foreach ($payload['hosting_orders'] ?? [] as $row) {
$this->importLegacyOrder($row, $commit);
}
foreach ($payload['hosting_account_members'] ?? [] as $row) {
$this->importMember($row, $commit);
}
foreach ($payload['hosting_billing_invoices'] ?? [] as $row) {
$this->importInvoice($row, $commit);
}
foreach ($payload['hosting_plan_changes'] ?? [] as $row) {
$this->importPlanChange($row, $commit);
}
});
$this->info(($commit ? 'Imported' : 'Would import').' '.count($this->accountMap).' hosting account(s).');
$siteCount = count($payload['hosted_sites'] ?? []);
if ($siteCount > 0) {
$this->info(($commit ? 'Imported' : 'Would import')." {$siteCount} hosted site(s).");
}
return self::SUCCESS;
}
/** @param array<string, mixed> $row */
private function importNode(array $row, bool $commit): void
{
$platformId = (int) ($row['id'] ?? 0);
unset($row['id'], $row['primary_hosting_node_id']);
if ($commit) {
$node = HostingNode::updateOrCreate(['hostname' => $row['hostname']], $row);
$this->nodeMap[$platformId] = $node->id;
} else {
$this->nodeMap[$platformId] = $platformId;
$this->line(" node {$platformId}{$row['hostname']}");
}
}
/** @param array<string, mixed> $row */
private function importAccount(array $row, bool $commit): void
{
$platformId = (int) ($row['id'] ?? 0);
$owner = $this->resolveUser(
(string) ($row['owner_public_id'] ?? ''),
(string) ($row['owner_email'] ?? ''),
);
if (! $owner) {
$this->warn("Skipping account #{$platformId}: no owner public_id");
return;
}
unset($row['id'], $row['owner_public_id'], $row['owner_email']);
$row['user_id'] = $owner->id;
$row['platform_id'] = $platformId;
$this->mapProductId($row);
$platformNodeId = (int) ($row['hosting_node_id'] ?? 0);
if ($platformNodeId && isset($this->nodeMap[$platformNodeId])) {
$row['hosting_node_id'] = $this->nodeMap[$platformNodeId];
}
if ($commit) {
$account = HostingAccount::query()->where('platform_id', $platformId)->first();
if (! $account && ($row['username'] ?? '') !== '') {
$account = HostingAccount::query()->where('username', $row['username'])->first();
}
if ($account) {
$account->update($row);
} else {
$account = HostingAccount::create($row);
}
$this->accountMap[$platformId] = $account->id;
} else {
$this->accountMap[$platformId] = $platformId;
$this->line(" account {$platformId} → user {$owner->email}");
}
}
/** @param array<string, mixed> $row */
private function importSite(array $row, bool $commit): void
{
$platformAccountId = (int) ($row['platform_hosting_account_id'] ?? 0);
$localAccountId = $this->accountMap[$platformAccountId] ?? null;
if (! $localAccountId) {
$this->warn("Skipping site {$row['domain']}: unknown hosting account #{$platformAccountId}");
return;
}
unset($row['platform_hosting_account_id'], $row['platform_id']);
$row['hosting_account_id'] = $localAccountId;
$row['domain_id'] = null;
if ($commit) {
HostedSite::withTrashed()->updateOrCreate(
['hosting_account_id' => $localAccountId, 'domain' => $row['domain']],
$row
);
} else {
$this->line(" site {$row['domain']} → account {$localAccountId}");
}
}
/** @param array<string, mixed> $row */
private function importCustomerOrder(array $row, bool $commit): void
{
$platformId = (int) ($row['id'] ?? 0);
$owner = $this->resolveUser((string) ($row['owner_public_id'] ?? ''));
if (! $owner) {
return;
}
unset($row['id'], $row['owner_public_id'], $row['owner_email']);
$row['user_id'] = $owner->id;
$row['platform_id'] = $platformId;
$this->mapProductId($row);
if ($commit) {
CustomerHostingOrder::updateOrCreate(['platform_id' => $platformId], $row);
}
}
/** @param array<string, mixed> $row */
private function importLegacyOrder(array $row, bool $commit): void
{
$platformId = (int) ($row['id'] ?? 0);
$owner = $this->resolveUser((string) ($row['owner_public_id'] ?? ''));
if (! $owner) {
return;
}
unset($row['id'], $row['owner_public_id'], $row['owner_email']);
$row['user_id'] = $owner->id;
$row['platform_id'] = $platformId;
if ($commit) {
HostingOrder::updateOrCreate(['platform_id' => $platformId], $row);
}
}
/** @param array<string, mixed> $row */
private function importMember(array $row, bool $commit): void
{
$member = $this->resolveUser((string) ($row['member_public_id'] ?? ''));
$platformAccountId = (int) ($row['hosting_account_id'] ?? 0);
$localAccountId = $this->accountMap[$platformAccountId] ?? null;
if (! $member || ! $localAccountId) {
return;
}
unset($row['id'], $row['owner_public_id'], $row['member_public_id']);
$row['user_id'] = $member->id;
$row['hosting_account_id'] = $localAccountId;
if (! empty($row['invited_by_user_id']) && ! User::query()->whereKey($row['invited_by_user_id'])->exists()) {
$row['invited_by_user_id'] = null;
}
if ($commit) {
HostingAccountMember::updateOrCreate(
['hosting_account_id' => $localAccountId, 'user_id' => $member->id],
$row
);
}
}
/** @param array<string, mixed> $row */
private function importInvoice(array $row, bool $commit): void
{
$platformId = (int) ($row['id'] ?? 0);
$owner = $this->resolveUser((string) ($row['owner_public_id'] ?? ''));
$platformAccountId = (int) ($row['hosting_account_id'] ?? 0);
$localAccountId = $this->accountMap[$platformAccountId] ?? null;
if (! $owner || ! $localAccountId) {
return;
}
unset($row['id'], $row['owner_public_id'], $row['owner_email']);
$row['user_id'] = $owner->id;
$row['hosting_account_id'] = $localAccountId;
$row['platform_id'] = $platformId;
if ($commit) {
HostingBillingInvoice::updateOrCreate(['platform_id' => $platformId], $row);
}
}
/** @param array<string, mixed> $row */
private function importPlanChange(array $row, bool $commit): void
{
$platformId = (int) ($row['id'] ?? 0);
$owner = $this->resolveUser((string) ($row['owner_public_id'] ?? ''));
$platformAccountId = (int) ($row['hosting_account_id'] ?? 0);
$localAccountId = $this->accountMap[$platformAccountId] ?? null;
if (! $owner || ! $localAccountId) {
return;
}
unset($row['id'], $row['owner_public_id'], $row['owner_email']);
$row['user_id'] = $owner->id;
$row['hosting_account_id'] = $localAccountId;
$row['platform_id'] = $platformId;
if ($commit) {
HostingPlanChange::updateOrCreate(['platform_id' => $platformId], $row);
}
}
private function resolveUser(string $publicId, string $email = '', string $name = ''): ?User
{
if ($publicId === '') {
return null;
}
$user = User::query()->where('public_id', $publicId)->first();
if ($user || $email === '') {
return $user;
}
return User::query()->create([
'public_id' => $publicId,
'email' => $email,
'name' => $name !== '' ? $name : $email,
]);
}
/** @param array<string, mixed> $row */
private function mapProductId(array &$row): void
{
$slug = (string) ($row['hosting_product_slug'] ?? '');
unset($row['hosting_product_slug']);
if ($slug === '') {
return;
}
$product = HostingProduct::query()->where('slug', $slug)->first();
if ($product) {
$row['hosting_product_id'] = $product->id;
}
}
}