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, ]; }); 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', 'email' => 'required_without:phone|nullable|email|max:255', ], [ 'phone.required_without' => 'Please provide a mobile number or email address.', 'email.required_without' => 'Please provide a mobile number or email address.', ]); 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', 'skills' => 'nullable|array', 'skills.*' => 'integer|exists:skills,id', ]); 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, '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.', 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id), 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, ]); // 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(['category', 'skills']); 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 backward compatibility and multipart file registration. */ public function register(Request $request) { $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', 'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', 'skills' => 'nullable', 'visa_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240', 'language' => 'nullable|string', 'nationality' => 'nullable|string|max:100', 'age' => 'nullable|integer', 'experience' => 'nullable|string|max:100', ]); 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))); } } $destinationPath = public_path('uploads/documents'); if (!file_exists($destinationPath)) { mkdir($destinationPath, 0755, true); } $passportPath = null; $visaPath = null; if ($request->hasFile('passport_file')) { $file = $request->file('passport_file'); $fileName = time() . '_passport_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName()); $file->move($destinationPath, $fileName); $passportPath = 'uploads/documents/' . $fileName; } if ($request->hasFile('visa_file')) { $file = $request->file('visa_file'); $fileName = time() . '_visa_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName()); $file->move($destinationPath, $fileName); $visaPath = 'uploads/documents/' . $fileName; } $apiToken = Str::random(80); $result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken) { $worker = Worker::create([ 'name' => $request->name, 'email' => $email, 'phone' => $request->phone, 'language' => $request->language ?? 'HI', 'password' => Hash::make($request->password), 'nationality' => $request->nationality ?? 'Not Specified', 'age' => $request->age ?? 25, 'salary' => $request->salary, 'availability'=> 'Immediate', 'experience' => $request->experience ?? 'Not Specified', 'religion' => 'Not Specified', 'bio' => 'Hardworking and reliable General Helper available for immediate hire.', 'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? (\App\Models\WorkerCategory::firstOrCreate(['name' => 'General Helper'])->id), 'verified' => false, 'status' => 'active', 'api_token' => $apiToken, ]); if (!empty($skillsArray)) { $validSkillIds = DB::table('skills')->whereIn('id', $skillsArray)->pluck('id')->toArray(); if (!empty($validSkillIds)) { $worker->skills()->sync($validSkillIds); } } $worker->documents()->create([ 'type' => 'passport', 'number' => 'P' . rand(100000, 999999), 'issue_date' => now()->subYears(2)->toDateString(), 'expiry_date' => now()->addYears(8)->toDateString(), 'ocr_accuracy'=> 98.5, 'file_path' => $passportPath, ]); $worker->documents()->create([ 'type' => 'visa', 'number' => 'V' . rand(100000, 999999), 'issue_date' => now()->subYear()->toDateString(), 'expiry_date' => now()->addYears(2)->toDateString(), 'ocr_accuracy'=> $visaPath ? 98.5 : 0.0, 'file_path' => $visaPath, ]); return $worker; }); $result->load(['category', 'skills', 'documents']); return response()->json([ 'success' => true, 'message' => 'Worker registered and authenticated successfully.', 'data' => ['worker' => $result, 'token' => $apiToken] ], 201); } catch (\Exception $e) { logger()->error('Worker Registration Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred during worker registration. Please try again.', 'error' => app()->environment('local') ? $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', ]); 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); $worker->update(['api_token' => $apiToken]); return response()->json([ 'success' => true, 'message' => 'Worker logged in successfully.', 'data' => [ 'worker' => $worker->load(['category', 'skills', 'documents']), '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); } } }