Files
ladill-hosting/app/Console/Commands/ImportHostingCommand.php
isaaccladandCursor e3fd235139
Deploy Ladill Hosting / deploy (push) Successful in 26s
Unify SSO and surface legacy ResellerClub hosting orders on the dashboard.
Add silent OAuth and session keepalive, and show imported RC orders alongside
native hosting accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 06:33:16 +00:00

366 lines
12 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\RcServiceOrder;
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['rc_service_orders'] ?? [] as $row) {
$this->importRcServiceOrder($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;
$row['meta'] = $this->normalizeMeta($row['meta'] ?? null);
if ($commit) {
HostingOrder::updateOrCreate(['platform_id' => $platformId], $row);
}
}
/** @param array<string, mixed> $row */
private function importRcServiceOrder(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) {
RcServiceOrder::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,
]);
}
/** @return array<string, mixed>|null */
private function normalizeMeta(mixed $meta): ?array
{
while (is_string($meta)) {
$decoded = json_decode($meta, true);
$meta = json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}
return is_array($meta) ? $meta : null;
}
/** @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;
}
}
}