migrant-web/app/Http/Controllers/Api/WorkerAuthController.php
2026-07-04 21:07:00 +05:30

1431 lines
61 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Worker;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class WorkerAuthController extends Controller
{
/**
* Get system configuration and supported languages for worker onboarding.
* Aligns with Step 2 "Select Language (HI / SW / TL / TA)".
*/
public function config()
{
return response()->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' => 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|integer',
'experience' => 'nullable|string|max:100',
'in_country' => 'nullable',
'visa_status' => 'nullable|string|in:Tourist Visa,Employment Visa,Residence Visa',
'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 ?? 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']));
$age = (new \DateTime())->diff($dob)->y;
} 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.',
]);
}
}