new product_id. Built from CLI option / config. */ protected array $productMap = []; public function setProductMap(array $map): self { $this->productMap = $map; return $this; } public function setDefaultVpsProductId(int $id): self { $this->defaultVpsProductId = $id; return $this; } /** * @return array{customers: array, services: array, products: array} */ public function import(bool $preview = false, ?callable $log = null): array { $log ??= fn ($_msg) => null; $summary = [ 'customers' => ['created' => 0, 'skipped_conflict' => 0, 'already_synced' => 0, 'errors' => 0], 'vmware' => ['created' => 0, 'updated' => 0, 'vcenter_matched' => 0, 'vcenter_missing' => 0, 'errors' => 0], 'services' => ['created' => 0, 'updated' => 0, 'skipped_no_customer' => 0, 'vm_linked' => 0, 'vm_unlinked' => 0, 'errors' => 0], 'products' => ['mapped' => 0, 'fallback_default' => 0], ]; $log("→ Connecting to legacy DB and counting rows..."); $legacyUsers = DB::connection('legacy')->table('users')->count(); $legacyServices = DB::connection('legacy')->table('services')->count(); $legacyVmware = DB::connection('legacy')->table('vmware')->count(); $log(" legacy.users = {$legacyUsers}, legacy.services = {$legacyServices}, legacy.vmware = {$legacyVmware}"); if ($preview) { $log("PREVIEW MODE — no writes will be performed."); } // ----- Customers ----- $log("→ Syncing customers..."); $customerIdMap = []; // legacy user_id => new customer_id DB::connection('legacy')->table('users')->orderBy('id')->chunk(200, function ($rows) use (&$summary, &$customerIdMap, $preview, $log) { foreach ($rows as $row) { try { $existingByLegacy = Customer::where('legacy_id', $row->id)->first(); if ($existingByLegacy) { $customerIdMap[$row->id] = $existingByLegacy->id; $summary['customers']['already_synced']++; continue; } $email = trim((string) $row->email) ?: null; $phone = trim((string) ($row->sdt ?? '')) ?: null; $conflict = Customer::query() ->when($email, fn ($q) => $q->where('email', $email)) ->when($phone, fn ($q, $p) => $q->orWhere('phone', $p)) ->first(); if ($conflict) { $customerIdMap[$row->id] = $conflict->id; $summary['customers']['skipped_conflict']++; $log(" · skip user#{$row->id} ({$email}) — conflict with customer#{$conflict->id}"); if (! $preview) { // Backfill legacy_id on existing record so future runs are idempotent. $conflict->forceFill(['legacy_id' => $row->id])->saveQuietly(); } continue; } $payload = [ 'legacy_id' => $row->id, 'fullname' => $row->fullname, 'avatar' => $row->thumb ?: null, 'gender' => (int) ($row->gender ?? 0), 'address' => $row->address, 'company' => $row->company, 'phone' => $phone, 'email' => $email, 'email_verify' => $row->email_verification, 'password' => $row->password, 'credit' => (int) ($row->price ?? 0), 'total' => (int) ($row->total ?? 0), 'identity_number' => $row->cccd, 'front_identity_number' => $row->cccd_mattruoc, 'back_identity_number' => $row->cccd_matsau, 'status' => (int) ($row->is_active ?? 1), 'created_at' => $row->create_at, 'updated_at' => $row->update_at, ]; if ($preview) { // sentinel so services chunk can resolve in dry-run $customerIdMap[$row->id] = -1; $summary['customers']['created']++; continue; } $customer = new Customer(); $customer->uuid = (string) Str::uuid(); $customer->forceFill($payload)->saveQuietly(); $customerIdMap[$row->id] = $customer->id; $summary['customers']['created']++; } catch (\Throwable $e) { $summary['customers']['errors']++; Log::warning("LegacyPortalImporter customer#{$row->id} failed", ['error' => $e->getMessage()]); $log(" ! user#{$row->id} ERROR: " . $e->getMessage()); } } }); if (! $preview) { Customer::whereNotNull('legacy_id')->pluck('id', 'legacy_id') ->each(fn ($newId, $legacyId) => $customerIdMap[$legacyId] = $newId); } // ----- VMware (module_vmware) ----- $log("→ Syncing vmware → module_vmware..."); $vmwareIdMap = []; // legacy_vmware.id => new module_vmware.id // Build vCenter inventory lookup: vmware_id string => virtual_machines.id $vcenterLookup = DB::table('virtual_machines')->pluck('id', 'vm_id'); DB::connection('legacy')->table('vmware')->orderBy('id')->chunk(200, function ($rows) use (&$summary, &$vmwareIdMap, $customerIdMap, $vcenterLookup, $preview, $log) { foreach ($rows as $row) { try { $newCustomerId = $customerIdMap[$row->user_id] ?? null; $vcVmId = $row->vmware_id; // e.g. "vm-4327" $vmInventoryId = $vcVmId ? ($vcenterLookup[$vcVmId] ?? null) : null; if ($vmInventoryId) $summary['vmware']['vcenter_matched']++; else $summary['vmware']['vcenter_missing']++; $payload = [ 'legacy_id' => $row->id, 'customer_id' => $newCustomerId, 'name' => $row->vmware_name, 'ip' => $row->vmware_ip, 'port' => is_numeric($row->vmware_port) ? (int) $row->vmware_port : null, 'user' => $row->vmware_user, 'password' => $row->vmware_password, 'hostname' => $row->vmware_hostname, 'vmware_id' => $vcVmId, 'virtual_machine_id' => $vmInventoryId, 'status' => (int) ($row->vmware_status ?? 1), 'created_at' => $row->create_at, 'updated_at' => $row->update_at, ]; if ($preview) { $vmwareIdMap[$row->id] = -1; $summary['vmware']['created']++; continue; } $existing = DB::table('module_vmware')->where('legacy_id', $row->id)->first(); if ($existing) { DB::table('module_vmware')->where('id', $existing->id)->update($payload); $vmwareIdMap[$row->id] = $existing->id; $summary['vmware']['updated']++; } else { $newId = DB::table('module_vmware')->insertGetId($payload); $vmwareIdMap[$row->id] = $newId; $summary['vmware']['created']++; } } catch (\Throwable $e) { $summary['vmware']['errors']++; Log::warning("LegacyPortalImporter vmware#{$row->id} failed", ['error' => $e->getMessage()]); $log(" ! vmware#{$row->id} ERROR: " . $e->getMessage()); } } }); if (! $preview) { DB::table('module_vmware')->whereNotNull('legacy_id')->select('id', 'legacy_id')->get() ->each(fn ($r) => $vmwareIdMap[$r->legacy_id] = $r->id); } // ----- Services ----- $log("→ Syncing services..."); DB::connection('legacy')->table('services')->orderBy('id')->chunk(200, function ($rows) use (&$summary, $customerIdMap, $vmwareIdMap, $preview, $log) { foreach ($rows as $row) { try { $newCustomerId = $customerIdMap[$row->user_id] ?? null; if (! $newCustomerId) { $summary['services']['skipped_no_customer']++; continue; } $productId = $this->resolveProductId((int) $row->product_id, $summary); $newVmId = $row->vmware_id !== null ? ($vmwareIdMap[$row->vmware_id] ?? null) : null; if ($newVmId) $summary['services']['vm_linked']++; else $summary['services']['vm_unlinked']++; $payload = [ 'legacy_id' => $row->id, 'customer_id' => $newCustomerId === -1 ? null : $newCustomerId, 'product_id' => $productId, 'legacy_product_id' => (int) $row->product_id, 'vm_id' => $newVmId === -1 ? null : $newVmId, 'status' => $this->mapStatus((int) $row->is_active), 'auto_renew' => (bool) $row->auto_pay, 'started_at' => $row->create_at, 'expires_at' => $row->date_exp, 'note' => 'product_info(legacy): ' . $row->product_info, 'created_at' => $row->create_at, 'updated_at' => $row->create_at, ]; if ($preview) { $summary['services']['created']++; continue; } $service = Service::where('legacy_id', $row->id)->first(); if ($service) { $service->forceFill($payload)->saveQuietly(); $summary['services']['updated']++; } else { $service = new Service(); $service->uuid = (string) Str::uuid(); $service->forceFill($payload)->saveQuietly(); $summary['services']['created']++; } } catch (\Throwable $e) { $summary['services']['errors']++; Log::warning("LegacyPortalImporter service#{$row->id} failed", ['error' => $e->getMessage()]); $log(" ! service#{$row->id} ERROR: " . $e->getMessage()); } } }); return $summary; } protected function resolveProductId(int $legacyProductId, array &$summary): int { if (isset($this->productMap[$legacyProductId])) { $summary['products']['mapped']++; return $this->productMap[$legacyProductId]; } $summary['products']['fallback_default']++; return $this->defaultVpsProductId; } /** Convert legacy `is_active` int → new lifecycle string. */ protected function mapStatus(int $legacy): string { return match ($legacy) { 0 => 'expired', 1 => 'active', 2 => 'pending', 3 => 'suspended', default => 'pending', }; } }