286 lines
13 KiB
PHP
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',
|
|
};
|
|
}
|
|
}
|