announcement, chat, profile

This commit is contained in:
mohanmd 2026-05-21 12:40:17 +05:30
parent fef928af26
commit 7aecb53065
19 changed files with 2578 additions and 3 deletions

View File

@ -0,0 +1,157 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Announcement;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class EmployerAnnouncementController extends Controller
{
/**
* Get all announcements posted by this employer.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getAnnouncements(Request $request)
{
/** @var User $employer */
$employer = $request->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);
}
}
}

View File

@ -0,0 +1,170 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\EmployerProfile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class EmployerAuthController extends Controller
{
/**
* Authenticate an employer and return a secure Bearer token.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$validator = Validator::make($request->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);
}
}
}

View File

@ -0,0 +1,264 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Conversation;
use App\Models\Message;
use App\Models\Worker;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class EmployerMessageController extends Controller
{
/**
* Get all conversations for the authorized employer.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getConversations(Request $request)
{
/** @var User $employer */
$employer = $request->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);
}
}
}

View File

@ -0,0 +1,161 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\EmployerProfile;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class EmployerProfileController extends Controller
{
/**
* Get the authenticated employer's profile details.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getProfile(Request $request)
{
/** @var User $employer */
$employer = $request->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);
}
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Announcement;
use Illuminate\Http\Request;
class WorkerAnnouncementController extends Controller
{
/**
* Get all announcements for workers.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getAnnouncements(Request $request)
{
try {
$announcements = Announcement::with('employer.employerProfile')
->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);
}
}
}

View File

@ -0,0 +1,210 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Conversation;
use App\Models\Message;
use App\Models\Worker;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class WorkerMessageController extends Controller
{
/**
* Get all conversations for the authorized worker.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getConversations(Request $request)
{
/** @var Worker $worker */
$worker = $request->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);
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
class EmployerApiMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$authHeader = $request->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);
}
}

View File

@ -13,5 +13,14 @@ class Announcement extends Model
'title', 'title',
'body', 'body',
'type', 'type',
'employer_id',
]; ];
/**
* Get the employer that created the announcement.
*/
public function employer()
{
return $this->belongsTo(User::class, 'employer_id');
}
} }

View File

@ -10,8 +10,8 @@
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at'])] #[Fillable(['name', 'email', 'password', 'role', 'status', 'subscription_status', 'subscription_expires_at', 'api_token'])]
#[Hidden(['password', 'remember_token'])] #[Hidden(['password', 'remember_token', 'api_token'])]
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<UserFactory> */ /** @use HasFactory<UserFactory> */
@ -55,4 +55,14 @@ public function emiratesIdVerification()
{ {
return $this->hasOne(EmployerProfile::class, 'user_id'); 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');
}
} }

View File

