|null fullname => user id, cached for the process lifetime */ private static ?array $salerIdsByName = null; public function __construct(private SyncReport $report) { } /** * Map a raw `tblclients` row (array or stdClass) to a ClientData DTO. */ public function transform(array|object $row): ClientData { $row = (array) $row; $fullname = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')); if ($fullname === '') { $fullname = $row['companyname'] ?: null; } $this->auditMissingInfo($row, $fullname); return new ClientData( uuid: $row['uuid'], fullname: $fullname, company: $row['companyname'] ?: null, address: trim(($row['address1'] ?? '') . ', ' . ($row['address2'] ?? ''), ', ') ?: null, state: $row['state'] ?: null, city: $row['city'] ?: null, // ponytail: WHMCS repurposes the `postcode` column to store the salesperson's fullname saler: $this->resolveSalerId($row['postcode'] ?? null), country: $row['country'] ?: null, phone: PhoneNumber::normalize($row['phonenumber'] ?? null), email: $row['email'] ? strtolower($row['email']) : null, // ponytail: WHMCS hashes are not Laravel bcrypt, copied as-is; customer must reset password to log into portal password: $row['password'] ?: null, note: $row['notes'] ?: null, credit: (int) round((float) $row['credit']), status: $row['status'] === 'Active' ? 1 : 0, emailMarketing: ((int) ($row['emailoptout'] ?? 0)) === 1 ? 0 : 1, createdAt: $row['created_at'] ?: $row['datecreated'] ?: null, updatedAt: $row['updated_at'] ?: $row['datecreated'] ?: null, ); } private function auditMissingInfo(array $row, ?string $fullname): void { $ref = (string) ($row['email'] ?: "#{$row['id']}"); $missing = []; if (! $fullname) { $missing[] = 'họ tên'; } if (! PhoneNumber::normalize($row['phonenumber'] ?? null)) { $missing[] = 'số điện thoại'; } if (trim(($row['address1'] ?? '') . ($row['address2'] ?? '')) === '') { $missing[] = 'địa chỉ'; } if ($missing) { $this->report->note('client', $ref, $row['id'], 'thieu_thong_tin', 'Thiếu: ' . implode(', ', $missing)); } if (! filter_var($row['email'] ?? '', FILTER_VALIDATE_EMAIL) || preg_match('/@(gmail|yahoo|hotmail|outlook)\.com\.\w+$/i', (string) $row['email'])) { $this->report->note('client', $ref, $row['id'], 'email_nghi_sai', "Email WHMCS có vẻ không hợp lệ: {$row['email']}"); } if (! empty($row['postcode']) && $this->resolveSalerId($row['postcode']) === null) { $this->report->note('client', $ref, $row['id'], 'khong_map_duoc_sale', "Không tìm thấy nhân viên sale tên \"{$row['postcode']}\" trong bảng user."); } } private function resolveSalerId(?string $fullname): ?int { if (! $fullname) { return null; } self::$salerIdsByName ??= DB::table('user')->get(['id', 'fullname'])->mapWithKeys(fn ($user) => [trim((string) $user->fullname) => $user->id])->all(); return self::$salerIdsByName[trim($fullname)] ?? null; } }