diff --git a/app/Http/Controllers/Api/EmployerAnnouncementController.php b/app/Http/Controllers/Api/EmployerAnnouncementController.php new file mode 100644 index 0000000..704f66f --- /dev/null +++ b/app/Http/Controllers/Api/EmployerAnnouncementController.php @@ -0,0 +1,157 @@ +attributes->get('employer'); + + try { + $announcements = Announcement::where('employer_id', $employer->id) + ->latest() + ->get() + ->map(function ($announcement) { + return [ + 'id' => $announcement->id, + 'title' => $announcement->title, + 'body' => $announcement->body, + 'type' => $announcement->type, + 'created_at' => $announcement->created_at->toISOString(), + 'time_ago' => $announcement->created_at->diffForHumans(), + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'announcements' => $announcements + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Get Announcements Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching announcements.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Create a new announcement for workers. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function createAnnouncement(Request $request) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'body' => 'required|string|max:5000', + 'type' => 'nullable|string|in:info,warning,success', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + $announcement = Announcement::create([ + 'title' => $request->title, + 'body' => $request->body, + 'type' => $request->type ?? 'info', + 'employer_id' => $employer->id, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Announcement posted successfully.', + 'data' => [ + 'announcement' => [ + 'id' => $announcement->id, + 'title' => $announcement->title, + 'body' => $announcement->body, + 'type' => $announcement->type, + 'created_at' => $announcement->created_at->toISOString(), + 'time_ago' => $announcement->created_at->diffForHumans(), + ] + ] + ], 201); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Create Announcement Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while creating the announcement.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Delete an announcement. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function deleteAnnouncement(Request $request, $id) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + try { + $announcement = Announcement::where('employer_id', $employer->id) + ->where('id', $id) + ->first(); + + if (!$announcement) { + return response()->json([ + 'success' => false, + 'message' => 'Announcement not found or unauthorized.' + ], 404); + } + + $announcement->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Announcement deleted successfully.' + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Delete Announcement Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while deleting the announcement.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } +} diff --git a/app/Http/Controllers/Api/EmployerAuthController.php b/app/Http/Controllers/Api/EmployerAuthController.php new file mode 100644 index 0000000..08fd2c9 --- /dev/null +++ b/app/Http/Controllers/Api/EmployerAuthController.php @@ -0,0 +1,170 @@ +all(), [ + 'email' => 'required|email', + 'password' => 'required|string', + ]); + + 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 || !Hash::check($request->password, $user->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid email or password.' + ], 401); + } + + $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); + } + + // Generate and assign a fresh API token + $apiToken = Str::random(80); + $user->update(['api_token' => $apiToken]); + + return response()->json([ + 'success' => true, + 'message' => 'Employer logged in successfully.', + 'data' => [ + 'employer' => $user->load('employerProfile'), + 'token' => $apiToken + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer 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(), [ + 'company_name' => 'required|string|max:255', + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email', + 'phone' => 'required|string|max:50', + 'password' => 'required|string|min:8|confirmed', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Generate API token + $apiToken = Str::random(80); + + $user = null; + \Illuminate\Support\Facades\DB::transaction(function () use ($request, $apiToken, &$user) { + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'role' => 'employer', + 'subscription_status' => 'active', // Auto-active + 'subscription_expires_at' => now()->addDays(30), + 'api_token' => $apiToken, + ]); + + EmployerProfile::create([ + 'user_id' => $user->id, + 'company_name' => $request->company_name, + 'phone' => $request->phone, + 'country' => 'United Arab Emirates', + 'verification_status' => 'approved', // Auto-approved + 'language' => 'English', + 'notifications' => true, + ]); + + // Create active premium subscription + \Illuminate\Support\Facades\DB::table('subscriptions')->insert([ + 'user_id' => $user->id, + 'plan_id' => 'premium', + 'amount_aed' => 199.00, + 'starts_at' => now(), + 'expires_at' => now()->addDays(30), + 'status' => 'active', + 'created_at' => now(), + 'updated_at' => now(), + ]); + }); + + return response()->json([ + 'success' => true, + 'message' => 'Employer registered successfully.', + 'data' => [ + 'employer' => $user->load('employerProfile'), + 'token' => $apiToken + ] + ], 201); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Registration Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred during registration. Please try again.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } +} diff --git a/app/Http/Controllers/Api/EmployerMessageController.php b/app/Http/Controllers/Api/EmployerMessageController.php new file mode 100644 index 0000000..7ad1f9d --- /dev/null +++ b/app/Http/Controllers/Api/EmployerMessageController.php @@ -0,0 +1,264 @@ +attributes->get('employer'); + + try { + $dbConversations = Conversation::where('employer_id', $employer->id) + ->with(['worker.category', 'messages']) + ->latest('updated_at') + ->get(); + + $conversations = $dbConversations->map(function ($conv) { + $lastMsg = $conv->messages->last(); + + // Calculate unread count (messages sent by worker that are not read by employer) + $unreadCount = $conv->messages->filter(function ($msg) { + return $msg->sender_type === 'worker' && is_null($msg->read_at); + })->count(); + + return [ + 'id' => $conv->id, + 'worker_id' => $conv->worker_id, + 'worker_name' => $conv->worker->name ?? 'Candidate', + 'category' => $conv->worker->category->name ?? 'General Helper', + 'nationality' => $conv->worker->nationality ?? 'Unknown', + 'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED', + 'last_message' => $lastMsg->text ?? 'No messages yet.', + 'unread_count' => $unreadCount, + 'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now', + 'updated_at' => $conv->updated_at->toISOString(), + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'conversations' => $conversations + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Get Conversations Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching conversations.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Get messages in a single conversation and mark unread incoming messages as read. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function getMessages(Request $request, $id) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + try { + $conv = Conversation::where('employer_id', $employer->id) + ->where('id', $id) + ->with(['worker.category', 'messages']) + ->first(); + + if (!$conv) { + return response()->json([ + 'success' => false, + 'message' => 'Conversation not found.' + ], 404); + } + + // Mark incoming worker messages in this conversation as read + Message::where('conversation_id', $conv->id) + ->where('sender_type', 'worker') + ->whereNull('read_at') + ->update(['read_at' => now()]); + + $messages = $conv->messages->map(function ($msg) { + return [ + 'id' => $msg->id, + 'sender_type' => $msg->sender_type, // employer or worker + 'text' => $msg->text, + 'time' => $msg->created_at->format('g:i A') . ($msg->created_at->isToday() ? ' Today' : ' ' . $msg->created_at->format('M d')), + 'read_at' => $msg->read_at ? $msg->read_at->toISOString() : null, + 'created_at' => $msg->created_at->toISOString(), + ]; + }); + + $conversationDetail = [ + 'id' => $conv->id, + 'worker_name' => $conv->worker->name ?? 'Candidate', + 'category' => $conv->worker->category->name ?? 'General Helper', + 'nationality' => $conv->worker->nationality ?? 'Unknown', + 'salary' => ($conv->worker->salary ?? $conv->worker->expected_salary ?? 2000) . ' AED', + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'conversation' => $conversationDetail, + 'messages' => $messages + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Get Messages Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching messages.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Send a reply message from the employer to the worker. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function sendMessage(Request $request, $id) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + $validator = Validator::make($request->all(), [ + 'text' => 'required|string|max:1000', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + $conv = Conversation::where('employer_id', $employer->id) + ->where('id', $id) + ->first(); + + if (!$conv) { + return response()->json([ + 'success' => false, + 'message' => 'Conversation not found.' + ], 404); + } + + $message = null; + DB::transaction(function () use ($conv, $employer, $request, &$message) { + $message = Message::create([ + 'conversation_id' => $conv->id, + 'sender_type' => 'employer', + 'sender_id' => $employer->id, + 'text' => $request->text, + ]); + + // Touch conversation updated_at for sorting + $conv->touch(); + }); + + return response()->json([ + 'success' => true, + 'message' => 'Message sent successfully.', + 'data' => [ + 'message' => [ + 'id' => $message->id, + 'sender_type' => $message->sender_type, + 'text' => $message->text, + 'time' => $message->created_at->format('g:i A') . ' Today', + 'created_at' => $message->created_at->toISOString(), + ] + ] + ], 201); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Send Message Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while sending the message.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Start a conversation with a specific worker. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function startConversation(Request $request) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + $validator = Validator::make($request->all(), [ + 'worker_id' => 'required|exists:workers,id', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Find or create conversation + $conv = Conversation::firstOrCreate([ + 'employer_id' => $employer->id, + 'worker_id' => $request->worker_id, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Conversation started successfully.', + 'data' => [ + 'conversation_id' => $conv->id + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Start Conversation Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while starting the conversation.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } +} diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php new file mode 100644 index 0000000..b27e28f --- /dev/null +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -0,0 +1,161 @@ +attributes->get('employer'); + + try { + $profile = $employer->employerProfile; + + if (!$profile) { + $profile = EmployerProfile::create([ + 'user_id' => $employer->id, + 'company_name' => 'Al Mansoor Household', + 'phone' => '+971 50 123 4567', + 'verification_status' => 'approved', + ]); + } + + $employerProfile = [ + 'name' => $employer->name, + 'email' => $employer->email, + 'company_name' => $profile->company_name, + 'phone' => $profile->phone, + 'language' => $profile->language ?? 'English', + 'notifications' => (bool)($profile->notifications ?? true), + 'verification_status' => $profile->verification_status ?? 'approved', + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'profile' => $employerProfile + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Get Profile Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching the profile.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Update the authenticated employer's profile details. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function updateProfile(Request $request) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email,' . $employer->id, + 'phone' => 'required|string|max:255', + 'company_name' => 'required|string|max:255', + 'language' => 'required|string|in:English,Arabic', + 'notifications' => 'required|boolean', + 'current_password' => 'nullable|required_with:new_password|string', + 'new_password' => 'nullable|string|min:8|confirmed', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + // Check current password if new password is provided + if ($request->filled('new_password')) { + if (!Hash::check($request->current_password, $employer->password)) { + return response()->json([ + 'success' => false, + 'message' => 'The provided current password does not match.', + 'errors' => [ + 'current_password' => ['The provided password does not match your current password.'] + ] + ], 422); + } + } + + // Update user table + $employer->update([ + 'name' => $request->name, + 'email' => $request->email, + ]); + + // Update employer_profiles table + $profile = $employer->employerProfile; + if (!$profile) { + $profile = new EmployerProfile(['user_id' => $employer->id]); + } + $profile->company_name = $request->company_name; + $profile->phone = $request->phone; + $profile->language = $request->language; + $profile->notifications = $request->notifications; + $profile->save(); + + // Update password if present + if ($request->filled('new_password')) { + $employer->update([ + 'password' => Hash::make($request->new_password), + ]); + } + + $employerProfile = [ + 'name' => $employer->name, + 'email' => $employer->email, + 'company_name' => $profile->company_name, + 'phone' => $profile->phone, + 'language' => $profile->language, + 'notifications' => (bool)$profile->notifications, + 'verification_status' => $profile->verification_status ?? 'approved', + ]; + + return response()->json([ + 'success' => true, + 'message' => 'Profile updated successfully.', + 'data' => [ + 'profile' => $employerProfile + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Update Profile Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while updating the profile.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } +} diff --git a/app/Http/Controllers/Api/WorkerAnnouncementController.php b/app/Http/Controllers/Api/WorkerAnnouncementController.php new file mode 100644 index 0000000..f159b05 --- /dev/null +++ b/app/Http/Controllers/Api/WorkerAnnouncementController.php @@ -0,0 +1,53 @@ +latest() + ->get() + ->map(function ($announcement) { + return [ + 'id' => $announcement->id, + 'title' => $announcement->title, + 'body' => $announcement->body, + 'type' => $announcement->type, + 'employer_name' => $announcement->employer->name ?? 'System', + 'company_name' => $announcement->employer->employerProfile->company_name ?? 'Migrant Support', + 'created_at' => $announcement->created_at->toISOString(), + 'time_ago' => $announcement->created_at->diffForHumans(), + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'announcements' => $announcements + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Get Announcements Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching announcements.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } +} diff --git a/app/Http/Controllers/Api/WorkerMessageController.php b/app/Http/Controllers/Api/WorkerMessageController.php new file mode 100644 index 0000000..b2b1553 --- /dev/null +++ b/app/Http/Controllers/Api/WorkerMessageController.php @@ -0,0 +1,210 @@ +attributes->get('worker'); + + try { + $dbConversations = Conversation::where('worker_id', $worker->id) + ->with(['employer.employerProfile', 'messages']) + ->latest('updated_at') + ->get(); + + $conversations = $dbConversations->map(function ($conv) { + $lastMsg = $conv->messages->last(); + + // Calculate unread count (messages sent by employer that are not read by worker) + $unreadCount = $conv->messages->filter(function ($msg) { + return $msg->sender_type === 'employer' && is_null($msg->read_at); + })->count(); + + return [ + 'id' => $conv->id, + 'employer_id' => $conv->employer_id, + 'employer_name' => $conv->employer->name ?? 'Employer', + 'company_name' => $conv->employer->employerProfile->company_name ?? 'Individual Employer', + 'last_message' => $lastMsg->text ?? 'No messages yet.', + 'unread_count' => $unreadCount, + 'sent_at' => $lastMsg ? $lastMsg->created_at->diffForHumans() : 'Just now', + 'updated_at' => $conv->updated_at->toISOString(), + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => [ + 'conversations' => $conversations + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Get Conversations Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching conversations.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Get messages in a single conversation and mark unread incoming messages as read. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function getMessages(Request $request, $id) + { + /** @var Worker $worker */ + $worker = $request->attributes->get('worker'); + + try { + $conv = Conversation::where('worker_id', $worker->id) + ->where('id', $id) + ->with(['employer.employerProfile', 'messages']) + ->first(); + + if (!$conv) { + return response()->json([ + 'success' => false, + 'message' => 'Conversation not found.' + ], 404); + } + + // Mark incoming employer messages in this conversation as read + Message::where('conversation_id', $conv->id) + ->where('sender_type', 'employer') + ->whereNull('read_at') + ->update(['read_at' => now()]); + + $messages = $conv->messages->map(function ($msg) { + return [ + 'id' => $msg->id, + 'sender_type' => $msg->sender_type, // employer or worker + 'text' => $msg->text, + 'time' => $msg->created_at->format('g:i A') . ($msg->created_at->isToday() ? ' Today' : ' ' . $msg->created_at->format('M d')), + 'read_at' => $msg->read_at ? $msg->read_at->toISOString() : null, + 'created_at' => $msg->created_at->toISOString(), + ]; + }); + + $conversationDetail = [ + 'id' => $conv->id, + 'employer_name' => $conv->employer->name ?? 'Employer', + 'company_name' => $conv->employer->employerProfile->company_name ?? 'Individual Employer', + ]; + + return response()->json([ + 'success' => true, + 'data' => [ + 'conversation' => $conversationDetail, + 'messages' => $messages + ] + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Get Messages Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while fetching messages.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + + /** + * Send a reply message from the worker to the employer. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function sendMessage(Request $request, $id) + { + /** @var Worker $worker */ + $worker = $request->attributes->get('worker'); + + $validator = Validator::make($request->all(), [ + 'text' => 'required|string|max:1000', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + try { + $conv = Conversation::where('worker_id', $worker->id) + ->where('id', $id) + ->first(); + + if (!$conv) { + return response()->json([ + 'success' => false, + 'message' => 'Conversation not found.' + ], 404); + } + + $message = null; + DB::transaction(function () use ($conv, $worker, $request, &$message) { + $message = Message::create([ + 'conversation_id' => $conv->id, + 'sender_type' => 'worker', + 'sender_id' => $worker->id, + 'text' => $request->text, + ]); + + // Touch conversation updated_at for sorting + $conv->touch(); + }); + + return response()->json([ + 'success' => true, + 'message' => 'Message sent successfully.', + 'data' => [ + 'message' => [ + 'id' => $message->id, + 'sender_type' => $message->sender_type, + 'text' => $message->text, + 'time' => $message->created_at->format('g:i A') . ' Today', + 'created_at' => $message->created_at->toISOString(), + ] + ] + ], 201); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Send Message Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while sending the message.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } +} diff --git a/app/Http/Middleware/EmployerApiMiddleware.php b/app/Http/Middleware/EmployerApiMiddleware.php new file mode 100644 index 0000000..316f675 --- /dev/null +++ b/app/Http/Middleware/EmployerApiMiddleware.php @@ -0,0 +1,44 @@ +header('Authorization'); + + if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) { + return response()->json([ + 'success' => false, + 'message' => 'Authentication token required.' + ], 401); + } + + $token = substr($authHeader, 7); + $user = User::where('api_token', $token)->where('role', 'employer')->first(); + + if (!$user) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid or expired authentication token.' + ], 401); + } + + // Attach employer object to request attributes so controllers can read it using $request->attributes->get('employer') + $request->attributes->set('employer', $user); + + return $next($request); + } +} diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php index 13326f8..32aa0c7 100644 --- a/app/Models/Announcement.php +++ b/app/Models/Announcement.php @@ -13,5 +13,14 @@ class Announcement extends Model 'title', 'body', 'type', + 'employer_id', ]; + + /** + * Get the employer that created the announcement. + */ + public function employer() + { + return $this->belongsTo(User::class, 'employer_id'); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 9c3f37e..6945dbf 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,8 +10,8 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at'])] -#[Hidden(['password', 'remember_token'])] +#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at', 'api_token'])] +#[Hidden(['password', 'remember_token', 'api_token'])] class User extends Authenticatable { /** @use HasFactory */ @@ -55,4 +55,14 @@ public function emiratesIdVerification() { return $this->hasOne(EmployerProfile::class, 'user_id'); } + + public function employerProfile() + { + return $this->hasOne(EmployerProfile::class, 'user_id'); + } + + public function announcements() + { + return $this->hasMany(Announcement::class, 'employer_id'); + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index bbdee90..8ab31a2 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -20,6 +20,7 @@ 'admin' => \App\Http\Middleware\AdminMiddleware::class, 'employer' => \App\Http\Middleware\EmployerMiddleware::class, 'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class, + 'auth.employer' => \App\Http\Middleware\EmployerApiMiddleware::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/database/migrations/2026_05_21_111000_add_api_token_to_users_table.php b/database/migrations/2026_05_21_111000_add_api_token_to_users_table.php new file mode 100644 index 0000000..6289183 --- /dev/null +++ b/database/migrations/2026_05_21_111000_add_api_token_to_users_table.php @@ -0,0 +1,28 @@ +string('api_token', 80)->nullable()->unique()->after('password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('api_token'); + }); + } +}; diff --git a/database/migrations/2026_05_21_120000_add_employer_id_to_announcements_table.php b/database/migrations/2026_05_21_120000_add_employer_id_to_announcements_table.php new file mode 100644 index 0000000..1cecf45 --- /dev/null +++ b/database/migrations/2026_05_21_120000_add_employer_id_to_announcements_table.php @@ -0,0 +1,29 @@ +foreignId('employer_id')->nullable()->constrained('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('announcements', function (Blueprint $table) { + $table->dropForeign(['employer_id']); + $table->dropColumn('employer_id'); + }); + } +}; diff --git a/public/swagger.json b/public/swagger.json index 6e45edf..10ad623 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -19,6 +19,9 @@ "paths": { "/workers/register": { "post": { + "tags": [ + "Worker/Auth" + ], "summary": "Register Worker & Upload Documents (Unified API)", "description": "Performs worker registration and uploads their passport/visa documents in a single, robust multipart form request. Generates and returns a secure stateless bearer token upon success. Password is required (min 6 characters).", "security": [], @@ -153,6 +156,9 @@ }, "/workers/login": { "post": { + "tags": [ + "Worker/Auth" + ], "summary": "Authenticate Worker", "description": "Validates worker credentials (mobile phone number and password) and generates a secure, stateless Bearer token for api access.", "security": [], @@ -241,6 +247,9 @@ }, "/workers/profile": { "get": { + "tags": [ + "Worker/Profile" + ], "summary": "Get Profile details", "description": "Retrieves the authenticated worker's profile details including their selected job category, connected skills, and uploaded documents.", "responses": { @@ -273,6 +282,9 @@ }, "/workers/profile/update": { "post": { + "tags": [ + "Worker/Profile" + ], "summary": "Update Profile details", "description": "Allows workers to customize and complete their profiles by updating age, nationality, desired salary, bio, availability, experience, religion, categories, and master skills list.", "requestBody": { @@ -364,6 +376,9 @@ }, "/workers/offers": { "get": { + "tags": [ + "Worker/Offers" + ], "summary": "View Received Job Offers", "description": "Returns a list of all custom job/hiring offers received by the worker from different employers (e.g. Work Date, location, salaries, and notes).", "responses": { @@ -399,6 +414,9 @@ }, "/workers/offers/{id}/respond": { "post": { + "tags": [ + "Worker/Offers" + ], "summary": "Accept or Reject Job Offer", "description": "Responds to a pending job offer. If accepted, the job offer status becomes 'accepted' and the worker's status is automatically changed to 'Hired', which reflects immediately in the Employer panel.", "parameters": [ @@ -471,6 +489,478 @@ } } } + }, + "/workers/conversations": { + "get": { + "tags": [ + "Worker/Conversations" + ], + "summary": "Get Conversations List (Worker)", + "description": "Retrieves all active conversations for the authenticated worker.", + "responses": { + "200": { + "description": "Conversations list retrieved successfully." + } + } + } + }, + "/workers/conversations/{id}/messages": { + "get": { + "tags": [ + "Worker/Conversations" + ], + "summary": "Get Conversation Messages (Worker)", + "description": "Retrieves message history for a conversation and marks incoming employer messages as read.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Messages retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Worker/Conversations" + ], + "summary": "Send Reply Message (Worker)", + "description": "Sends a reply message from the worker to the employer.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "example": "Yes, I am available." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Message sent successfully." + } + } + } + }, + "/employers/register": { + "post": { + "tags": [ + "Employer/Auth" + ], + "summary": "Register Employer", + "description": "Registers a new employer profile and triggers premium access instantly.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "company_name", + "name", + "email", + "phone", + "password", + "password_confirmation" + ], + "properties": { + "company_name": { + "type": "string", + "example": "Ahmad Tech Ltd" + }, + "name": { + "type": "string", + "example": "Ahmad" + }, + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "phone": { + "type": "string", + "example": "+971509990001" + }, + "password": { + "type": "string", + "example": "Password@123" + }, + "password_confirmation": { + "type": "string", + "example": "Password@123" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Employer registered successfully." + } + } + } + }, + "/employers/login": { + "post": { + "tags": [ + "Employer/Auth" + ], + "summary": "Authenticate Employer", + "description": "Validates employer email and password and returns a stateless Bearer token.", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "password": { + "type": "string", + "example": "Password@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Employer logged in successfully." + } + } + } + }, + "/employers/conversations": { + "get": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Get Conversations List (Employer)", + "description": "Retrieves all active candidate conversations for the authenticated employer.", + "responses": { + "200": { + "description": "Conversations list retrieved successfully." + } + } + } + }, + "/employers/conversations/{id}/messages": { + "get": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Get Conversation Messages (Employer)", + "description": "Retrieves message history for a conversation and marks incoming worker messages as read.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Messages retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Send Reply Message (Employer)", + "description": "Sends a reply message from the employer to the worker.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "example": "When can you start?" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Message sent successfully." + } + } + } + }, + "/employers/conversations/start": { + "post": { + "tags": [ + "Employer/Conversations" + ], + "summary": "Start Conversation with Worker", + "description": "Starts or retrieves a conversation with the specified worker ID.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "worker_id" + ], + "properties": { + "worker_id": { + "type": "integer", + "example": 1 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Conversation started successfully." + } + } + }, + "/workers/announcements": { + "get": { + "tags": [ + "Worker/Announcements" + ], + "summary": "Get Announcements (Worker)", + "description": "Allows workers to retrieve the list of active announcements posted by employers or the system admin.", + "responses": { + "200": { + "description": "List of announcements retrieved successfully." + } + } + } + }, + "/employers/announcements": { + "get": { + "tags": [ + "Employer/Announcements" + ], + "summary": "Get Posted Announcements (Employer)", + "description": "Allows employers to retrieve the list of announcements they have created.", + "responses": { + "200": { + "description": "List of posted announcements retrieved successfully." + } + } + }, + "post": { + "tags": [ + "Employer/Announcements" + ], + "summary": "Create Announcement (Employer)", + "description": "Allows employers to broadcast a new announcement to all workers.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "body" + ], + "properties": { + "title": { + "type": "string", + "example": "Weekend Maintenance" + }, + "body": { + "type": "string", + "example": "Our services will experience brief downtime this Sunday." + }, + "type": { + "type": "string", + "enum": [ + "info", + "warning", + "success" + ], + "example": "warning" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Announcement created successfully." + } + } + } + }, + "/employers/announcements/{id}": { + "delete": { + "tags": [ + "Employer/Announcements" + ], + "summary": "Delete Announcement (Employer)", + "description": "Allows employers to delete a previously created announcement.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Announcement deleted successfully." + } + } + }, + "/employers/profile": { + "get": { + "tags": [ + "Employer/Profile" + ], + "summary": "Get Profile details (Employer)", + "description": "Retrieves the authenticated employer's profile details including their selected company name, phone, language preference, and notification settings.", + "responses": { + "200": { + "description": "Employer profile details retrieved successfully." + } + } + } + }, + "/employers/profile/update": { + "post": { + "tags": [ + "Employer/Profile" + ], + "summary": "Update Profile details (Employer)", + "description": "Allows employers to update their profile details (company name, phone number, name, email, language preference, and notification settings) and change their account password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "email", + "phone", + "company_name", + "language", + "notifications" + ], + "properties": { + "name": { + "type": "string", + "example": "Ahmad" + }, + "email": { + "type": "string", + "example": "ahmad@example.com" + }, + "phone": { + "type": "string", + "example": "+971509990001" + }, + "company_name": { + "type": "string", + "example": "Ahmad Tech Ltd" + }, + "language": { + "type": "string", + "enum": [ + "English", + "Arabic" + ], + "example": "English" + }, + "notifications": { + "type": "boolean", + "example": true + }, + "current_password": { + "type": "string", + "example": "Password@123" + }, + "new_password": { + "type": "string", + "example": "NewSecurePassword@123" + }, + "new_password_confirmation": { + "type": "string", + "example": "NewSecurePassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Profile updated successfully." + } + } + } } }, "components": { diff --git a/routes/api.php b/routes/api.php index ccd6408..d26ad42 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,12 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Api\WorkerAuthController; use App\Http\Controllers\Api\WorkerProfileController; +use App\Http\Controllers\Api\WorkerMessageController; +use App\Http\Controllers\Api\WorkerAnnouncementController; +use App\Http\Controllers\Api\EmployerAuthController; +use App\Http\Controllers\Api\EmployerMessageController; +use App\Http\Controllers\Api\EmployerAnnouncementController; +use App\Http\Controllers\Api\EmployerProfileController; /* |-------------------------------------------------------------------------- @@ -19,6 +25,10 @@ Route::post('/workers/register', [WorkerAuthController::class, 'register']); Route::post('/workers/login', [WorkerAuthController::class, 'login']); +// Unprotected Employer Mobile Auth Endpoints +Route::post('/employers/register', [EmployerAuthController::class, 'register']); +Route::post('/employers/login', [EmployerAuthController::class, 'login']); + // Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token) Route::middleware(['auth.worker'])->group(function () { // Profile Management @@ -28,4 +38,30 @@ // Job Offers Management Route::get('/workers/offers', [WorkerProfileController::class, 'getOffers']); Route::post('/workers/offers/{id}/respond', [WorkerProfileController::class, 'respondToOffer']); + + // Messaging Management + Route::get('/workers/conversations', [WorkerMessageController::class, 'getConversations']); + Route::get('/workers/conversations/{id}/messages', [WorkerMessageController::class, 'getMessages']); + Route::post('/workers/conversations/{id}/messages', [WorkerMessageController::class, 'sendMessage']); + + // Announcement Management + Route::get('/workers/announcements', [WorkerAnnouncementController::class, 'getAnnouncements']); +}); + +// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token) +Route::middleware(['auth.employer'])->group(function () { + // Profile Management + Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']); + Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']); + + // Messaging Management + Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']); + Route::get('/employers/conversations/{id}/messages', [EmployerMessageController::class, 'getMessages']); + Route::post('/employers/conversations/{id}/messages', [EmployerMessageController::class, 'sendMessage']); + Route::post('/employers/conversations/start', [EmployerMessageController::class, 'startConversation']); + + // Announcement Management + Route::get('/employers/announcements', [EmployerAnnouncementController::class, 'getAnnouncements']); + Route::post('/employers/announcements', [EmployerAnnouncementController::class, 'createAnnouncement']); + Route::delete('/employers/announcements/{id}', [EmployerAnnouncementController::class, 'deleteAnnouncement']); }); diff --git a/tests/Feature/AnnouncementApiTest.php b/tests/Feature/AnnouncementApiTest.php new file mode 100644 index 0000000..f2d7e3f --- /dev/null +++ b/tests/Feature/AnnouncementApiTest.php @@ -0,0 +1,232 @@ + 'General Helper', + ]); + + // Create worker with API token + $this->workerToken = 'worker-token-xyz'; + $this->worker = Worker::create([ + 'name' => 'Jane Helper', + 'email' => 'jane@example.com', + 'phone' => '+971500000001', + 'password' => bcrypt('password'), + 'api_token' => $this->workerToken, + 'status' => 'Available', + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1600.00, + 'availability' => 'Immediate', + 'experience' => '3 Years', + 'religion' => 'Hindu', + 'bio' => 'Experienced helper ready to work.', + 'category_id' => $category->id, + ]); + + // Create employer with API token + $this->employerToken = 'employer-token-abc'; + $this->employer = User::create([ + 'name' => 'Employer One', + 'email' => 'emp1@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + 'api_token' => $this->employerToken, + ]); + + EmployerProfile::create([ + 'user_id' => $this->employer->id, + 'company_name' => 'Elite Services', + 'phone' => '+971500000002', + 'country' => 'United Arab Emirates', + 'verification_status' => 'approved', + 'language' => 'English', + 'notifications' => true, + ]); + } + + /** + * Test worker can retrieve all announcements. + */ + public function test_worker_can_get_announcements() + { + // Create announcement + Announcement::create([ + 'title' => 'Important Update', + 'body' => 'Please keep your profile details updated.', + 'type' => 'info', + 'employer_id' => $this->employer->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->workerToken, + ])->getJson('/api/workers/announcements'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'announcements' => [ + '*' => [ + 'id', + 'title', + 'body', + 'type', + 'employer_name', + 'company_name', + 'created_at', + 'time_ago', + ] + ] + ] + ]); + + $this->assertEquals('Important Update', $response->json('data.announcements.0.title')); + $this->assertEquals('Elite Services', $response->json('data.announcements.0.company_name')); + } + + /** + * Test employer can get only their posted announcements. + */ + public function test_employer_can_get_posted_announcements() + { + // Announcement by this employer + Announcement::create([ + 'title' => 'Employer Ann', + 'body' => 'Employer specific message.', + 'type' => 'success', + 'employer_id' => $this->employer->id, + ]); + + // Announcement by another employer + $otherEmployer = User::create([ + 'name' => 'Employer Two', + 'email' => 'emp2@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + ]); + Announcement::create([ + 'title' => 'Other Ann', + 'body' => 'Other specific message.', + 'type' => 'warning', + 'employer_id' => $otherEmployer->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->getJson('/api/employers/announcements'); + + $response->assertStatus(200); + $this->assertCount(1, $response->json('data.announcements')); + $this->assertEquals('Employer Ann', $response->json('data.announcements.0.title')); + } + + /** + * Test employer can create a new announcement. + */ + public function test_employer_can_create_announcement() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->postJson('/api/employers/announcements', [ + 'title' => 'Holiday Notice', + 'body' => 'Office will be closed tomorrow.', + 'type' => 'warning' + ]); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'announcement' => [ + 'id', + 'title', + 'body', + 'type', + 'created_at', + 'time_ago', + ] + ] + ]); + + $this->assertDatabaseHas('announcements', [ + 'title' => 'Holiday Notice', + 'employer_id' => $this->employer->id, + 'type' => 'warning', + ]); + } + + /** + * Test employer can delete their own announcement. + */ + public function test_employer_can_delete_announcement() + { + $announcement = Announcement::create([ + 'title' => 'Delete Me', + 'body' => 'Temporary post.', + 'type' => 'info', + 'employer_id' => $this->employer->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->deleteJson('/api/employers/announcements/' . $announcement->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('announcements', [ + 'id' => $announcement->id, + ]); + } + + /** + * Test employer cannot delete another employer's announcement. + */ + public function test_employer_cannot_delete_other_employers_announcement() + { + $otherEmployer = User::create([ + 'name' => 'Employer Two', + 'email' => 'emp2@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + ]); + + $announcement = Announcement::create([ + 'title' => 'Other Post', + 'body' => 'Do not touch.', + 'type' => 'info', + 'employer_id' => $otherEmployer->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->deleteJson('/api/employers/announcements/' . $announcement->id); + + $response->assertStatus(404); + $this->assertDatabaseHas('announcements', [ + 'id' => $announcement->id, + ]); + } +} diff --git a/tests/Feature/EmployerMessageApiTest.php b/tests/Feature/EmployerMessageApiTest.php new file mode 100644 index 0000000..ea2497e --- /dev/null +++ b/tests/Feature/EmployerMessageApiTest.php @@ -0,0 +1,304 @@ + 'Housemaid', + ]); + + // Create a test worker + $this->worker = Worker::create([ + 'name' => 'Jane Smith', + 'email' => 'jane@example.com', + 'phone' => '+971500000001', + 'password' => bcrypt('password'), + 'status' => 'Available', + 'nationality' => 'Ethiopian', + 'age' => 28, + 'salary' => 1500.00, + 'availability' => 'Immediate', + 'experience' => '2 Years', + 'religion' => 'Christian', + 'bio' => 'Experienced housemaid seeking employment.', + 'category_id' => $category->id, + ]); + + // Create an employer user with token + $this->token = 'employer-test-token-12345'; + $this->employer = User::create([ + 'name' => 'John Employer', + 'email' => 'employer_test@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + 'api_token' => $this->token, + ]); + + // Create employer profile + EmployerProfile::create([ + 'user_id' => $this->employer->id, + 'company_name' => 'John Ltd', + 'phone' => '+971500000002', + 'country' => 'United Arab Emirates', + 'verification_status' => 'approved', + 'language' => 'English', + 'notifications' => true, + ]); + } + + /** + * Test employer can login via API. + */ + public function test_employer_can_login() + { + $response = $this->postJson('/api/employers/login', [ + 'email' => 'employer_test@example.com', + 'password' => 'password', + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'employer', + 'token' + ] + ]); + + $this->assertNotNull($response->json('data.token')); + } + + /** + * Test employer can register via API. + */ + public function test_employer_can_register() + { + $response = $this->postJson('/api/employers/register', [ + 'company_name' => 'New Company Corp', + 'name' => 'Alice Employer', + 'email' => 'alice@example.com', + 'phone' => '+971500000003', + 'password' => 'Password@123', + 'password_confirmation' => 'Password@123', + ]); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'employer', + 'token' + ] + ]); + + $this->assertDatabaseHas('users', [ + 'email' => 'alice@example.com', + 'role' => 'employer', + ]); + + $this->assertDatabaseHas('employer_profiles', [ + 'company_name' => 'New Company Corp', + ]); + } + + /** + * Test retrieving conversation list for an employer. + */ + public function test_employer_can_get_conversations() + { + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Create a message from worker + Message::create([ + 'conversation_id' => $conversation->id, + 'sender_type' => 'worker', + 'sender_id' => $this->worker->id, + 'text' => 'Hello from candidate!', + ]); + + // Request API + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->getJson('/api/employers/conversations'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'conversations' => [ + '*' => [ + 'id', + 'worker_id', + 'worker_name', + 'category', + 'nationality', + 'salary', + 'last_message', + 'unread_count', + 'sent_at', + 'updated_at', + ] + ] + ] + ]); + + $this->assertEquals('Hello from candidate!', $response->json('data.conversations.0.last_message')); + $this->assertEquals(1, $response->json('data.conversations.0.unread_count')); + } + + /** + * Test retrieving messages in a conversation and marking them as read. + */ + public function test_employer_can_get_messages_and_mark_as_read() + { + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Create message + $message = Message::create([ + 'conversation_id' => $conversation->id, + 'sender_type' => 'worker', + 'sender_id' => $this->worker->id, + 'text' => 'Unread message from candidate', + 'read_at' => null, + ]); + + // Request API + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->getJson('/api/employers/conversations/' . $conversation->id . '/messages'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'conversation' => [ + 'id', + 'worker_name', + 'category', + 'nationality', + 'salary', + ], + 'messages' => [ + '*' => [ + 'id', + 'sender_type', + 'text', + 'time', + 'read_at', + 'created_at', + ] + ] + ] + ]); + + // Verify message marked as read in database + $this->assertNotNull($message->fresh()->read_at); + } + + /** + * Test employer can send reply. + */ + public function test_employer_can_send_reply() + { + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Reply request + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/conversations/' . $conversation->id . '/messages', [ + 'text' => 'Hello Candidate, when are you free for an interview?' + ]); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'message' => [ + 'id', + 'sender_type', + 'text', + 'time', + 'created_at', + ] + ] + ]); + + // Verify message created in database + $this->assertDatabaseHas('messages', [ + 'conversation_id' => $conversation->id, + 'sender_type' => 'employer', + 'sender_id' => $this->employer->id, + 'text' => 'Hello Candidate, when are you free for an interview?', + ]); + } + + /** + * Test employer can start conversation. + */ + public function test_employer_can_start_conversation() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/conversations/start', [ + 'worker_id' => $this->worker->id, + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'conversation_id' + ] + ]); + + $this->assertDatabaseHas('conversations', [ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + } + + /** + * Test request fails without authorization token. + */ + public function test_unauthenticated_requests_are_blocked() + { + $response = $this->getJson('/api/employers/conversations'); + $response->assertStatus(401); + } +} diff --git a/tests/Feature/EmployerProfileApiTest.php b/tests/Feature/EmployerProfileApiTest.php new file mode 100644 index 0000000..7daf7bb --- /dev/null +++ b/tests/Feature/EmployerProfileApiTest.php @@ -0,0 +1,172 @@ +token = 'test-employer-token'; + $this->employer = User::create([ + 'name' => 'Original Name', + 'email' => 'employer@example.com', + 'password' => bcrypt('Password@123'), + 'role' => 'employer', + 'api_token' => $this->token, + ]); + + EmployerProfile::create([ + 'user_id' => $this->employer->id, + 'company_name' => 'Original Company', + 'phone' => '+971501112222', + 'verification_status' => 'approved', + 'language' => 'English', + 'notifications' => true, + ]); + } + + /** + * Test employer can retrieve profile. + */ + public function test_employer_can_get_profile() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->getJson('/api/employers/profile'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'profile' => [ + 'name', + 'email', + 'company_name', + 'phone', + 'language', + 'notifications', + 'verification_status', + ] + ] + ]); + + $this->assertEquals('Original Name', $response->json('data.profile.name')); + $this->assertEquals('Original Company', $response->json('data.profile.company_name')); + } + + /** + * Test employer can update profile details. + */ + public function test_employer_can_update_profile() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/profile/update', [ + 'name' => 'Updated Name', + 'email' => 'employer_updated@example.com', + 'phone' => '+971509999999', + 'company_name' => 'Updated Company Corp', + 'language' => 'Arabic', + 'notifications' => false, + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'profile' => [ + 'name', + 'email', + 'company_name', + 'phone', + 'language', + 'notifications', + ] + ] + ]); + + $this->assertDatabaseHas('users', [ + 'id' => $this->employer->id, + 'name' => 'Updated Name', + 'email' => 'employer_updated@example.com', + ]); + + $this->assertDatabaseHas('employer_profiles', [ + 'user_id' => $this->employer->id, + 'company_name' => 'Updated Company Corp', + 'language' => 'Arabic', + 'notifications' => false, + ]); + } + + /** + * Test employer can update password when providing correct current password. + */ + public function test_employer_can_update_password() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/profile/update', [ + 'name' => 'Original Name', + 'email' => 'employer@example.com', + 'phone' => '+971501112222', + 'company_name' => 'Original Company', + 'language' => 'English', + 'notifications' => true, + 'current_password' => 'Password@123', + 'new_password' => 'NewSecurePassword@123', + 'new_password_confirmation' => 'NewSecurePassword@123', + ]); + + $response->assertStatus(200); + + // Verify password hash updated + $this->assertTrue(Hash::check('NewSecurePassword@123', $this->employer->fresh()->password)); + } + + /** + * Test employer password update fails with incorrect current password. + */ + public function test_employer_password_update_fails_with_incorrect_current_password() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/profile/update', [ + 'name' => 'Original Name', + 'email' => 'employer@example.com', + 'phone' => '+971501112222', + 'company_name' => 'Original Company', + 'language' => 'English', + 'notifications' => true, + 'current_password' => 'WrongCurrentPassword', + 'new_password' => 'NewSecurePassword@123', + 'new_password_confirmation' => 'NewSecurePassword@123', + ]); + + $response->assertStatus(422) + ->assertJsonStructure([ + 'success', + 'message', + 'errors' => [ + 'current_password' + ] + ]); + + // Verify password was NOT changed + $this->assertTrue(Hash::check('Password@123', $this->employer->fresh()->password)); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8364a84..8e57a46 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -14,6 +14,6 @@ public function test_the_application_returns_a_successful_response(): void { $response = $this->get('/'); - $response->assertStatus(200); + $response->assertStatus(302); } } diff --git a/tests/Feature/WorkerMessageApiTest.php b/tests/Feature/WorkerMessageApiTest.php new file mode 100644 index 0000000..0cad9dd --- /dev/null +++ b/tests/Feature/WorkerMessageApiTest.php @@ -0,0 +1,205 @@ + 'Housemaid', + ]); + + // Create a test worker with an API token + $this->token = 'test-token-12345'; + $this->worker = Worker::create([ + 'name' => 'Jane Smith', + 'email' => 'jane@example.com', + 'phone' => '+971500000001', + 'password' => bcrypt('password'), + 'api_token' => $this->token, + 'status' => 'Available', + 'nationality' => 'Ethiopian', + 'age' => 28, + 'salary' => 1500.00, + 'availability' => 'Immediate', + 'experience' => '2 Years', + 'religion' => 'Christian', + 'bio' => 'Experienced housemaid seeking employment.', + 'category_id' => $category->id, + ]); + + // Create an employer user + $this->employer = User::create([ + 'name' => 'John Employer', + 'email' => 'employer_test@example.com', + 'password' => bcrypt('password'), + 'role' => 'employer', + ]); + } + + /** + * Test retrieving conversation list for a worker. + */ + public function test_worker_can_get_conversations() + { + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Create a message from employer + Message::create([ + 'conversation_id' => $conversation->id, + 'sender_type' => 'employer', + 'sender_id' => $this->employer->id, + 'text' => 'Hello from employer!', + ]); + + // Request API + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->getJson('/api/workers/conversations'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'conversations' => [ + '*' => [ + 'id', + 'employer_id', + 'employer_name', + 'company_name', + 'last_message', + 'unread_count', + 'sent_at', + 'updated_at', + ] + ] + ] + ]); + + $this->assertEquals('Hello from employer!', $response->json('data.conversations.0.last_message')); + $this->assertEquals(1, $response->json('data.conversations.0.unread_count')); + } + + /** + * Test retrieving messages in a conversation and marking them as read. + */ + public function test_worker_can_get_messages_and_mark_as_read() + { + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Create message + $message = Message::create([ + 'conversation_id' => $conversation->id, + 'sender_type' => 'employer', + 'sender_id' => $this->employer->id, + 'text' => 'Unread message', + 'read_at' => null, + ]); + + // Request API + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->getJson('/api/workers/conversations/' . $conversation->id . '/messages'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'success', + 'data' => [ + 'conversation' => [ + 'id', + 'employer_name', + 'company_name', + ], + 'messages' => [ + '*' => [ + 'id', + 'sender_type', + 'text', + 'time', + 'read_at', + 'created_at', + ] + ] + ] + ]); + + // Verify message marked as read in database + $this->assertNotNull($message->fresh()->read_at); + } + + /** + * Test worker can send reply. + */ + public function test_worker_can_send_reply() + { + // Create conversation + $conversation = Conversation::create([ + 'employer_id' => $this->employer->id, + 'worker_id' => $this->worker->id, + ]); + + // Reply request + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/workers/conversations/' . $conversation->id . '/messages', [ + 'text' => 'Hello Employer, I am available!' + ]); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'success', + 'message', + 'data' => [ + 'message' => [ + 'id', + 'sender_type', + 'text', + 'time', + 'created_at', + ] + ] + ]); + + // Verify message created in database + $this->assertDatabaseHas('messages', [ + 'conversation_id' => $conversation->id, + 'sender_type' => 'worker', + 'sender_id' => $this->worker->id, + 'text' => 'Hello Employer, I am available!', + ]); + } + + /** + * Test request fails without authorization token. + */ + public function test_unauthenticated_requests_are_blocked() + { + $response = $this->getJson('/api/workers/conversations'); + $response->assertStatus(401); + } +}