json([ 'success' => true, 'languages' => [ ['code' => 'HI', 'name' => 'Hindi (हिन्दी)'], ['code' => 'SW', 'name' => 'Swahili (Kiswahili)'], ['code' => 'TL', 'name' => 'Tagalog (Wikang Tagalog)'], ['code' => 'TA', 'name' => 'Tamil (தமிழ்)'], ] ], 200); } /** * Get all master skills for worker registration/onboarding. */ public function skills() { $skills = \App\Models\Skill::all()->map(function ($skill) { return [ 'id' => $skill->id, 'name' => $skill->name, 'image_url' => $skill->image_path ? asset('storage/' . $skill->image_path) : null, ]; }); return response()->json([ 'success' => true, 'skills' => $skills ], 200); } /** * Step 1: Send OTP to worker's mobile number or email. * Accepts: phone OR email (one is required). */ public function sendOtp(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required_without:email|nullable|string|max:50|unique:workers,phone', 'email' => 'required_without:phone|nullable|email|max:255|unique:workers,email', ], [ 'phone.required_without' => 'Please provide a mobile number or email address.', 'phone.unique' => 'This mobile number is already registered.', 'email.required_without' => 'Please provide a mobile number or email address.', 'email.unique' => 'This email address is already registered.', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } // Determine identifier (phone takes priority) $identifier = $request->phone ?? $request->email; $isPhone = !empty($request->phone); // Generate OTP (always 111111 in local/dev, random in production) $otp = app()->environment('production') ? strval(rand(100000, 999999)) : '111111'; // Cache OTP for 10 minutes Cache::put('worker_otp_' . $identifier, [ 'code' => $otp, 'expires_at' => now()->addMinutes(10)->timestamp, ], 600); // Send OTP via email if email provided (phone SMS would be handled by SMS gateway) if (!$isPhone && $request->email) { try { Mail::raw("Your Migrant Worker verification code is: {$otp}\n\nThis code expires in 10 minutes.", function ($m) use ($request, $otp) { $m->to($request->email) ->subject('Your Migrant Worker Verification Code'); }); } catch (\Exception $mailEx) { logger()->error('Worker OTP Mail Failure: ' . $mailEx->getMessage()); } } return response()->json([ 'success' => true, 'message' => $isPhone ? 'OTP sent to your mobile number.' : 'OTP sent to your email address.', 'identifier' => $identifier, 'otp' => app()->environment('production') ? null : $otp, // Return OTP in response in non-prod for testing convenience ], 200); } /** * Step 2: Verify OTP sent to mobile/email. * Accepts: phone OR email, otp. */ public function verifyOtp(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required_without:email|nullable|string|max:50', 'email' => 'required_without:phone|nullable|email|max:255', 'otp' => 'required|string|size:6', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } $identifier = $request->phone ?? $request->email; $cachedData = Cache::get('worker_otp_' . $identifier); if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) { return response()->json([ 'success' => false, 'message' => 'Invalid or expired verification code.' ], 400); } // Handle any dirty/legacy cache objects gracefully if (is_object($cachedData['expires_at']) || $cachedData['expires_at'] instanceof \__PHP_Incomplete_Class) { Cache::forget('worker_otp_' . $identifier); return response()->json([ 'success' => false, 'message' => 'Session expired. Please request a new verification code.' ], 400); } if ($cachedData['code'] !== $request->otp || now()->timestamp > $cachedData['expires_at']) { return response()->json([ 'success' => false, 'message' => 'Invalid or expired verification code.' ], 400); } // Mark OTP as verified in cache (used as gate for profile setup step) Cache::put('worker_otp_verified_' . $identifier, true, 3600); // 1 hour grace Cache::forget('worker_otp_' . $identifier); return response()->json([ 'success' => true, 'message' => 'Mobile number verified successfully. Please complete your profile.', 'identifier' => $identifier, ], 200); } /** * Step 3: Complete basic profile setup after OTP verification. * Accepts: phone OR email, name, nationality, skills (array of skill IDs), language. */ public function setupProfile(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required_without:email|nullable|string|max:50', 'email' => 'required_without:phone|nullable|email|max:255', 'name' => 'required|string|max:255', 'nationality' => 'required|string|max:100', 'language' => 'nullable|string', 'gender' => 'nullable|string|in:male,female,other', 'live_in_out' => 'nullable|string|in:live_in,live_out', 'preferred_location' => 'nullable|string|max:255', 'main_profession' => 'nullable|string|max:100', 'skills' => 'nullable|array', 'skills.*' => 'integer|exists:skills,id', 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } $identifier = $request->phone ?? $request->email; // Ensure OTP was verified in this session if (!Cache::get('worker_otp_verified_' . $identifier)) { return response()->json([ 'success' => false, 'message' => 'OTP verification is required before profile setup.' ], 403); } // Check if already registered if ($request->phone && Worker::where('phone', $request->phone)->exists()) { return response()->json([ 'success' => false, 'message' => 'This mobile number is already registered.' ], 409); } if ($request->email && Worker::where('email', $request->email)->exists()) { return response()->json([ 'success' => false, 'message' => 'This email address is already registered.' ], 409); } try { // Build email for workers who registered by phone $email = $request->email; if (!$email && $request->phone) { $phoneClean = preg_replace('/[^0-9]/', '', $request->phone); $email = "worker.{$phoneClean}@migrant.ae"; if (Worker::where('email', $email)->exists()) { $email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae"; } } $apiToken = Str::random(80); $worker = DB::transaction(function () use ($request, $email, $apiToken) { $worker = Worker::create([ 'name' => $request->name, 'email' => $email, 'phone' => $request->phone ?? null, 'language' => $request->language ?? 'HI', // Default to Hindi if not selected 'password' => Hash::make(Str::random(16)), // No password at this stage 'nationality' => $request->nationality, 'gender' => $request->gender, 'live_in_out' => $request->live_in_out, 'preferred_location' => $request->preferred_location, 'main_profession' => $request->main_profession, 'age' => '18-25', // Default — updated later in full profile 'salary' => 1500, // Default AED — updated later 'availability' => 'Immediate', 'experience' => 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'New worker on Migrant platform.', 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, 'fcm_token' => $request->fcm_token, ]); // Sync skills if provided if (!empty($request->skills)) { $worker->skills()->sync($request->skills); } return $worker; }); // Clear the OTP verification gate Cache::forget('worker_otp_verified_' . $identifier); $worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']); return response()->json([ 'success' => true, 'message' => 'Registration complete! Welcome to Migrant.', 'data' => [ 'worker' => $worker, 'token' => $apiToken, ] ], 201); } catch (\Exception $e) { logger()->error('Worker Profile Setup Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Registration failed. Please try again.', 'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * Unified registration — supports direct JSON payloads for passport and visa. */ public function register(Request $request) { $data = $request->all(); if (isset($data['passport']) && is_string($data['passport'])) { $decoded = json_decode($data['passport'], true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { $data['passport'] = $decoded; } } if (isset($data['visa']) && is_string($data['visa'])) { $decoded = json_decode($data['visa'], true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { $data['visa'] = $decoded; } } $request->merge($data); $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'phone' => 'required|string|max:50|unique:workers,phone', 'salary' => 'required|numeric|min:0', 'password' => 'required|string|min:6', 'skills' => 'nullable', 'language' => 'nullable|string', 'nationality' => 'nullable|string|max:100', 'age' => [ 'nullable', function ($attribute, $value, $fail) { if (is_numeric($value)) { $intValue = (int)$value; if ($intValue < 18 || $intValue > 100) { $fail('The age must be between 18 and 100.'); } } else { if (!in_array($value, ['18-25', '26-35', '36-45', '46-55', '56-60'])) { $fail('The age must be a valid number or one of the ranges: 18-25, 26-35, 36-45, 46-55, 56-60.'); } } } ], 'experience' => 'nullable|string|max:100', 'in_country' => 'nullable', 'visa_status' => 'nullable|string', 'preferred_job_type' => 'nullable|string|max:100', 'main_profession' => 'nullable|string|max:100', 'gender' => 'nullable|string|in:male,female,other', 'live_in_out' => 'nullable|string|in:live_in,live_out', 'preferred_location' => 'nullable|string|max:255', 'fcm_token' => 'nullable|string|max:255', 'passport' => 'nullable|array', 'passport.passport_number' => [ 'nullable', 'string', Rule::unique('worker_documents', 'number')->where(function ($query) { return $query->where('type', 'passport'); }), ], 'passport.document_type' => 'nullable|string|max:10', 'passport.issuing_country' => 'nullable|string|max:10', 'passport.surname' => 'nullable|string|max:255', 'passport.given_names' => 'nullable|string|max:255', 'passport.nationality' => 'nullable|string|max:100', 'passport.date_of_birth' => 'nullable|string', 'passport.date_of_expiry' => 'nullable|string', 'passport.place_of_birth' => 'nullable|string|max:255', 'passport.authority' => 'nullable|string|max:255', 'passport.sex' => 'nullable|string|max:10', 'passport.date_of_issue' => 'nullable|string', 'passport.ocr_accuracy' => 'nullable|numeric', 'visa' => 'nullable|array', 'visa.document_type' => 'nullable|string|max:100', 'visa.country' => 'nullable|string|max:100', 'visa.visa_type' => 'nullable|string|max:100', 'visa.entry_permit_no' => 'nullable|string|max:100', 'visa.issue_date' => 'nullable|string', 'visa.valid_until' => 'nullable|string', 'visa.uid_no' => 'nullable|string|max:100', 'visa.full_name' => 'nullable|string|max:255', 'visa.nationality' => 'nullable|string|max:100', 'visa.place_of_birth' => 'nullable|string|max:255', 'visa.date_of_birth' => 'nullable|string', 'visa.passport_no' => 'nullable|string|max:100', 'visa.profession' => 'nullable|string|max:255', 'visa.sponsor_name' => 'nullable|string|max:255', 'visa.ocr_accuracy' => 'nullable|numeric', ], [ 'phone.unique' => 'This mobile number is already registered.', 'passport.passport_number.unique' => 'This passport number is already registered.', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $phoneClean = preg_replace('/[^0-9]/', '', $request->phone); $email = "worker.{$phoneClean}@migrant.ae"; if (Worker::where('email', $email)->exists()) { $email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae"; } $skillsArray = []; if ($request->has('skills')) { $skillsInput = $request->skills; if (is_array($skillsInput)) { $skillsArray = $skillsInput; } elseif (is_string($skillsInput)) { $decoded = json_decode($skillsInput, true); $skillsArray = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : array_filter(array_map('trim', explode(',', $skillsInput))); } } $inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN); $apiToken = Str::random(80); $result = DB::transaction(function () use ($request, $email, $skillsArray, $apiToken, $inCountry) { // Initialize fields $name = $request->name; $nationality = $request->nationality ?? 'Not Specified'; $age = $request->age ?? '18-25'; $visaStatus = $inCountry ? $request->visa_status : null; $passportDataInput = $request->input('passport'); $visaDataInput = $request->input('visa'); // Process passport if provided if ($passportDataInput) { $givenNames = trim($passportDataInput['given_names'] ?? ''); $surname = trim($passportDataInput['surname'] ?? ''); $fullName = trim($givenNames . ' ' . $surname); if (empty($fullName)) { $fullName = $passportDataInput['name'] ?? null; } if ($fullName) { $name = $fullName; } if (!empty($passportDataInput['nationality'])) { $codeUpper = strtoupper($passportDataInput['nationality']); $matchedNationality = \App\Models\Nationality::where('iso3', $codeUpper) ->orWhere('code', $codeUpper) ->first(); $nationality = $matchedNationality ? $matchedNationality->name : $passportDataInput['nationality']; } if (!empty($passportDataInput['date_of_birth'])) { try { $dob = new \DateTime($this->normaliseDateForController($passportDataInput['date_of_birth'])); $ageVal = (new \DateTime())->diff($dob)->y; if ($ageVal >= 18 && $ageVal <= 25) { $age = '18-25'; } elseif ($ageVal >= 26 && $ageVal <= 35) { $age = '26-35'; } elseif ($ageVal >= 36 && $ageVal <= 45) { $age = '36-45'; } elseif ($ageVal >= 46 && $ageVal <= 55) { $age = '46-55'; } elseif ($ageVal >= 56 && $ageVal <= 60) { $age = '56-60'; } else { $age = $ageVal < 18 ? '18-25' : '56-60'; } } catch (\Exception $dateEx) { logger()->error('Passport DOB parse error in register: ' . $dateEx->getMessage()); } } } // Process visa if provided if ($visaDataInput) { if (!empty($visaDataInput['profession'])) { if (empty($visaStatus)) { $visaStatus = $visaDataInput['profession']; } } } $worker = Worker::create([ 'name' => $name, 'email' => $email, 'phone' => $request->phone, 'language' => $request->language ?? 'HI', 'password' => Hash::make($request->password), 'nationality' => $nationality, 'gender' => $request->gender, 'live_in_out' => $request->live_in_out, 'preferred_location' => $request->preferred_location, 'main_profession' => $request->main_profession, 'age' => $age, 'salary' => $request->salary, 'availability' => 'Immediate', 'experience' => $request->experience ?? 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Hardworking and reliable General Helper available for immediate hire.', 'verified' => ($passportDataInput || $visaDataInput) ? true : false, 'status' => 'active', 'api_token' => $apiToken, 'in_country' => $inCountry, 'visa_status' => $visaStatus, 'preferred_job_type' => $request->preferred_job_type, 'fcm_token' => $request->fcm_token, ]); if (!empty($skillsArray)) { $validSkillIds = DB::table('skills')->whereIn('id', $skillsArray)->pluck('id')->toArray(); if (!empty($validSkillIds)) { $worker->skills()->sync($validSkillIds); } } // Create passport document if provided if ($passportDataInput) { $passportNum = $passportDataInput['passport_number'] ?? ('P' . rand(1000000, 9999999)); $expiryDate = $passportDataInput['date_of_expiry'] ?? null; $issueDate = $passportDataInput['date_of_issue'] ?? null; $expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(8)->toDateString(); $issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(3)->toDateString(); $worker->documents()->create([ 'type' => 'passport', 'number' => $passportNum, 'issue_date' => $issueDateFormatted, 'expiry_date' => $expiryDateFormatted, 'ocr_accuracy' => isset($passportDataInput['ocr_accuracy']) ? (float)$passportDataInput['ocr_accuracy'] : 99.0, 'file_path' => null, 'ocr_data' => $passportDataInput, ]); } // Create visa document if provided if ($visaDataInput) { $cleanedVisaOcr = $this->cleanVisaData($visaDataInput); $visaNum = $cleanedVisaOcr['entry_permit_no'] ?: ('V' . rand(1000000, 9999999)); $expiryDate = $cleanedVisaOcr['valid_until'] ?? null; $issueDate = $cleanedVisaOcr['issue_date'] ?? null; $expiryDateFormatted = $expiryDate ? $this->normaliseDateForController($expiryDate) : now()->addYears(2)->toDateString(); $issueDateFormatted = $issueDate ? $this->normaliseDateForController($issueDate) : now()->subYears(2)->toDateString(); $worker->documents()->create([ 'type' => 'visa', 'number' => $visaNum, 'issue_date' => $issueDateFormatted, 'expiry_date' => $expiryDateFormatted, 'ocr_accuracy' => isset($visaDataInput['ocr_accuracy']) ? (float)$visaDataInput['ocr_accuracy'] : 99.0, 'file_path' => null, 'ocr_data' => $cleanedVisaOcr, ]); } return $worker; }); $result->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']); return response()->json([ 'success' => true, 'message' => 'Worker registered successfully.', 'data' => ['worker' => $result, 'token' => $apiToken] ], 201); } catch (\Exception $e) { logger()->error('Worker Registration Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Registration failed.', 'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * Authenticate a worker and return a secure Bearer token. */public function login(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required_without:email|nullable|string', 'email' => 'required_without:phone|nullable|email', 'password' => 'required|string', 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $worker = $request->phone ? Worker::where('phone', $request->phone)->first() : Worker::where('email', $request->email)->first(); if (!$worker || !Hash::check($request->password, $worker->password)) { return response()->json([ 'success' => false, 'message' => 'Invalid credentials.' ], 401); } $apiToken = Str::random(80); $updateData = ['api_token' => $apiToken]; if ($request->has('fcm_token')) { $updateData['fcm_token'] = $request->fcm_token; } $worker->update($updateData); if ($request->filled('fcm_token')) { \App\Services\FCMService::sendPushNotification( $request->fcm_token, 'Successful Login', 'Worker logged in successfully.' ); } return response()->json([ 'success' => true, 'message' => 'Worker logged in successfully.', 'data' => [ 'worker' => $worker->load(['skills', 'documents', 'reviews.employer.employerProfile', 'employerReviews.employer.employerProfile']), 'token' => $apiToken ] ], 200); } catch (\Exception $e) { logger()->error('Worker Login Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred during login. Please try again.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } /** * Get list of nationalities translated to requested language. * Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta). */ public function nationalities(Request $request) { $lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en'); if (str_contains($lang, ',')) { $lang = explode(',', $lang)[0]; } if (str_contains($lang, '-')) { $lang = explode('-', $lang)[0]; } $langMap = [ 'hindi' => 'hi', 'sawahi' => 'sw', 'swahili' => 'sw', 'taglo' => 'tl', 'tagalog' => 'tl', 'tamil' => 'ta', ]; if (isset($langMap[$lang])) { $lang = $langMap[$lang]; } if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) { $lang = 'en'; } $rawNationalities = \App\Models\Nationality::all(); $callingCodes = [ 'AF' => '+93', 'AL' => '+355', 'DZ' => '+213', 'AD' => '+376', 'AO' => '+244', 'AG' => '+1-268', 'AR' => '+54', 'AM' => '+374', 'AU' => '+61', 'AT' => '+43', 'AZ' => '+994', 'BS' => '+1-242', 'BH' => '+973', 'BD' => '+880', 'BB' => '+1-246', 'BY' => '+375', 'BE' => '+32', 'BZ' => '+501', 'BJ' => '+229', 'BT' => '+975', 'BO' => '+591', 'BA' => '+387', 'BW' => '+267', 'BR' => '+55', 'BN' => '+673', 'BG' => '+359', 'BF' => '+226', 'BI' => '+257', 'KH' => '+855', 'CM' => '+237', 'CA' => '+1', 'CV' => '+238', 'CF' => '+236', 'TD' => '+235', 'CL' => '+56', 'CN' => '+86', 'CO' => '+57', 'KM' => '+269', 'CG' => '+242', 'CD' => '+243', 'CR' => '+506', 'HR' => '+385', 'CU' => '+53', 'CY' => '+357', 'CZ' => '+420', 'DK' => '+45', 'DJ' => '+253', 'DM' => '+1-767', 'DO' => '+1-809', 'TL' => '+670', 'EC' => '+593', 'EG' => '+20', 'SV' => '+503', 'GQ' => '+240', 'ER' => '+291', 'EE' => '+372', 'ET' => '+251', 'FJ' => '+679', 'FI' => '+358', 'FR' => '+33', 'GA' => '+241', 'GM' => '+220', 'GE' => '+995', 'DE' => '+49', 'GH' => '+233', 'GR' => '+30', 'GD' => '+1-473', 'GT' => '+502', 'GN' => '+224', 'GW' => '+245', 'GY' => '+592', 'HT' => '+509', 'HN' => '+504', 'HU' => '+36', 'IS' => '+354', 'IN' => '+91', 'ID' => '+62', 'IR' => '+98', 'IQ' => '+964', 'IE' => '+353', 'IL' => '+972', 'IT' => '+39', 'JM' => '+1-876', 'JP' => '+81', 'JO' => '+962', 'KZ' => '+7', 'KE' => '+254', 'KI' => '+686', 'KP' => '+850', 'KR' => '+82', 'KW' => '+965', 'KG' => '+996', 'LA' => '+856', 'LV' => '+371', 'LB' => '+961', 'LS' => '+266', 'LR' => '+231', 'LY' => '+218', 'LI' => '+423', 'LT' => '+370', 'LU' => '+352', 'MK' => '+389', 'MG' => '+261', 'MW' => '+265', 'MY' => '+60', 'MV' => '+960', 'ML' => '+223', 'MT' => '+356', 'MH' => '+692', 'MR' => '+222', 'MU' => '+230', 'MX' => '+52', 'FM' => '+691', 'MD' => '+373', 'MC' => '+377', 'MN' => '+976', 'ME' => '+382', 'MA' => '+212', 'MZ' => '+258', 'MM' => '+95', 'NA' => '+264', 'NR' => '+674', 'NP' => '+977', 'NL' => '+31', 'NZ' => '+64', 'NI' => '+505', 'NE' => '+227', 'NG' => '+234', 'NO' => '+47', 'OM' => '+968', 'PK' => '+92', 'PW' => '+680', 'PA' => '+507', 'PG' => '+675', 'PY' => '+595', 'PE' => '+51', 'PH' => '+63', 'PL' => '+48', 'PT' => '+351', 'QA' => '+974', 'RO' => '+40', 'RU' => '+7', 'RW' => '+250', 'KN' => '+1-869', 'LC' => '+1-758', 'VC' => '+1-784', 'WS' => '+685', 'SM' => '+378', 'ST' => '+239', 'SA' => '+966', 'SN' => '+221', 'RS' => '+381', 'SC' => '+248', 'SL' => '+232', 'SG' => '+65', 'SK' => '+421', 'SI' => '+386', 'SB' => '+677', 'SO' => '+252', 'ZA' => '+27', 'SS' => '+211', 'ES' => '+34', 'LK' => '+94', 'SD' => '+249', 'SR' => '+597', 'SZ' => '+268', 'SE' => '+46', 'CH' => '+41', 'SY' => '+963', 'TJ' => '+992', 'TZ' => '+255', 'TH' => '+66', 'TG' => '+228', 'TO' => '+676', 'TT' => '+1-868', 'TN' => '+216', 'TR' => '+90', 'TM' => '+993', 'TV' => '+688', 'UG' => '+256', 'UA' => '+380', 'AE' => '+971', 'GB' => '+44', 'US' => '+1', 'UY' => '+598', 'UZ' => '+998', 'VU' => '+678', 'VE' => '+58', 'VN' => '+84', 'YE' => '+967', 'ZM' => '+260', 'ZW' => '+263', 'AI' => '+1-264', 'VG' => '+1-284', 'BM' => '+1-441', 'KY' => '+1-345', 'CK' => '+682', 'CY-W' => '+44', 'CY-M' => '+44', 'FO' => '+298', 'GI' => '+350', 'GL' => '+299', 'GU' => '+1-671', 'HK' => '+852', 'XK' => '+383', 'MO' => '+853', 'MQ' => '+596', 'MS' => '+1-664', 'NU' => '+683', 'GB-NIR' => '+44', 'PS' => '+970', 'PN' => '+64', 'PR' => '+1-787', 'GB-SCT' => '+44', 'SH' => '+290', 'ST-L' => '', 'TA-T' => '+290', 'TC' => '+1-649', 'VA' => '+379', 'WF' => '+681', 'GB-WLS' => '+44', ]; $list = []; foreach ($rawNationalities as $item) { $list[] = [ 'code' => $item->code, 'country_code' => $item->code, 'phone_code' => $callingCodes[$item->code] ?? '', 'dial_code' => $callingCodes[$item->code] ?? '', 'calling_code' => $callingCodes[$item->code] ?? '', 'name' => $item->{$lang} ?? $item->name, ]; } $search = strtolower($request->input('search') ?? $request->input('q') ?? ''); if ($search !== '') { $list = array_filter($list, function ($item) use ($search) { return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false; }); $list = array_values($list); } // Apply Pagination $page = (int) $request->input('page', 1); $perPage = (int) $request->input('per_page', 500); if ($perPage === 15) { $perPage = 500; } $total = count($list); $offset = ($page - 1) * $perPage; $paginatedList = array_slice($list, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'nationalities' => $paginatedList, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int) ceil($total / $perPage)), ] ], ], 200); } /** * Get list of preferred locations translated to requested language. * Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta). */ public function preferredLocations(Request $request) { $lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en'); if (str_contains($lang, ',')) { $lang = explode(',', $lang)[0]; } if (str_contains($lang, '-')) { $lang = explode('-', $lang)[0]; } $langMap = [ 'hindi' => 'hi', 'sawahi' => 'sw', 'swahili' => 'sw', 'taglo' => 'tl', 'tagalog' => 'tl', 'tamil' => 'ta', ]; if (isset($langMap[$lang])) { $lang = $langMap[$lang]; } if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) { $lang = 'en'; } $rawLocations = [ ['code' => 'dubai', 'name' => 'Dubai', 'hi' => 'दुबई', 'sw' => 'Dubai', 'tl' => 'Dubai', 'ta' => 'துபாய்'], ['code' => 'abu dhabi', 'name' => 'Abu Dhabi', 'hi' => 'अबू धाबी', 'sw' => 'Abu Dhabi', 'tl' => 'Abu Dhabi', 'ta' => 'அபுதாபி'], ['code' => 'oman', 'name' => 'Oman', 'hi' => 'ओमान', 'sw' => 'Omani', 'tl' => 'Oman', 'ta' => 'ஓமன்'] ]; $list = []; foreach ($rawLocations as $item) { $list[] = [ 'code' => $item['code'], 'name' => $item[$lang] ?? $item['name'], ]; } $search = strtolower($request->input('search') ?? $request->input('q') ?? ''); if ($search !== '') { $list = array_filter($list, function ($item) use ($search) { return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false; }); $list = array_values($list); } // Apply Pagination $page = (int) $request->input('page', 1); $perPage = (int) $request->input('per_page', 15); $total = count($list); $offset = ($page - 1) * $perPage; $paginatedList = array_slice($list, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'preferred_locations' => $paginatedList, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int) ceil($total / $perPage)), ] ], ], 200); } /** * Get list of areas for a specific preferred location translated to requested language. * Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta). */ public function preferredLocationAreas(Request $request, $location = null) { // Get location from path or query parameter $loc = strtolower(trim($location ?? $request->input('location') ?? $request->input('city') ?? '')); // Handle URL-friendly formats like abu-dhabi, abu_dhabi, abu dhabi $loc = str_replace(['_', '-'], ' ', $loc); if (empty($loc)) { return response()->json([ 'success' => false, 'message' => 'Please provide a valid location (e.g., dubai, abu dhabi, oman).' ], 400); } $lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en'); if (str_contains($lang, ',')) { $lang = explode(',', $lang)[0]; } if (str_contains($lang, '-')) { $lang = explode('-', $lang)[0]; } $langMap = [ 'hindi' => 'hi', 'sawahi' => 'sw', 'swahili' => 'sw', 'taglo' => 'tl', 'tagalog' => 'tl', 'tamil' => 'ta', ]; if (isset($langMap[$lang])) { $lang = $langMap[$lang]; } if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) { $lang = 'en'; } $areasMapping = [ 'dubai' => [ ['code' => 'dubai', 'name' => 'Dubai', 'hi' => 'दुबई', 'sw' => 'Dubai', 'tl' => 'Dubai', 'ta' => 'துபாய்'], ['code' => 'marina', 'name' => 'Dubai Marina', 'hi' => 'दुबई मरीना', 'sw' => 'Dubai Marina', 'tl' => 'Dubai Marina', 'ta' => 'துபாய் மெரினா'], ['code' => 'barsha', 'name' => 'Al Barsha', 'hi' => 'अल बरशा', 'sw' => 'Al Barsha', 'tl' => 'Al Barsha', 'ta' => 'அல் பர்ஷா'], ['code' => 'nahda', 'name' => 'Al Nahda', 'hi' => 'अल नहदा', 'sw' => 'Al Nahda', 'tl' => 'Al Nahda', 'ta' => 'அல் நஹ்தா'], ['code' => 'jumeirah', 'name' => 'Jumeirah', 'hi' => 'जुमेराह', 'sw' => 'Jumeirah', 'tl' => 'Jumeirah', 'ta' => 'ஜுமேரா'], ['code' => 'deira', 'name' => 'Deira', 'hi' => 'देइरा', 'sw' => 'Deira', 'tl' => 'Deira', 'ta' => 'தேரா'], ['code' => 'downtown', 'name' => 'Downtown Dubai', 'hi' => 'डाउनटाउन दुबई', 'sw' => 'Downtown Dubai', 'tl' => 'Downtown Dubai', 'ta' => 'டவுன்டவுன் துபாய்'], ['code' => 'silicon', 'name' => 'Silicon Oasis', 'hi' => 'सिलिकॉन ओएसिस', 'sw' => 'Silicon Oasis', 'tl' => 'Silicon Oasis', 'ta' => 'சிலிக்கான் ஓசிஸ்'], ['code' => 'sports', 'name' => 'Sports City', 'hi' => 'स्पोर्ट्स सिटी', 'sw' => 'Sports City', 'tl' => 'Sports City', 'ta' => 'ஸ்போர்ட்ஸ் சிட்டி'], ['code' => 'motor', 'name' => 'Motor City', 'hi' => 'मोटर सिटी', 'sw' => 'Motor City', 'tl' => 'Motor City', 'ta' => 'மோட்டார் சிட்டி'], ['code' => 'jlt', 'name' => 'JLT', 'hi' => 'JLT', 'sw' => 'JLT', 'tl' => 'JLT', 'ta' => 'ஜே.எல்.டி'], ['code' => 'jbr', 'name' => 'JBR', 'hi' => 'JBR', 'sw' => 'JBR', 'tl' => 'JBR', 'ta' => 'ஜே.பி.ஆர்'], ['code' => 'meydan', 'name' => 'Meydan', 'hi' => 'मेदान', 'sw' => 'Meydan', 'tl' => 'Meydan', 'ta' => 'மெய்தான்'], ['code' => 'ranches', 'name' => 'Arabian Ranches', 'hi' => 'अरेबियन रैंचेस', 'sw' => 'Arabian Ranches', 'tl' => 'Arabian Ranches', 'ta' => 'அரேபியன் ரான்சஸ்'], ['code' => 'bay', 'name' => 'Business Bay', 'hi' => 'बिजनेस बे', 'sw' => 'Business Bay', 'tl' => 'Business Bay', 'ta' => 'பிசினஸ் பே'], ['code' => 'mirdif', 'name' => 'Mirdif', 'hi' => 'मिर्डिफ', 'sw' => 'Mirdif', 'tl' => 'Mirdif', 'ta' => 'மிர்திஃப்'], ['code' => 'quoz', 'name' => 'Al Quoz', 'hi' => 'अल क्वोज़', 'sw' => 'Al Quoz', 'tl' => 'Al Quoz', 'ta' => 'அல் கூஸ்'], ], 'abu dhabi' => [ ['code' => 'abu dhabi', 'name' => 'Abu Dhabi City', 'hi' => 'अबू धाबी शहर', 'sw' => 'Abu Dhabi City', 'tl' => 'Abu Dhabi City', 'ta' => 'அபுதாபி நகரம்'], ['code' => 'yas', 'name' => 'Yas Island', 'hi' => 'यास द्वीप', 'sw' => 'Yas Island', 'tl' => 'Yas Island', 'ta' => 'யாஸ் தீவு'], ['code' => 'khalifa', 'name' => 'Khalifa City', 'hi' => 'खलीफा शहर', 'sw' => 'Khalifa City', 'tl' => 'Khalifa City', 'ta' => 'கலீஃபா நகரம்'], ['code' => 'reem', 'name' => 'Reem Island', 'hi' => 'रीम द्वीप', 'sw' => 'Reem Island', 'tl' => 'Reem Island', 'ta' => 'ரீம் தீவு'], ['code' => 'saadiyat', 'name' => 'Saadiyat Island', 'hi' => 'सादियात द्वीप', 'sw' => 'Saadiyat Island', 'tl' => 'Saadiyat Island', 'ta' => 'சாதியாத் தீவு'], ['code' => 'raha', 'name' => 'Al Raha', 'hi' => 'अल राहा', 'sw' => 'Al Raha', 'tl' => 'Al Raha', 'ta' => 'அல் ராஹா'], ['code' => 'mussafah', 'name' => 'Mussafah', 'hi' => 'मुसफ्फह', 'sw' => 'Mussafah', 'tl' => 'Mussafah', 'ta' => 'முஸாஃபா'], ['code' => 'zahiyah', 'name' => 'Al Zahiyah', 'hi' => 'अल ज़ाहिया', 'sw' => 'Al Zahiyah', 'tl' => 'Al Zahiyah', 'ta' => 'அல் ஜாஹியா'], ['code' => 'karamah', 'name' => 'Al Karamah', 'hi' => 'अल करमा', 'sw' => 'Al Karamah', 'tl' => 'Al Karamah', 'ta' => 'அல் கராமா'], ], 'oman' => [ ['code' => 'oman', 'name' => 'Oman', 'hi' => 'ओमान', 'sw' => 'Omani', 'tl' => 'Oman', 'ta' => 'ஓமன்'], ['code' => 'muscat', 'name' => 'Muscat', 'hi' => 'मस्कट', 'sw' => 'Muscat', 'tl' => 'Muscat', 'ta' => 'மस्कट'], ['code' => 'salalah', 'name' => 'Salalah', 'hi' => 'सलालाह', 'sw' => 'Salalah', 'tl' => 'Salalah', 'ta' => 'சலாலா'], ['code' => 'sohar', 'name' => 'Sohar', 'hi' => 'सोहर', 'sw' => 'Sohar', 'tl' => 'Sohar', 'ta' => 'சோஹார்'], ['code' => 'nizwa', 'name' => 'Nizwa', 'hi' => 'निज़वा', 'sw' => 'Nizwa', 'tl' => 'Nizwa', 'ta' => 'நிஸ்வா'], ['code' => 'sur', 'name' => 'Sur', 'hi' => 'सुर', 'sw' => 'Sur', 'tl' => 'Sur', 'ta' => 'சூர்'], ['code' => 'ibri', 'name' => 'Ibri', 'hi' => 'इबरी', 'sw' => 'Ibri', 'tl' => 'Ibri', 'ta' => 'இப்ரி'], ['code' => 'rustaq', 'name' => 'Rustaq', 'hi' => 'रुस्तक़', 'sw' => 'Rustaq', 'tl' => 'Rustaq', 'ta' => 'रुस्तक़'], ] ]; if (!isset($areasMapping[$loc])) { return response()->json([ 'success' => false, 'message' => 'Areas for the specified location were not found. Supported locations: dubai, abu dhabi, oman.' ], 404); } $rawAreas = $areasMapping[$loc]; $list = []; foreach ($rawAreas as $item) { $list[] = [ 'code' => $item['code'], 'name' => $item[$lang] ?? $item['name'], ]; } $search = strtolower($request->input('search') ?? $request->input('q') ?? ''); if ($search !== '') { $list = array_filter($list, function ($item) use ($search) { return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false; }); $list = array_values($list); } // Apply Pagination $page = (int) $request->input('page', 1); $perPage = (int) $request->input('per_page', 15); $total = count($list); $offset = ($page - 1) * $perPage; $paginatedList = array_slice($list, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'location' => $loc, 'areas' => $paginatedList, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int) ceil($total / $perPage)), ] ], ], 200); } /** * Get list of languages translated to requested language. * Supports en, hindi (hi), sawahi (sw), taglo (tl), tamil (ta). */ public function languages(Request $request) { $lang = strtolower($request->input('lang') ?? $request->header('Accept-Language') ?? 'en'); if (str_contains($lang, ',')) { $lang = explode(',', $lang)[0]; } if (str_contains($lang, '-')) { $lang = explode('-', $lang)[0]; } $langMap = [ 'hindi' => 'hi', 'sawahi' => 'sw', 'swahili' => 'sw', 'taglo' => 'tl', 'tagalog' => 'tl', 'tamil' => 'ta', ]; if (isset($langMap[$lang])) { $lang = $langMap[$lang]; } if (!in_array($lang, ['en', 'hi', 'sw', 'tl', 'ta'])) { $lang = 'en'; } $rawLanguages = [ ['code' => 'en', 'name' => 'English', 'hi' => 'अंग्रेज़ी', 'sw' => 'Kiingereza', 'tl' => 'Ingles', 'ta' => 'ஆங்கிலம்'], ['code' => 'ar', 'name' => 'Arabic', 'hi' => 'अरबी', 'sw' => 'Kiarabu', 'tl' => 'Arabe', 'ta' => 'அரபு'], ['code' => 'hi', 'name' => 'Hindi', 'hi' => 'हिन्दी', 'sw' => 'Kihindi', 'tl' => 'Hindi', 'ta' => 'இந்தி'], ['code' => 'tl', 'name' => 'Tagalog', 'hi' => 'तागालोग', 'sw' => 'Kitagalog', 'tl' => 'Tagalog', 'ta' => 'தகலாக்'], ['code' => 'sw', 'name' => 'Swahili', 'hi' => 'स्वाहिली', 'sw' => 'Kiswahili', 'tl' => 'Swahili', 'ta' => 'சுவாஹிலி'], ['code' => 'ta', 'name' => 'Tamil', 'hi' => 'तमिल', 'sw' => 'Kitamil', 'tl' => 'Tamil', 'ta' => 'தமிழ்'], ['code' => 'ur', 'name' => 'Urdu', 'hi' => 'उर्दू', 'sw' => 'Kiurdu', 'tl' => 'Urdu', 'ta' => 'உருது'], ['code' => 'bn', 'name' => 'Bengali', 'hi' => 'बंगाली', 'sw' => 'Kibengali', 'tl' => 'Bengali', 'ta' => 'பெங்காலி'], ['code' => 'ml', 'name' => 'Malayalam', 'hi' => 'मलयालम', 'sw' => 'Kimalayalam', 'tl' => 'Malayalam', 'ta' => 'மலையாளம்'], ['code' => 'ne', 'name' => 'Nepali', 'hi' => 'नेपाली', 'sw' => 'Kinepali', 'tl' => 'Nepali', 'ta' => 'நேபாளி'], ['code' => 'si', 'name' => 'Sinhala', 'hi' => 'सिंहली', 'sw' => 'Kisinhala', 'tl' => 'Sinhala', 'ta' => 'சிங்களம்'], ['code' => 'te', 'name' => 'Telugu', 'hi' => 'तेलुगु', 'sw' => 'Kitelugu', 'tl' => 'Telugu', 'ta' => 'தெலுங்கு'], ['code' => 'pa', 'name' => 'Punjabi', 'hi' => 'पंजाबी', 'sw' => 'Kipunjabi', 'tl' => 'Punjabi', 'ta' => 'பஞ்சாபி'], ['code' => 'ps', 'name' => 'Pashto', 'hi' => 'पश्तो', 'sw' => 'Kipashto', 'tl' => 'Pashto', 'ta' => 'பஷ்தூ'], ['code' => 'kn', 'name' => 'Kannada', 'hi' => 'कन्नड़', 'sw' => 'Kikannada', 'tl' => 'Kannada', 'ta' => 'கன்னடம்'], ['code' => 'mr', 'name' => 'Marathi', 'hi' => 'मराठी', 'sw' => 'Kimarathi', 'tl' => 'Marathi', 'ta' => 'மராத்தி'], ['code' => 'gu', 'name' => 'Gujarati', 'hi' => 'गुजराती', 'sw' => 'Kigujarati', 'tl' => 'Gujarati', 'ta' => 'குஜராत्ति'], ['code' => 'fr', 'name' => 'French', 'hi' => 'फ़्रांसीसी', 'sw' => 'Kifaransa', 'tl' => 'Pranses', 'ta' => 'பிரெஞ்சு'], ['code' => 'es', 'name' => 'Spanish', 'hi' => 'स्पैनिश', 'sw' => 'Kihispania', 'tl' => 'Espanyol', 'ta' => 'ஸ்பானிஷ்'], ['code' => 'zh', 'name' => 'Chinese', 'hi' => 'चीनी', 'sw' => 'Kichina', 'tl' => 'Intsik', 'ta' => 'சீனம்'], ['code' => 'ru', 'name' => 'Russian', 'hi' => 'रूसी', 'sw' => 'Kirusi', 'tl' => 'Ruso', 'ta' => 'ரஷ்யன்'], ['code' => 'id', 'name' => 'Indonesian', 'hi' => 'इंडोनेशियाई', 'sw' => 'Kiindonesia', 'tl' => 'Indonesian', 'ta' => 'இந்தோனேசியன்'], ]; $list = []; foreach ($rawLanguages as $item) { $list[] = [ 'code' => $item['code'], 'name' => $item[$lang] ?? $item['name'], ]; } $search = strtolower($request->input('search') ?? $request->input('q') ?? ''); if ($search !== '') { $list = array_filter($list, function ($item) use ($search) { return stripos($item['name'], $search) !== false || stripos($item['code'], $search) !== false; }); $list = array_values($list); } // Apply Pagination $page = (int) $request->input('page', 1); $perPage = (int) $request->input('per_page', 500); if ($perPage === 15) { $perPage = 500; } $total = count($list); $offset = ($page - 1) * $perPage; $paginatedList = array_slice($list, $offset, $perPage); return response()->json([ 'success' => true, 'data' => [ 'languages' => $paginatedList, 'pagination' => [ 'total' => $total, 'per_page' => $perPage, 'current_page' => $page, 'last_page' => max(1, (int) ceil($total / $perPage)), ] ], ], 200); } /** * E.g. "14/05/1990" -> "1990-05-14" */ private function normaliseDateForController(?string $raw): ?string { if (empty($raw)) { return null; } $raw = trim($raw); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) { return $raw; } try { if (preg_match('/^(\d{1,2})[\/\- ]([A-Za-z0-9]+)[\/\- ](\d{4})$/', $raw, $m)) { $day = str_pad($m[1], 2, '0', STR_PAD_LEFT); $month = $m[2]; $year = $m[3]; if (is_numeric($month)) { $month = str_pad($month, 2, '0', STR_PAD_LEFT); return "{$year}-{$month}-{$day}"; } $ts = strtotime("{$day} {$month} {$year}"); return $ts ? date('Y-m-d', $ts) : $raw; } $dt = new \DateTime($raw); return $dt->format('Y-m-d'); } catch (\Exception $e) { return $raw; } } /** * Standardize and clean Visa OCR data structure to exactly the 15 required fields. */ private function cleanVisaData(array $visaDataInput): array { $rawVisaType = $visaDataInput['visa_type'] ?? null; if ($rawVisaType) { $normalized = strtolower(trim($rawVisaType)); if (str_contains($normalized, 'residence')) { $rawVisaType = 'Residence Visa'; } elseif (str_contains($normalized, 'employment')) { $rawVisaType = 'Employment Visa'; } else { $rawVisaType = 'Tourist Visa'; } } else { $rawVisaType = 'Tourist Visa'; } $rawEntryPermitNo = $visaDataInput['entry_permit_no'] ?? $visaDataInput['file_number'] ?? $visaDataInput['id_number'] ?? $visaDataInput['number'] ?? null; $rawIssueDate = $visaDataInput['issue_date'] ?? null; $rawValidUntil = $visaDataInput['valid_until'] ?? $visaDataInput['expiry_date'] ?? null; $rawUidNo = $visaDataInput['uid_no'] ?? $visaDataInput['id_number'] ?? null; $rawFullName = $visaDataInput['full_name'] ?? $visaDataInput['name'] ?? null; $rawNationality = $visaDataInput['nationality'] ?? null; $rawPlaceOfBirth = $visaDataInput['place_of_birth'] ?? null; $rawDateOfBirth = $visaDataInput['date_of_birth'] ?? $visaDataInput['dob'] ?? null; $rawPassportNo = $visaDataInput['passport_no'] ?? $visaDataInput['passport_number'] ?? null; $rawProfession = $visaDataInput['profession'] ?? null; $rawSponsorName = $visaDataInput['sponsor_name'] ?? null; // Clean sponsor $sponsorData = $visaDataInput['sponsor'] ?? []; if (is_string($sponsorData)) { $sponsorNameFromObj = $sponsorData; $sponsorAddress = ''; $sponsorPhone = ''; } else { $sponsorNameFromObj = $sponsorData['name'] ?? $sponsorData['sponsor_name'] ?? $sponsorData['full_name'] ?? ''; $sponsorAddress = $sponsorData['address'] ?? ''; $sponsorPhone = $sponsorData['phone'] ?? $sponsorData['mobile'] ?? $sponsorData['mobile_number'] ?? $sponsorData['phone_number'] ?? ''; } if (empty($rawSponsorName)) { $rawSponsorName = $sponsorNameFromObj; } return [ 'document_type' => 'uae_visa', 'country' => 'United Arab Emirates', 'visa_type' => $rawVisaType, 'entry_permit_no' => $rawEntryPermitNo ?: '', 'issue_date' => $rawIssueDate ?: '', 'valid_until' => $rawValidUntil ?: '', 'uid_no' => $rawUidNo ?: '', 'full_name' => $rawFullName ?: '', 'nationality' => $rawNationality ?: '', 'place_of_birth' => $rawPlaceOfBirth ?: '', 'date_of_birth' => $rawDateOfBirth ?: '', 'passport_no' => $rawPassportNo ?: '', 'profession' => $rawProfession ?: '', 'sponsor_name' => $rawSponsorName ?: '', 'sponsor' => [ 'name' => $sponsorNameFromObj ?: '', 'address' => $sponsorAddress ?: '', 'phone' => $sponsorPhone ?: '', ] ]; } // ------------------------------------------------------------------------- // Forgot / Reset Password (via Mobile Number) // ------------------------------------------------------------------------- /** * POST /api/workers/forgot-password * Send a 6-digit OTP to verify the worker's identity by phone number. * Workers do not use email — phone is the primary identifier. */ public function forgotPassword(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors(), ], 422); } $worker = Worker::where('phone', $request->phone)->first(); if (!$worker) { return response()->json([ 'success' => false, 'message' => 'user not found this mobile number', ], 404); } $otp = (string) mt_rand(100000, 999999); $cacheKey = 'worker_pwd_reset_' . md5($request->phone); Cache::put($cacheKey, [ 'otp' => Hash::make($otp), 'expires_at' => now()->addMinutes(10)->timestamp, ], now()->addMinutes(10)); // Send via email if available, otherwise log for dev if ($worker->email) { try { Mail::to($worker->email)->send( new \App\Mail\ForgotPasswordOtpMail($otp, $worker->name, 'Worker') ); } catch (\Exception $e) { logger()->error('Worker forgot-password mail failed: ' . $e->getMessage()); } } else { // Log OTP for debugging when no email configured logger()->info("Worker password reset OTP for {$request->phone}: {$otp}"); } return response()->json([ 'success' => true, 'message' => 'If an account with that mobile number exists, a reset OTP has been sent.', ]); } /** * POST /api/workers/reset-password * Verify OTP sent to phone and set a new password. */ public function resetPassword(Request $request) { $validator = Validator::make($request->all(), [ 'phone' => 'required|string', 'otp' => 'required|string|size:6', 'password' => 'required|string|min:8|confirmed', 'password_confirmation' => 'required|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors(), ], 422); } $cacheKey = 'worker_pwd_reset_' . md5($request->phone); $cached = Cache::get($cacheKey); if (!$cached || $cached['expires_at'] < now()->timestamp) { Cache::forget($cacheKey); return response()->json([ 'success' => false, 'message' => 'OTP has expired or is invalid. Please request a new one.', ], 422); } if (!Hash::check($request->otp, $cached['otp'])) { return response()->json([ 'success' => false, 'message' => 'Invalid OTP. Please check and try again.', ], 422); } $worker = Worker::where('phone', $request->phone)->first(); if (!$worker) { return response()->json([ 'success' => false, 'message' => 'Account not found.', ], 404); } $worker->update(['password' => Hash::make($request->password)]); Cache::forget($cacheKey); return response()->json([ 'success' => true, 'message' => 'Password reset successfully. You can now log in with your new password.', ]); } }