all(), [ 'email' => 'nullable|string', 'mobile' => 'nullable|string', 'password' => 'required|string', 'fcm_token' => 'nullable|string|max:255', ]); $validator->after(function ($validator) use ($request) { if (!$request->filled('email') && !$request->filled('mobile')) { $validator->errors()->add('mobile', 'Either mobile number or email is required.'); } }); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $loginValue = $request->input('mobile') ?? $request->input('email'); $sponsor = Sponsor::where('mobile', $loginValue) ->orWhere('email', $loginValue) ->first(); if ($sponsor) { $user = User::where('email', $sponsor->email) ->where('role', 'employer') ->first(); } else { $user = User::where('email', $loginValue) ->where('role', 'employer') ->first(); } if (!$sponsor && !$user) { return response()->json([ 'success' => false, 'message' => 'Invalid credentials.' ], 401); } $passwordHash = $sponsor ? $sponsor->password : $user->password; if (!Hash::check($request->password, $passwordHash)) { return response()->json([ 'success' => false, 'message' => 'Invalid credentials.' ], 401); } if ($sponsor && $sponsor->status === 'suspended') { return response()->json([ 'success' => false, 'message' => 'Your account has been suspended. Please contact support.' ], 403); } $apiToken = Str::random(80); if ($user) { // Employer account $profile = EmployerProfile::where('user_id', $user->id)->first(); $verification_status = $profile ? $profile->verification_status : 'approved'; if ($verification_status === 'pending') { return response()->json([ 'success' => false, 'message' => 'Your account is pending verification.' ], 403); } if ($verification_status === 'rejected') { return response()->json([ 'success' => false, 'message' => 'Your account verification has been rejected.', 'reason' => $profile->rejection_reason ?? 'Verification rejected.' ], 403); } $userUpdateData = ['api_token' => $apiToken]; if ($request->has('fcm_token')) { $userUpdateData['fcm_token'] = $request->fcm_token; } $user->update($userUpdateData); if ($sponsor) { $sponsorUpdateData = [ 'api_token' => $apiToken, 'last_login_at' => now(), ]; if ($request->has('fcm_token')) { $sponsorUpdateData['fcm_token'] = $request->fcm_token; } $sponsor->update($sponsorUpdateData); } if ($request->filled('fcm_token')) { \App\Services\FCMService::sendPushNotification( $request->fcm_token, 'Successful Login', 'Welcome back to your Migrant employer account.' ); } return response()->json([ 'success' => true, 'message' => 'Login successful.', 'role' => 'employer', 'data' => [ 'employer' => $user->load('employerProfile'), 'token' => $apiToken ] ], 200); } else { // Pure Sponsor account $sponsorUpdateData = [ 'api_token' => $apiToken, 'last_login_at' => now(), ]; if ($request->has('fcm_token')) { $sponsorUpdateData['fcm_token'] = $request->fcm_token; } $sponsor->update($sponsorUpdateData); if ($request->filled('fcm_token')) { \App\Services\FCMService::sendPushNotification( $request->fcm_token, 'Successful Login', 'Welcome back to your Migrant sponsor account.' ); } return response()->json([ 'success' => true, 'message' => 'Login successful.', 'role' => 'sponsor', 'data' => [ 'sponsor' => $sponsor->makeHidden(['password', 'api_token']), 'token' => $apiToken ] ], 200); } } catch (\Exception $e) { logger()->error('Mobile 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); } } /** * Register a new employer via API. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function register(Request $request) { $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users,email', 'phone' => 'required|string|max:50', 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $otp = '111111'; // default development OTP if (app()->environment('production')) { $otp = strval(rand(100000, 999999)); } \Illuminate\Support\Facades\Cache::put('employer_otp_' . $request->email, [ 'code' => $otp, 'expires_at' => now()->addMinutes(10)->timestamp ], 600); // Create inactive User $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make(Str::random(16)), // temporary password 'role' => 'employer', 'subscription_status' => 'none', 'subscription_expires_at' => null, 'fcm_token' => $request->fcm_token, ]); // Create pending Profile EmployerProfile::create([ 'user_id' => $user->id, 'company_name' => $request->name . ' Household', 'phone' => $request->phone, 'country' => 'United Arab Emirates', 'verification_status' => 'pending', 'language' => 'English', 'notifications' => true, ]); // Create unverified Sponsor \App\Models\Sponsor::create([ 'full_name' => $request->name, 'email' => $request->email, 'mobile' => $request->phone, 'password' => $user->password, 'country_code' => 'AE', 'subscription_status' => 'none', 'subscription_plan' => null, 'subscription_start_date' => null, 'subscription_end_date' => null, 'payment_status' => 'unpaid', 'is_verified' => false, 'otp_verified_at' => null, 'status' => 'inactive', 'fcm_token' => $request->fcm_token, ]); if ($request->filled('fcm_token')) { \App\Services\FCMService::sendPushNotification( $request->fcm_token, 'Registration Initiated', 'Your verification code has been sent to your email.' ); } // Try sending email try { \Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerOtpMail( $otp, $request->name . ' Household', $request->name )); } catch (\Exception $mailEx) { logger()->error('API Sponsor Registration Mail Failure: ' . $mailEx->getMessage()); } return response()->json([ 'success' => true, 'message' => 'Verification code sent to email successfully.', 'email' => $request->email ], 201); } catch (\Exception $e) { logger()->error('API Sponsor Registration Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Registration failed. Please try again.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } public function verify(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'otp' => 'required|string|size:6', 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } $cachedData = \Illuminate\Support\Facades\Cache::get('employer_otp_' . $request->email); 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) { \Illuminate\Support\Facades\Cache::forget('employer_otp_' . $request->email); 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); } try { // Update Sponsor verification status $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); $user = User::where('email', $request->email)->where('role', 'employer')->first(); $fcmToken = $request->fcm_token; if ($sponsor) { $sponsorUpdateData = [ 'is_verified' => true, 'otp_verified_at' => now(), ]; if ($fcmToken) { $sponsorUpdateData['fcm_token'] = $fcmToken; } $sponsor->update($sponsorUpdateData); } if ($user && $fcmToken) { $user->update(['fcm_token' => $fcmToken]); } $tokenToSend = $fcmToken ?? ($sponsor ? $sponsor->fcm_token : null) ?? ($user ? $user->fcm_token : null); if ($tokenToSend) { \App\Services\FCMService::sendPushNotification( $tokenToSend, 'Email Verified', 'Proceed to payment selection.' ); } // Clear Cache \Illuminate\Support\Facades\Cache::forget('employer_otp_' . $request->email); return response()->json([ 'success' => true, 'message' => 'Email verified successfully. Proceed to payment selection.', 'email' => $request->email ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Verification failed. Please try again.' ], 500); } } /** * Upload Emirates ID during registration steps (Unauthenticated). * * POST /api/employers/upload-emirates-id */ public function uploadEmiratesId(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|string|email|max:255', 'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240', ], [ 'email.required' => 'The email address is required.', 'emirates_id_file.required' => 'Please upload a clear scan or image of your Emirates ID.', 'emirates_id_file.mimes' => 'The Emirates ID document must be an image (jpg, jpeg, png) or a PDF file.', 'emirates_id_file.max' => 'The document must not exceed 10MB.', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $user = User::where('email', $request->email)->first(); $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); if (!$user) { return response()->json([ 'success' => false, 'message' => 'User account not found.' ], 404); } $file = $request->file('emirates_id_file'); $destinationPath = public_path('uploads/documents'); if (!file_exists($destinationPath)) { mkdir($destinationPath, 0755, true); } $fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName()); $file->move($destinationPath, $fileName); $filePath = 'uploads/documents/' . $fileName; // OCR Simulated extraction $extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9); $extractedExpiry = now()->addYears(rand(2, 4))->toDateString(); // We are not storing this document, just extract data and delete this file if (file_exists(public_path($filePath))) { unlink(public_path($filePath)); } // Update or create EmployerProfile $profile = EmployerProfile::where('user_id', $user->id)->first(); if (!$profile) { $profile = EmployerProfile::create([ 'user_id' => $user->id, 'company_name' => $user->name . ' Household', 'phone' => $sponsor ? $sponsor->mobile : '+971 50 123 4567', 'country' => 'United Arab Emirates', 'verification_status' => 'pending', ]); } $profile->update([ 'emirates_id_front' => null, 'emirates_id_number' => $extractedIdNumber, 'emirates_id_expiry' => $extractedExpiry, ]); // Sync with corresponding sponsor record if found if ($sponsor) { $sponsor->update([ 'emirates_id_file' => null, ]); } return response()->json([ 'success' => true, 'message' => 'Emirates ID uploaded successfully.', 'ocr_extracted_data' => [ 'emirates_id_number' => $extractedIdNumber, 'expiry_date' => $extractedExpiry, ], 'email' => $request->email, ], 200); } catch (\Exception $e) { logger()->error('API Sponsor Registration Emirates ID Upload Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred while uploading the Emirates ID.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } public function payment(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'plan_id' => 'required|string', 'amount_aed' => 'required|numeric', 'paytabs_transaction_id' => 'required|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } try { $user = User::where('email', $request->email)->first(); $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); if (!$user || !$sponsor) { return response()->json([ 'success' => false, 'message' => 'User account not found.' ], 404); } if (!$sponsor->is_verified) { return response()->json([ 'success' => false, 'message' => 'Please verify your email before initiating payment.' ], 403); } \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor) { // Update User subscription $user->update([ 'subscription_status' => 'active', 'subscription_expires_at' => now()->addDays(30), ]); // Update Sponsor status $sponsor->update([ 'subscription_status' => 'active', 'subscription_plan' => $request->plan_id, 'subscription_start_date' => now(), 'subscription_end_date' => now()->addDays(30), 'payment_status' => 'paid', ]); // Insert into subscriptions table \Illuminate\Support\Facades\DB::table('subscriptions')->insert([ 'user_id' => $user->id, 'plan_id' => $request->plan_id, 'amount_aed' => $request->amount_aed, 'starts_at' => now(), 'expires_at' => now()->addDays(30), 'paytabs_transaction_id' => $request->paytabs_transaction_id, 'status' => 'active', 'created_at' => now(), 'updated_at' => now(), ]); }); return response()->json([ 'success' => true, 'message' => 'Subscription payment processed and activated successfully.', 'email' => $request->email ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Payment processing failed. Please try again.' ], 500); } } public function password(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'password' => 'required|string|min:8|confirmed', 'fcm_token' => 'nullable|string|max:255', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'errors' => $validator->errors() ], 422); } try { $user = User::where('email', $request->email)->first(); $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); if (!$user || !$sponsor) { return response()->json([ 'success' => false, 'message' => 'User account not found.' ], 404); } if ($sponsor->payment_status !== 'paid') { return response()->json([ 'success' => false, 'message' => 'Please complete subscription payment first.' ], 403); } // Generate API Bearer token $apiToken = Str::random(80); \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) { $hashedPassword = Hash::make($request->password); $fcmToken = $request->fcm_token; $userUpdateData = [ 'password' => $hashedPassword, 'api_token' => $apiToken, ]; if ($fcmToken) { $userUpdateData['fcm_token'] = $fcmToken; } $user->update($userUpdateData); $sponsorUpdateData = [ 'password' => $hashedPassword, 'status' => 'active', ]; if ($fcmToken) { $sponsorUpdateData['fcm_token'] = $fcmToken; } $sponsor->update($sponsorUpdateData); // Approve profile verification $profile = EmployerProfile::where('user_id', $user->id)->first(); if ($profile) { $profile->update([ 'verification_status' => 'approved' ]); } }); $tokenToSend = $request->fcm_token ?? ($user ? $user->fcm_token : null) ?? ($sponsor ? $sponsor->fcm_token : null); if ($tokenToSend) { \App\Services\FCMService::sendPushNotification( $tokenToSend, 'Welcome to Migrant', 'Your employer registration has been completed successfully.' ); } return response()->json([ 'success' => true, 'message' => 'Password created successfully. Registration finalized.', 'data' => [ 'sponsor' => $user->load('employerProfile'), 'token' => $apiToken ] ]); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to set password. Please try again.' ], 500); } } public function plans() { return response()->json([ 'success' => true, 'data' => [ 'plans' => [ [ 'id' => 'basic', 'name' => 'Basic Search', 'price' => 99.00, 'currency' => 'AED', 'period' => 'month', 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'], 'popular' => false, ], [ 'id' => 'premium', 'name' => 'Premium Employer Pass', 'price' => 199.00, 'currency' => 'AED', 'period' => 'month', 'features' => ['Unlimited shortlisting', 'Direct candidate messaging', 'Priority interview scheduling', 'Dedicated support'], 'popular' => true, ], [ 'id' => 'enterprise', 'name' => 'VIP Concierge', 'price' => 499.00, 'currency' => 'AED', 'period' => 'month', 'features' => ['All Premium features', 'Assigned recruitment manager', 'Background medical verification guarantee', 'Free replacement within 30 days'], 'popular' => false, ] ] ] ]); } public function forgotPassword(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } try { $user = User::where('email', $request->email)->where('role', 'employer')->first(); if (!$user) { return response()->json([ 'success' => false, 'message' => 'No employer account found with this email address.' ], 404); } $otp = '111111'; // default development OTP if (app()->environment('production')) { $otp = strval(rand(100000, 999999)); } \Illuminate\Support\Facades\Cache::put('employer_reset_otp_' . $request->email, [ 'code' => $otp, 'expires_at' => now()->addMinutes(10)->timestamp ], 600); // Send reset OTP email try { \Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerResetOtpMail( $otp, $user->name )); } catch (\Exception $mailEx) { logger()->error('API Employer Forgot Password Mail Failure: ' . $mailEx->getMessage()); } return response()->json([ 'success' => true, 'message' => 'Password reset verification code sent to email successfully.', 'email' => $request->email ], 200); } catch (\Exception $e) { logger()->error('API Employer Forgot Password Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'An error occurred. Please try again.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } public function resetPassword(Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|email', 'otp' => 'required|string|size:6', 'password' => 'required|string|min:8|confirmed', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validation error.', 'errors' => $validator->errors() ], 422); } $cachedData = \Illuminate\Support\Facades\Cache::get('employer_reset_otp_' . $request->email); if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) { return response()->json([ 'success' => false, 'message' => 'Invalid or expired 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); } try { $user = User::where('email', $request->email)->where('role', 'employer')->first(); $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); if (!$user || !$sponsor) { return response()->json([ 'success' => false, 'message' => 'User account not found.' ], 404); } $apiToken = Str::random(80); \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) { $hashedPassword = Hash::make($request->password); $user->update([ 'password' => $hashedPassword, 'api_token' => $apiToken, ]); $sponsor->update([ 'password' => $hashedPassword, ]); }); \Illuminate\Support\Facades\Cache::forget('employer_reset_otp_' . $request->email); return response()->json([ 'success' => true, 'message' => 'Password reset successfully.', 'data' => [ 'employer' => $user->load('employerProfile'), 'token' => $apiToken ] ], 200); } catch (\Exception $e) { logger()->error('API Employer Reset Password Failure: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Failed to reset password. Please try again.', 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' ], 500); } } }