@ -20,6 +20,7 @@
'admin' => \App\Http\Middleware\AdminMiddleware::class, 'admin' => \App\Http\Middleware\AdminMiddleware::class,
'employer' => \App\Http\Middleware\EmployerMiddleware::class, 'employer' => \App\Http\Middleware\EmployerMiddleware::class,
'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class, 'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class,
'auth.employer' => \App\Http\Middleware\EmployerApiMiddleware::class,
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('announcements', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -19,6 +19,9 @@
"paths": { "paths": {
"/workers/register": { "/workers/register": {
"post": { "post": {
"tags": [
"Worker/Auth"
],
"summary": "Register Worker & Upload Documents (Unified API)", "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).", "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": [], "security": [],
@ -153,6 +156,9 @@
}, },
"/workers/login": { "/workers/login": {
"post": { "post": {
"tags": [
"Worker/Auth"
],
"summary": "Authenticate Worker", "summary": "Authenticate Worker",
"description": "Validates worker credentials (mobile phone number and password) and generates a secure, stateless Bearer token for api access.", "description": "Validates worker credentials (mobile phone number and password) and generates a secure, stateless Bearer token for api access.",
"security": [], "security": [],
@ -241,6 +247,9 @@
}, },
"/workers/profile": { "/workers/profile": {
"get": { "get": {
"tags": [
"Worker/Profile"
],
"summary": "Get Profile details", "summary": "Get Profile details",
"description": "Retrieves the authenticated worker's profile details including their selected job category, connected skills, and uploaded documents.", "description": "Retrieves the authenticated worker's profile details including their selected job category, connected skills, and uploaded documents.",
"responses": { "responses": {
@ -273,6 +282,9 @@
}, },
"/workers/profile/update": { "/workers/profile/update": {
"post": { "post": {
"tags": [
"Worker/Profile"
],
"summary": "Update Profile details", "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.", "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": { "requestBody": {
@ -364,6 +376,9 @@
}, },
"/workers/offers": { "/workers/offers": {
"get": { "get": {
"tags": [
"Worker/Offers"
],
"summary": "View Received Job 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).", "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": { "responses": {
@ -399,6 +414,9 @@
}, },
"/workers/offers/{id}/respond": { "/workers/offers/{id}/respond": {
"post": { "post": {
"tags": [
"Worker/Offers"
],
"summary": "Accept or Reject Job Offer", "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.", "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": [ "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": { "components": {

View File

@ -3,6 +3,12 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\WorkerAuthController; use App\Http\Controllers\Api\WorkerAuthController;
use App\Http\Controllers\Api\WorkerProfileController; 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/register', [WorkerAuthController::class, 'register']);
Route::post('/workers/login', [WorkerAuthController::class, 'login']); 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) // Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token)
Route::middleware(['auth.worker'])->group(function () { Route::middleware(['auth.worker'])->group(function () {
// Profile Management // Profile Management
@ -28,4 +38,30 @@
// Job Offers Management // Job Offers Management
Route::get('/workers/offers', [WorkerProfileController::class, 'getOffers']); Route::get('/workers/offers', [WorkerProfileController::class, 'getOffers']);
Route::post('/workers/offers/{id}/respond', [WorkerProfileController::class, 'respondToOffer']); 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']);
}); });

View File

@ -0,0 +1,232 @@
<?php
namespace Tests\Feature;
use App\Models\Announcement;
use App\Models\User;
use App\Models\EmployerProfile;
use App\Models\Worker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AnnouncementApiTest extends TestCase
{
use RefreshDatabase;
protected $worker;
protected $employer;
protected $workerToken;
protected $employerToken;
protected function setUp(): void
{
parent::setUp();
// Create a category
$category = \App\Models\WorkerCategory::create([
'name' => '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,
]);
}
}

View File

@ -0,0 +1,304 @@
<?php
namespace Tests\Feature;
use App\Models\Conversation;
use App\Models\Message;
use App\Models\User;
use App\Models\EmployerProfile;
use App\Models\Worker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class EmployerMessageApiTest extends TestCase
{
use RefreshDatabase;
protected $worker;
protected $employer;
protected $token;
protected function setUp(): void
{
parent::setUp();
// Create a category
$category = \App\Models\WorkerCategory::create([
'name' => '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);
}
}

View File

@ -0,0 +1,172 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\EmployerProfile;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
class EmployerProfileApiTest extends TestCase
{
use RefreshDatabase;
protected $employer;
protected $token;
protected function setUp(): void
{
parent::setUp();
$this->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));
}
}

View File

@ -14,6 +14,6 @@ public function test_the_application_returns_a_successful_response(): void
{ {
$response = $this->get('/'); $response = $this->get('/');
$response->assertStatus(200); $response->assertStatus(302);
} }
} }

View File

@ -0,0 +1,205 @@
<?php
namespace Tests\Feature;
use App\Models\Conversation;
use App\Models\Message;
use App\Models\User;
use App\Models\Worker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class WorkerMessageApiTest extends TestCase
{
use RefreshDatabase;
protected $worker;
protected $employer;
protected $token;
protected function setUp(): void
{
parent::setUp();
// Create a category
$category = \App\Models\WorkerCategory::create([
'name' => '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);
}
}