sanitizeDescription($data['description'] ?? ''), location: $this->parseLocation($data), salaryMin: $this->parseSalary($data['salary'] ?? null, 'min'), salaryMax: $this->parseSalary($data['salary'] ?? null, 'max'), salaryCurrency: 'USD', jobType: $this->mapJobType($data['job_type'] ?? null), sourceUrl: $data['url'] ?? null, applyUrl: $data['url'] ?? null, postedAt: $this->parseDate($data['publication_date'] ?? null), expiresAt: null, remoteAllowed: true, // Remotive only has remote jobs experienceLevel: $this->mapExperienceLevel($data['candidate_required_location'] ?? null), category: $data['category'] ?? null, rawData: $data, ); } catch (\Throwable $e) { throw new TransformationException( 'Remotive', 'Failed to transform Remotive 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 { // Remove HTML tags and clean up whitespace $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['candidate_required_location'] ?? null; if (empty($location)) { return 'Remote'; } return $location; } private function parseSalary(?string $salary, string $type): ?float { if (empty($salary)) { return null; } // Try to extract numbers from salary string if (preg_match_all('/[\d,]+/', $salary, $matches)) { $numbers = array_map(fn($n) => (float) str_replace(',', '', $n), $matches[0]); if (count($numbers) >= 2) { return $type === 'min' ? min($numbers) : max($numbers); } elseif (count($numbers) === 1) { return $numbers[0]; } } return null; } 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 'full-time'; } return match (strtolower($type)) { 'full_time', 'full-time' => 'full-time', 'part_time', 'part-time' => 'part-time', 'contract' => 'contract', 'freelance' => 'freelance', default => 'full-time', }; } private function mapExperienceLevel(?string $location): ?string { // Remotive doesn't provide experience level directly // This is a placeholder that could be enhanced with ML/NLP return null; } }