sanitizeDescription($data['description'] ?? ''), location: $this->parseLocation($data), salaryMin: $data['salary_min'] ?? null, salaryMax: $data['salary_max'] ?? null, salaryCurrency: 'USD', jobType: $this->mapJobType($data['contract_type'] ?? null), sourceUrl: $data['redirect_url'] ?? null, applyUrl: $data['redirect_url'] ?? null, postedAt: $this->parseDate($data['created'] ?? null), expiresAt: null, remoteAllowed: $this->isRemote($data), experienceLevel: null, category: $data['category']['label'] ?? null, rawData: $data, ); } catch (\Throwable $e) { throw new TransformationException( 'Adzuna', 'Failed to transform Adzuna job data: ' . $e->getMessage(), ['data' => $data, 'error' => $e->getMessage()] ); } } public function transformMany(array $jobs): array { return array_map(fn($job) => $this->transform($job), $jobs); } private function sanitizeDescription(string $description): string { $clean = strip_tags($description); $clean = html_entity_decode($clean, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $clean = preg_replace('/\s+/', ' ', $clean); return trim($clean); } private function parseLocation(array $data): ?string { $location = $data['location']['display_name'] ?? null; if (empty($location)) { // Fallback to area $area = $data['location']['area'] ?? []; if (!empty($area)) { return implode(', ', $area); } } return $location; } private function isRemote(array $data): bool { $title = strtolower($data['title'] ?? ''); $description = strtolower($data['description'] ?? ''); $location = strtolower($data['location']['display_name'] ?? ''); return str_contains($title, 'remote') || str_contains($description, 'remote') || str_contains($location, 'remote'); } private function parseDate(?string $date): ?DateTime { if (empty($date)) { return null; } try { return new DateTime($date); } catch (\Exception $e) { return null; } } private function mapJobType(?string $type): ?string { if (empty($type)) { return null; } return match (strtolower($type)) { 'permanent', 'full_time' => 'full-time', 'part_time' => 'part-time', 'contract' => 'contract', 'temporary' => 'temporary', default => 'full-time', }; } }