Files
portal-api/app/Services/Imports/LegacyPortalImporter.php
2026-06-15 09:11:57 +07:00

286 lines
13 KiB
PHP

<?php
namespace App\Services\Imports;
use App\Models\Customer;
use App\Models\Service;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Sync customers + services + product mapping from legacy `portal` DB
* (connection: `legacy`) into the current portal schema.
*
* Idempotent via `legacy_id` columns. Conflicting customers (existing email
* or phone) are skipped — never overwritten.
*/
class LegacyPortalImporter
{
/** Default new-system product id used when no explicit mapping is given. */
protected int $defaultVpsProductId = 1;
/** Map legacy product_id => 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',
};
}
}