review, worker, employer app
This commit is contained in:
parent
51193f3188
commit
a95a33829c
291
app/Console/Commands/SendReminders.php
Normal file
291
app/Console/Commands/SendReminders.php
Normal file
@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SendReminders extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:send-reminders';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Send automated reminder notifications for document expiry and pending confirmations';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting automated reminder notification system...');
|
||||
|
||||
$this->info('1. Checking Worker Documents...');
|
||||
$this->checkWorkerDocuments();
|
||||
|
||||
$this->info('2. Checking Employer Profiles (Emirates ID Expiry)...');
|
||||
$this->checkEmployerProfiles();
|
||||
|
||||
$this->info('3. Checking Sponsors (Emirates ID and License Expiry)...');
|
||||
$this->checkSponsors();
|
||||
|
||||
$this->info('4. Checking Pending Hire Confirmations (Employer)...');
|
||||
$this->checkHireConfirmations();
|
||||
|
||||
$this->info('5. Checking Pending Joining Confirmations (Worker)...');
|
||||
$this->checkJoiningConfirmations();
|
||||
|
||||
$this->info('6. Checking Pending Direct Job Offers...');
|
||||
$this->checkJobOffers();
|
||||
|
||||
$this->info('Automated reminder system run complete!');
|
||||
}
|
||||
|
||||
private function getReminderLevel(int $days): ?string
|
||||
{
|
||||
if ($days === 7) {
|
||||
return '7_days';
|
||||
} elseif ($days === 3) {
|
||||
return '3_days';
|
||||
} elseif ($days === 1) {
|
||||
return '1_day';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function alreadySent($notifiable, string $eventType, $entity, string $reminderLevel): bool
|
||||
{
|
||||
return \App\Models\ReminderLog::where('user_type', get_class($notifiable))
|
||||
->where('user_id', $notifiable->id)
|
||||
->where('event_type', $eventType)
|
||||
->where('entity_type', get_class($entity))
|
||||
->where('entity_id', $entity->id)
|
||||
->where('reminder_level', $reminderLevel)
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function logReminder($notifiable, string $eventType, $entity, string $reminderLevel, string $channel): void
|
||||
{
|
||||
\App\Models\ReminderLog::create([
|
||||
'user_type' => get_class($notifiable),
|
||||
'user_id' => $notifiable->id,
|
||||
'event_type' => $eventType,
|
||||
'entity_type' => get_class($entity),
|
||||
'entity_id' => $entity->id,
|
||||
'reminder_level' => $reminderLevel,
|
||||
'channel' => $channel,
|
||||
'sent_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function checkWorkerDocuments()
|
||||
{
|
||||
$workerDocs = \App\Models\WorkerDocument::whereNotNull('expiry_date')->get();
|
||||
foreach ($workerDocs as $doc) {
|
||||
$worker = $doc->worker;
|
||||
if (!$worker) continue;
|
||||
|
||||
$expiry = Carbon::parse($doc->expiry_date);
|
||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if (!$level) continue;
|
||||
|
||||
if ($this->alreadySent($worker, 'document_expiry', $doc, $level)) continue;
|
||||
|
||||
$this->info("Sending {$level} reminder to worker {$worker->name} for document {$doc->type} expiry.");
|
||||
$worker->notify(new \App\Notifications\DocumentExpiryNotification(
|
||||
ucfirst($doc->type),
|
||||
$doc->expiry_date,
|
||||
$days,
|
||||
$level
|
||||
));
|
||||
|
||||
$this->logReminder($worker, 'document_expiry', $doc, $level, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
private function checkEmployerProfiles()
|
||||
{
|
||||
$profiles = \App\Models\EmployerProfile::whereNotNull('emirates_id_expiry')->get();
|
||||
foreach ($profiles as $profile) {
|
||||
$user = \App\Models\User::find($profile->user_id);
|
||||
if (!$user) continue;
|
||||
|
||||
$expiry = Carbon::parse($profile->emirates_id_expiry);
|
||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if (!$level) continue;
|
||||
|
||||
if ($this->alreadySent($user, 'document_expiry', $profile, $level)) continue;
|
||||
|
||||
$this->info("Sending {$level} reminder to employer {$user->name} for Emirates ID expiry.");
|
||||
$user->notify(new \App\Notifications\DocumentExpiryNotification(
|
||||
'Emirates ID',
|
||||
$profile->emirates_id_expiry,
|
||||
$days,
|
||||
$level
|
||||
));
|
||||
|
||||
$this->logReminder($user, 'document_expiry', $profile, $level, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
private function checkSponsors()
|
||||
{
|
||||
$sponsors = \App\Models\Sponsor::all();
|
||||
foreach ($sponsors as $sponsor) {
|
||||
// 1. Check Emirates ID expiry
|
||||
if ($sponsor->emirates_id_expiry) {
|
||||
$expiry = Carbon::parse($sponsor->emirates_id_expiry);
|
||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if ($level && !$this->alreadySent($sponsor, 'document_expiry_emirates', $sponsor, $level)) {
|
||||
$this->info("Sending {$level} reminder to sponsor {$sponsor->full_name} for Emirates ID expiry.");
|
||||
$sponsor->notify(new \App\Notifications\DocumentExpiryNotification(
|
||||
'Emirates ID',
|
||||
$sponsor->emirates_id_expiry,
|
||||
$days,
|
||||
$level
|
||||
));
|
||||
$this->logReminder($sponsor, 'document_expiry_emirates', $sponsor, $level, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check License expiry
|
||||
if ($sponsor->license_expiry) {
|
||||
$expiry = Carbon::parse($sponsor->license_expiry);
|
||||
$days = (int) now()->startOfDay()->diffInDays($expiry->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if ($level && !$this->alreadySent($sponsor, 'document_expiry_license', $sponsor, $level)) {
|
||||
$this->info("Sending {$level} reminder to sponsor {$sponsor->full_name} for License expiry.");
|
||||
$sponsor->notify(new \App\Notifications\DocumentExpiryNotification(
|
||||
'Trade License',
|
||||
$sponsor->license_expiry->toDateString(),
|
||||
$days,
|
||||
$level
|
||||
));
|
||||
$this->logReminder($sponsor, 'document_expiry_license', $sponsor, $level, 'all');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkHireConfirmations()
|
||||
{
|
||||
$applications = \App\Models\JobApplication::where('status', 'selected')
|
||||
->with('jobPost.employer')
|
||||
->get();
|
||||
|
||||
foreach ($applications as $app) {
|
||||
$job = $app->jobPost;
|
||||
if (!$job || !$job->start_date) continue;
|
||||
$employer = $job->employer;
|
||||
if (!$employer) continue;
|
||||
$worker = $app->worker;
|
||||
if (!$worker) continue;
|
||||
|
||||
$startDate = Carbon::parse($job->start_date);
|
||||
$days = (int) now()->startOfDay()->diffInDays($startDate->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if (!$level) continue;
|
||||
|
||||
if ($this->alreadySent($employer, 'hire_confirmation', $app, $level)) continue;
|
||||
|
||||
$this->info("Sending {$level} reminder to employer {$employer->name} to confirm hiring for job {$job->title}.");
|
||||
$employer->notify(new \App\Notifications\HireConfirmationNotification(
|
||||
$job->title,
|
||||
$worker->name,
|
||||
$days,
|
||||
$level,
|
||||
$app->id
|
||||
));
|
||||
|
||||
$this->logReminder($employer, 'hire_confirmation', $app, $level, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
private function checkJoiningConfirmations()
|
||||
{
|
||||
$applications = \App\Models\JobApplication::where('status', 'hired')
|
||||
->whereNull('joining_confirmed_at')
|
||||
->with(['jobPost.employer', 'worker'])
|
||||
->get();
|
||||
|
||||
foreach ($applications as $app) {
|
||||
$job = $app->jobPost;
|
||||
if (!$job || !$job->start_date) continue;
|
||||
$worker = $app->worker;
|
||||
if (!$worker) continue;
|
||||
$employer = $job->employer;
|
||||
$employerName = $employer ? $employer->name : 'Employer';
|
||||
|
||||
$startDate = Carbon::parse($job->start_date);
|
||||
$days = (int) now()->startOfDay()->diffInDays($startDate->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if (!$level) continue;
|
||||
|
||||
if ($this->alreadySent($worker, 'joining_confirmation', $app, $level)) continue;
|
||||
|
||||
$this->info("Sending {$level} reminder to worker {$worker->name} to confirm joining for job {$job->title}.");
|
||||
$worker->notify(new \App\Notifications\JoiningConfirmationNotification(
|
||||
$job->title,
|
||||
$employerName,
|
||||
$days,
|
||||
$level,
|
||||
$app->id
|
||||
));
|
||||
|
||||
$this->logReminder($worker, 'joining_confirmation', $app, $level, 'all');
|
||||
}
|
||||
}
|
||||
|
||||
private function checkJobOffers()
|
||||
{
|
||||
$offers = \App\Models\JobOffer::where('status', 'pending')
|
||||
->with(['worker', 'employer'])
|
||||
->get();
|
||||
|
||||
foreach ($offers as $offer) {
|
||||
$worker = $offer->worker;
|
||||
if (!$worker) continue;
|
||||
$employer = $offer->employer;
|
||||
$employerName = $employer ? $employer->name : 'Employer';
|
||||
|
||||
if (!$offer->work_date) continue;
|
||||
$workDate = Carbon::parse($offer->work_date);
|
||||
$days = (int) now()->startOfDay()->diffInDays($workDate->startOfDay(), false);
|
||||
|
||||
$level = $this->getReminderLevel($days);
|
||||
if (!$level) continue;
|
||||
|
||||
if ($this->alreadySent($worker, 'pending_confirmation', $offer, $level)) continue;
|
||||
|
||||
$this->info("Sending {$level} reminder to worker {$worker->name} for pending direct offer from {$employerName}.");
|
||||
$worker->notify(new \App\Notifications\PendingConfirmationNotification(
|
||||
'accept_offer',
|
||||
"Pending Job Offer",
|
||||
"You have a pending job offer from {$employerName} starting on {$offer->work_date->toDateString()}.",
|
||||
$days,
|
||||
$level,
|
||||
get_class($offer),
|
||||
$offer->id
|
||||
));
|
||||
|
||||
$this->logReminder($worker, 'pending_confirmation', $offer, $level, 'all');
|
||||
}
|
||||
}
|
||||
}
|
||||
321
app/Http/Controllers/Api/WorkerReviewController.php
Normal file
321
app/Http/Controllers/Api/WorkerReviewController.php
Normal file
@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Worker;
|
||||
use App\Models\User;
|
||||
use App\Models\JobPost;
|
||||
use App\Models\JobApplication;
|
||||
use App\Models\JobOffer;
|
||||
use App\Models\EmployerReview;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WorkerReviewController extends Controller
|
||||
{
|
||||
/**
|
||||
* Add a new review for an employer.
|
||||
* POST /api/workers/reviews
|
||||
*/
|
||||
public function addReview(Request $request)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'employer_id' => 'required|exists:users,id',
|
||||
'job_id' => 'nullable|exists:job_posts,id',
|
||||
'rating' => 'required|integer|min:1|max:5',
|
||||
'title' => 'nullable|string|max:255',
|
||||
'comment' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$employerId = $request->employer_id;
|
||||
$jobId = $request->job_id;
|
||||
|
||||
// 1. Validate confirmed hire and completed joining, and 3-month requirement
|
||||
$hasConfirmedHire = false;
|
||||
$joiningDate = null;
|
||||
|
||||
// Check standard JobApplication
|
||||
$appQuery = JobApplication::where('worker_id', $worker->id)
|
||||
->where('status', 'hired')
|
||||
->whereNotNull('joining_confirmed_at')
|
||||
->whereHas('jobPost', function ($query) use ($employerId, $jobId) {
|
||||
$query->where('employer_id', $employerId);
|
||||
if ($jobId) {
|
||||
$query->where('id', $jobId);
|
||||
}
|
||||
});
|
||||
|
||||
$app = $appQuery->first();
|
||||
if ($app) {
|
||||
$hasConfirmedHire = true;
|
||||
$joiningDate = Carbon::parse($app->joining_confirmed_at);
|
||||
} else {
|
||||
// Check direct JobOffer
|
||||
$offerQuery = JobOffer::where('worker_id', $worker->id)
|
||||
->where('employer_id', $employerId)
|
||||
->where('status', 'accepted');
|
||||
|
||||
$offer = $offerQuery->first();
|
||||
if ($offer) {
|
||||
$hasConfirmedHire = true;
|
||||
// For direct offers, we treat work_date or updated_at as joining date
|
||||
$joiningDate = $offer->work_date ? Carbon::parse($offer->work_date) : $offer->updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasConfirmedHire) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'You can only review employers with a confirmed hire and completed joining.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Verify if completed 3 months from joining date
|
||||
if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < 3) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'You can write a review only after 3 months from the joining date.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// 2. Prevent duplicate reviews for the same job/employer combination
|
||||
$existing = EmployerReview::where('worker_id', $worker->id)
|
||||
->where('employer_id', $employerId)
|
||||
->where('job_id', $jobId)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'You have already submitted a review for this job/employer.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 3. Create the review
|
||||
$review = EmployerReview::create([
|
||||
'worker_id' => $worker->id,
|
||||
'employer_id' => $employerId,
|
||||
'job_id' => $jobId,
|
||||
'rating' => $request->rating,
|
||||
'title' => $request->title,
|
||||
'comment' => $request->comment,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Review submitted successfully.',
|
||||
'data' => [
|
||||
'review' => $review
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker Add Employer Review Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while adding the review.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit/update an existing review.
|
||||
* PUT /api/workers/reviews/{id}
|
||||
*/
|
||||
public function editReview(Request $request, $id)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'rating' => 'required|integer|min:1|max:5',
|
||||
'title' => 'nullable|string|max:255',
|
||||
'comment' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$review = EmployerReview::where('id', $id)
|
||||
->where('worker_id', $worker->id)
|
||||
->first();
|
||||
|
||||
if (!$review) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Review not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if the 7-day edit window has expired
|
||||
if ($review->created_at->addDays(7)->isPast()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'The 7-day edit window for this review has expired.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
$review->update([
|
||||
'rating' => $request->rating,
|
||||
'title' => $request->title,
|
||||
'comment' => $request->comment,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Review updated successfully.',
|
||||
'data' => [
|
||||
'review' => $review
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker Edit Employer Review Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while updating the review.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View a single review.
|
||||
* GET /api/workers/reviews/{id}
|
||||
*/
|
||||
public function viewReview(Request $request, $id)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
try {
|
||||
$review = EmployerReview::where('id', $id)
|
||||
->where('worker_id', $worker->id)
|
||||
->with(['employer.employerProfile', 'jobPost'])
|
||||
->first();
|
||||
|
||||
if (!$review) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Review not found.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$editable = !$review->created_at->addDays(7)->isPast();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'review' => [
|
||||
'id' => $review->id,
|
||||
'employer_id' => $review->employer_id,
|
||||
'employer_name' => $review->employer->name ?? 'Employer',
|
||||
'job_id' => $review->job_id,
|
||||
'job_title' => $review->jobPost->title ?? null,
|
||||
'rating' => $review->rating,
|
||||
'title' => $review->title,
|
||||
'comment' => $review->comment,
|
||||
'editable' => $editable,
|
||||
'created_at' => $review->created_at->toIso8601String(),
|
||||
'updated_at' => $review->updated_at->toIso8601String(),
|
||||
]
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker View Employer Review Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching the review.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reviews written by this worker.
|
||||
* GET /api/workers/reviews
|
||||
*/
|
||||
public function getReviews(Request $request)
|
||||
{
|
||||
/** @var Worker $worker */
|
||||
$worker = $request->attributes->get('worker');
|
||||
|
||||
try {
|
||||
$page = (int) $request->input('page', 1);
|
||||
$perPage = (int) $request->input('per_page', 15);
|
||||
|
||||
$query = EmployerReview::where('worker_id', $worker->id)
|
||||
->with(['employer.employerProfile', 'jobPost'])
|
||||
->latest();
|
||||
|
||||
$total = $query->count();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$reviews = $query->skip($offset)->take($perPage)->get()
|
||||
->map(function ($rev) {
|
||||
$editable = !$rev->created_at->addDays(7)->isPast();
|
||||
|
||||
return [
|
||||
'id' => $rev->id,
|
||||
'employer_id' => $rev->employer_id,
|
||||
'employer_name' => $rev->employer->name ?? 'Employer',
|
||||
'job_id' => $rev->job_id,
|
||||
'job_title' => $rev->jobPost->title ?? null,
|
||||
'rating' => $rev->rating,
|
||||
'title' => $rev->title,
|
||||
'comment' => $rev->comment,
|
||||
'editable' => $editable,
|
||||
'created_at' => $rev->created_at->toIso8601String(),
|
||||
'updated_at' => $rev->updated_at->toIso8601String(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'reviews' => $reviews,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int) ceil($total / $perPage)),
|
||||
]
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker Get Employer Reviews Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching reviews.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Models/EmployerReview.php
Normal file
37
app/Models/EmployerReview.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmployerReview extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'employer_reviews';
|
||||
|
||||
protected $fillable = [
|
||||
'worker_id',
|
||||
'employer_id',
|
||||
'job_id',
|
||||
'rating',
|
||||
'title',
|
||||
'comment',
|
||||
];
|
||||
|
||||
public function worker()
|
||||
{
|
||||
return $this->belongsTo(Worker::class, 'worker_id');
|
||||
}
|
||||
|
||||
public function employer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'employer_id');
|
||||
}
|
||||
|
||||
public function jobPost()
|
||||
{
|
||||
return $this->belongsTo(JobPost::class, 'job_id');
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ class JobApplication extends Model
|
||||
'status',
|
||||
'notes',
|
||||
'status_history',
|
||||
'joining_confirmed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
23
app/Models/ReminderLog.php
Normal file
23
app/Models/ReminderLog.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ReminderLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_type',
|
||||
'user_id',
|
||||
'event_type',
|
||||
'entity_type',
|
||||
'entity_id',
|
||||
'reminder_level',
|
||||
'channel',
|
||||
'sent_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sent_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@ -5,10 +5,11 @@
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class Worker extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, Notifiable;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
|
||||
35
app/Notifications/Channels/FcmChannel.php
Normal file
35
app/Notifications/Channels/FcmChannel.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\Services\FCMService;
|
||||
|
||||
class FcmChannel
|
||||
{
|
||||
/**
|
||||
* Send the given notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function send($notifiable, Notification $notification)
|
||||
{
|
||||
if (!method_exists($notification, 'toFcm')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fcmData = $notification->toFcm($notifiable);
|
||||
if (!$fcmData || empty($fcmData['token'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
FCMService::sendPushNotification(
|
||||
$fcmData['token'],
|
||||
$fcmData['title'],
|
||||
$fcmData['body'],
|
||||
$fcmData['data'] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
106
app/Notifications/DocumentExpiryNotification.php
Normal file
106
app/Notifications/DocumentExpiryNotification.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\Notifications\Channels\FcmChannel;
|
||||
|
||||
class DocumentExpiryNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $documentType;
|
||||
public $expiryDate;
|
||||
public $daysRemaining;
|
||||
public $reminderLevel;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($documentType, $expiryDate, $daysRemaining, $reminderLevel)
|
||||
{
|
||||
$this->documentType = $documentType;
|
||||
$this->expiryDate = $expiryDate;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->reminderLevel = $reminderLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*/
|
||||
public function via($notifiable): array
|
||||
{
|
||||
$channels = ['database'];
|
||||
|
||||
// Determine if email channel should be included
|
||||
$emailEnabled = true;
|
||||
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||
$emailEnabled = (bool) ($notifiable->employerProfile->email_notifications ?? true);
|
||||
}
|
||||
if ($emailEnabled && !empty($notifiable->email)) {
|
||||
$channels[] = 'mail';
|
||||
}
|
||||
|
||||
// Determine if push channel should be included
|
||||
$pushEnabled = true;
|
||||
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||
$pushEnabled = (bool) ($notifiable->employerProfile->push_notifications ?? true);
|
||||
}
|
||||
if ($pushEnabled && !empty($notifiable->fcm_token)) {
|
||||
$channels[] = FcmChannel::class;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail($notifiable): MailMessage
|
||||
{
|
||||
$name = $notifiable->name ?? $notifiable->full_name ?? 'User';
|
||||
return (new MailMessage)
|
||||
->subject("Document Expiry Alert: {$this->documentType}")
|
||||
->greeting("Hello {$name},")
|
||||
->line("Your {$this->documentType} is expiring in {$this->daysRemaining} days on {$this->expiryDate}.")
|
||||
->line("Please renew your document as soon as possible to avoid service disruption.")
|
||||
->action('Update Profile', url('/profile'))
|
||||
->line('Thank you for using our application!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification for in-app storage.
|
||||
*/
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => "Document Expiry Warning",
|
||||
'body' => "Your {$this->documentType} will expire in {$this->daysRemaining} days on {$this->expiryDate}.",
|
||||
'document_type' => $this->documentType,
|
||||
'expiry_date' => $this->expiryDate,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
'reminder_level' => $this->reminderLevel,
|
||||
'type' => 'document_expiry',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the push notification representation.
|
||||
*/
|
||||
public function toFcm($notifiable): array
|
||||
{
|
||||
return [
|
||||
'token' => $notifiable->fcm_token,
|
||||
'title' => "Document Expiry Alert",
|
||||
'body' => "Your {$this->documentType} will expire in {$this->daysRemaining} days.",
|
||||
'data' => [
|
||||
'type' => 'document_expiry',
|
||||
'document_type' => $this->documentType,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
107
app/Notifications/HireConfirmationNotification.php
Normal file
107
app/Notifications/HireConfirmationNotification.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\Notifications\Channels\FcmChannel;
|
||||
|
||||
class HireConfirmationNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $jobTitle;
|
||||
public $workerName;
|
||||
public $daysRemaining;
|
||||
public $reminderLevel;
|
||||
public $applicationId;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($jobTitle, $workerName, $daysRemaining, $reminderLevel, $applicationId)
|
||||
{
|
||||
$this->jobTitle = $jobTitle;
|
||||
$this->workerName = $workerName;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->reminderLevel = $reminderLevel;
|
||||
$this->applicationId = $applicationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*/
|
||||
public function via($notifiable): array
|
||||
{
|
||||
$channels = ['database'];
|
||||
|
||||
$emailEnabled = true;
|
||||
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||
$emailEnabled = (bool) ($notifiable->employerProfile->email_notifications ?? true);
|
||||
}
|
||||
if ($emailEnabled && !empty($notifiable->email)) {
|
||||
$channels[] = 'mail';
|
||||
}
|
||||
|
||||
$pushEnabled = true;
|
||||
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||
$pushEnabled = (bool) ($notifiable->employerProfile->push_notifications ?? true);
|
||||
}
|
||||
if ($pushEnabled && !empty($notifiable->fcm_token)) {
|
||||
$channels[] = FcmChannel::class;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail($notifiable): MailMessage
|
||||
{
|
||||
$name = $notifiable->name ?? 'Employer';
|
||||
return (new MailMessage)
|
||||
->subject("Action Required: Confirm Hiring for {$this->jobTitle}")
|
||||
->greeting("Hello {$name},")
|
||||
->line("You selected {$this->workerName} for the job: {$this->jobTitle}.")
|
||||
->line("Please confirm the hiring within {$this->daysRemaining} days before the job start date.")
|
||||
->action('Confirm Hiring Now', url("/employer/candidates"))
|
||||
->line('Thank you for using our application!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification for in-app storage.
|
||||
*/
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => "Pending Hire Confirmation",
|
||||
'body' => "Please confirm hiring {$this->workerName} for '{$this->jobTitle}' in {$this->daysRemaining} days.",
|
||||
'job_title' => $this->jobTitle,
|
||||
'worker_name' => $this->workerName,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
'reminder_level' => $this->reminderLevel,
|
||||
'application_id' => $this->applicationId,
|
||||
'type' => 'hire_confirmation',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the push notification representation.
|
||||
*/
|
||||
public function toFcm($notifiable): array
|
||||
{
|
||||
return [
|
||||
'token' => $notifiable->fcm_token,
|
||||
'title' => "Action Required: Confirm Hiring",
|
||||
'body' => "Please confirm hiring {$this->workerName} for '{$this->jobTitle}' within {$this->daysRemaining} days.",
|
||||
'data' => [
|
||||
'type' => 'hire_confirmation',
|
||||
'application_id' => $this->applicationId,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
101
app/Notifications/JoiningConfirmationNotification.php
Normal file
101
app/Notifications/JoiningConfirmationNotification.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\Notifications\Channels\FcmChannel;
|
||||
|
||||
class JoiningConfirmationNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $jobTitle;
|
||||
public $employerName;
|
||||
public $daysRemaining;
|
||||
public $reminderLevel;
|
||||
public $applicationId;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($jobTitle, $employerName, $daysRemaining, $reminderLevel, $applicationId)
|
||||
{
|
||||
$this->jobTitle = $jobTitle;
|
||||
$this->employerName = $employerName;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->reminderLevel = $reminderLevel;
|
||||
$this->applicationId = $applicationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*/
|
||||
public function via($notifiable): array
|
||||
{
|
||||
$channels = ['database'];
|
||||
|
||||
// Emails are sent to workers by default
|
||||
if (!empty($notifiable->email)) {
|
||||
$channels[] = 'mail';
|
||||
}
|
||||
|
||||
// Push notifications are sent to workers if they have an FCM token
|
||||
if (!empty($notifiable->fcm_token)) {
|
||||
$channels[] = FcmChannel::class;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail($notifiable): MailMessage
|
||||
{
|
||||
$name = $notifiable->name ?? 'Worker';
|
||||
return (new MailMessage)
|
||||
->subject("Action Required: Confirm Joining for {$this->jobTitle}")
|
||||
->greeting("Hello {$name},")
|
||||
->line("You have been hired by {$this->employerName} for the job: {$this->jobTitle}.")
|
||||
->line("Please confirm your joining within {$this->daysRemaining} days before the job start date.")
|
||||
->action('Confirm Joining Now', url("/worker/applications"))
|
||||
->line('Thank you for using our application!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification for in-app storage.
|
||||
*/
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => "Pending Joining Confirmation",
|
||||
'body' => "Please confirm you will join the job '{$this->jobTitle}' with {$this->employerName} in {$this->daysRemaining} days.",
|
||||
'job_title' => $this->jobTitle,
|
||||
'employer_name' => $this->employerName,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
'reminder_level' => $this->reminderLevel,
|
||||
'application_id' => $this->applicationId,
|
||||
'type' => 'joining_confirmation',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the push notification representation.
|
||||
*/
|
||||
public function toFcm($notifiable): array
|
||||
{
|
||||
return [
|
||||
'token' => $notifiable->fcm_token,
|
||||
'title' => "Action Required: Confirm Joining",
|
||||
'body' => "Please confirm your joining for '{$this->jobTitle}' within {$this->daysRemaining} days.",
|
||||
'data' => [
|
||||
'type' => 'joining_confirmation',
|
||||
'application_id' => $this->applicationId,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
114
app/Notifications/PendingConfirmationNotification.php
Normal file
114
app/Notifications/PendingConfirmationNotification.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\Notifications\Channels\FcmChannel;
|
||||
|
||||
class PendingConfirmationNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $actionType;
|
||||
public $messageTitle;
|
||||
public $messageBody;
|
||||
public $daysRemaining;
|
||||
public $reminderLevel;
|
||||
public $entityType;
|
||||
public $entityId;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct($actionType, $messageTitle, $messageBody, $daysRemaining, $reminderLevel, $entityType, $entityId)
|
||||
{
|
||||
$this->actionType = $actionType;
|
||||
$this->messageTitle = $messageTitle;
|
||||
$this->messageBody = $messageBody;
|
||||
$this->daysRemaining = $daysRemaining;
|
||||
$this->reminderLevel = $reminderLevel;
|
||||
$this->entityType = $entityType;
|
||||
$this->entityId = $entityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*/
|
||||
public function via($notifiable): array
|
||||
{
|
||||
$channels = ['database'];
|
||||
|
||||
// Determine if email channel should be included
|
||||
$emailEnabled = true;
|
||||
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||
$emailEnabled = (bool) ($notifiable->employerProfile->email_notifications ?? true);
|
||||
}
|
||||
if ($emailEnabled && !empty($notifiable->email)) {
|
||||
$channels[] = 'mail';
|
||||
}
|
||||
|
||||
// Determine if push channel should be included
|
||||
$pushEnabled = true;
|
||||
if (method_exists($notifiable, 'employerProfile') && $notifiable->employerProfile) {
|
||||
$pushEnabled = (bool) ($notifiable->employerProfile->push_notifications ?? true);
|
||||
}
|
||||
if ($pushEnabled && !empty($notifiable->fcm_token)) {
|
||||
$channels[] = FcmChannel::class;
|
||||
}
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail($notifiable): MailMessage
|
||||
{
|
||||
$name = $notifiable->name ?? $notifiable->full_name ?? 'User';
|
||||
return (new MailMessage)
|
||||
->subject("Pending Action: {$this->messageTitle}")
|
||||
->greeting("Hello {$name},")
|
||||
->line($this->messageBody)
|
||||
->line("This action is pending and needs your confirmation within {$this->daysRemaining} days.")
|
||||
->action('View Actions', url('/dashboard'))
|
||||
->line('Thank you for using our application!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification for in-app storage.
|
||||
*/
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->messageTitle,
|
||||
'body' => $this->messageBody,
|
||||
'action_type' => $this->actionType,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
'reminder_level' => $this->reminderLevel,
|
||||
'entity_type' => $this->entityType,
|
||||
'entity_id' => $this->entityId,
|
||||
'type' => 'pending_confirmation',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the push notification representation.
|
||||
*/
|
||||
public function toFcm($notifiable): array
|
||||
{
|
||||
return [
|
||||
'token' => $notifiable->fcm_token,
|
||||
'title' => $this->messageTitle,
|
||||
'body' => $this->messageBody,
|
||||
'data' => [
|
||||
'type' => 'pending_confirmation',
|
||||
'action_type' => $this->actionType,
|
||||
'entity_id' => $this->entityId,
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?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
|
||||
{
|
||||
// 1. Laravel default notifications table for in-app notifications
|
||||
if (!Schema::hasTable('notifications')) {
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
$table->morphs('notifiable');
|
||||
$table->text('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Add joining_confirmed_at to job_applications
|
||||
if (Schema::hasTable('job_applications') && !Schema::hasColumn('job_applications', 'joining_confirmed_at')) {
|
||||
Schema::table('job_applications', function (Blueprint $table) {
|
||||
$table->timestamp('joining_confirmed_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Create reminder_logs table to track and prevent duplicate reminders
|
||||
if (!Schema::hasTable('reminder_logs')) {
|
||||
Schema::create('reminder_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('user_type');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->string('event_type');
|
||||
$table->string('entity_type');
|
||||
$table->unsignedBigInteger('entity_id');
|
||||
$table->string('reminder_level');
|
||||
$table->string('channel');
|
||||
$table->timestamp('sent_at')->useCurrent();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['entity_type', 'entity_id', 'event_type', 'reminder_level'], 'idx_reminder_uniqueness');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('reminder_logs');
|
||||
if (Schema::hasTable('job_applications') && Schema::hasColumn('job_applications', 'joining_confirmed_at')) {
|
||||
Schema::table('job_applications', function (Blueprint $table) {
|
||||
$table->dropColumn('joining_confirmed_at');
|
||||
});
|
||||
}
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?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
|
||||
{
|
||||
if (!Schema::hasTable('employer_reviews')) {
|
||||
Schema::create('employer_reviews', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('worker_id')->constrained('workers')->onDelete('cascade');
|
||||
$table->foreignId('employer_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('job_id')->nullable()->constrained('job_posts')->onDelete('set null');
|
||||
$table->integer('rating');
|
||||
$table->string('title')->nullable();
|
||||
$table->text('comment')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Unique index to prevent duplicate reviews for the same job/employer combination
|
||||
$table->unique(['worker_id', 'employer_id', 'job_id'], 'idx_worker_employer_job_unique');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('employer_reviews');
|
||||
}
|
||||
};
|
||||
@ -3312,6 +3312,185 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/reviews": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Reviews"
|
||||
],
|
||||
"summary": "List Worker Reviews",
|
||||
"description": "Retrieves a paginated list of all reviews submitted by the authenticated worker.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 15
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Reviews retrieved successfully."
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Worker/Reviews"
|
||||
],
|
||||
"summary": "Submit Review for Employer",
|
||||
"description": "Submits a review for an employer. Eligible only after 3 months from joining, for a confirmed hire.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"employer_id",
|
||||
"rating",
|
||||
"comment"
|
||||
],
|
||||
"properties": {
|
||||
"employer_id": {
|
||||
"type": "integer",
|
||||
"example": 5,
|
||||
"description": "The ID of the employer (User ID)."
|
||||
},
|
||||
"job_id": {
|
||||
"type": "integer",
|
||||
"example": 2,
|
||||
"description": "Optional Job ID associated with the review."
|
||||
},
|
||||
"rating": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"example": 5,
|
||||
"description": "Rating out of 5 stars."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Great workplace",
|
||||
"description": "Optional review title."
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"example": "Highly supportive team and on-time salary payments.",
|
||||
"description": "Review comment."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Review submitted successfully."
|
||||
},
|
||||
"403": {
|
||||
"description": "Eligibility check failed (no confirmed hire or under 3 months since joining)."
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error or duplicate review."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/reviews/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Worker/Reviews"
|
||||
],
|
||||
"summary": "Get Review Details",
|
||||
"description": "Retrieves the details of a specific review submitted by the worker.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Review details retrieved successfully."
|
||||
},
|
||||
"404": {
|
||||
"description": "Review not found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Worker/Reviews"
|
||||
],
|
||||
"summary": "Edit Submitted Review",
|
||||
"description": "Updates a review. Permitted only within a 7-day edit window from the initial submission date.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rating",
|
||||
"comment"
|
||||
],
|
||||
"properties": {
|
||||
"rating": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"example": 4
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Good workplace"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string",
|
||||
"example": "Updating my comment with details."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Review edited successfully."
|
||||
},
|
||||
"403": {
|
||||
"description": "Edit option disabled because the 7-day window expired."
|
||||
},
|
||||
"404": {
|
||||
"description": "Review not found."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
||||
@ -111,6 +111,12 @@
|
||||
Route::get('/workers/applied-jobs', [\App\Http\Controllers\Api\WorkerJobController::class, 'appliedJobs']);
|
||||
Route::get('/workers/applied-jobs/{id}', [\App\Http\Controllers\Api\WorkerJobController::class, 'appliedJobDetail']);
|
||||
Route::post('/workers/applied-jobs/{id}/withdraw', [\App\Http\Controllers\Api\WorkerJobController::class, 'withdrawApplication']);
|
||||
|
||||
// Review Management (Worker reviewing Employer)
|
||||
Route::get('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'getReviews']);
|
||||
Route::post('/workers/reviews', [\App\Http\Controllers\Api\WorkerReviewController::class, 'addReview']);
|
||||
Route::get('/workers/reviews/{id}', [\App\Http\Controllers\Api\WorkerReviewController::class, 'viewReview']);
|
||||
Route::put('/workers/reviews/{id}', [\App\Http\Controllers\Api\WorkerReviewController::class, 'editReview']);
|
||||
});
|
||||
|
||||
// Protected Employer Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
|
||||
@ -2,7 +2,10 @@
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('app:send-reminders')->daily();
|
||||
|
||||
281
tests/Feature/AutomatedReminderTest.php
Normal file
281
tests/Feature/AutomatedReminderTest.php
Normal file
@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
use App\Models\Sponsor;
|
||||
use App\Models\WorkerDocument;
|
||||
use App\Models\EmployerProfile;
|
||||
use App\Models\JobPost;
|
||||
use App\Models\JobApplication;
|
||||
use App\Models\JobOffer;
|
||||
use App\Models\ReminderLog;
|
||||
use App\Notifications\DocumentExpiryNotification;
|
||||
use App\Notifications\HireConfirmationNotification;
|
||||
use App\Notifications\JoiningConfirmationNotification;
|
||||
use App\Notifications\PendingConfirmationNotification;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AutomatedReminderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function createTestWorker(array $overrides = []): Worker
|
||||
{
|
||||
return Worker::create(array_merge([
|
||||
'name' => 'John Worker',
|
||||
'email' => 'worker@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'phone' => '971500000001',
|
||||
'nationality' => 'Indian',
|
||||
'status' => 'active',
|
||||
'age' => 25,
|
||||
'salary' => 2000,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '2 Years',
|
||||
'religion' => 'Christian',
|
||||
'bio' => 'Helper info',
|
||||
'passport_status' => 'valid',
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
public function test_document_expiry_reminders()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
// 1. Create a Worker with a document expiring in exactly 7 days
|
||||
$worker = $this->createTestWorker();
|
||||
|
||||
$doc = WorkerDocument::create([
|
||||
'worker_id' => $worker->id,
|
||||
'type' => 'passport',
|
||||
'number' => 'PASS123',
|
||||
'expiry_date' => now()->addDays(7)->toDateString(),
|
||||
]);
|
||||
|
||||
// 2. Create an Employer (User) with Emirates ID expiring in exactly 3 days
|
||||
$employer = User::create([
|
||||
'name' => 'Alice Employer',
|
||||
'email' => 'employer@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
|
||||
$employerProfile = EmployerProfile::create([
|
||||
'user_id' => $employer->id,
|
||||
'phone' => '971500000002',
|
||||
'address' => 'Dubai',
|
||||
'emirates_id_expiry' => now()->addDays(3)->toDateString(),
|
||||
]);
|
||||
|
||||
// 3. Create a Sponsor with Emirates ID expiring in exactly 1 day
|
||||
$sponsor = Sponsor::create([
|
||||
'full_name' => 'Sponsor Bob',
|
||||
'email' => 'sponsor@example.com',
|
||||
'mobile' => '971500000003',
|
||||
'password' => bcrypt('password'),
|
||||
'emirates_id_expiry' => now()->addDays(1)->toDateString(),
|
||||
]);
|
||||
|
||||
// Run the command
|
||||
Artisan::call('app:send-reminders');
|
||||
|
||||
// Assert Worker got Passport expiry 7 days notification
|
||||
Notification::assertSentTo(
|
||||
$worker,
|
||||
DocumentExpiryNotification::class,
|
||||
function ($notification) {
|
||||
return $notification->documentType === 'Passport' &&
|
||||
$notification->daysRemaining === 7 &&
|
||||
$notification->reminderLevel === '7_days';
|
||||
}
|
||||
);
|
||||
|
||||
// Assert Employer got Emirates ID expiry 3 days notification
|
||||
Notification::assertSentTo(
|
||||
$employer,
|
||||
DocumentExpiryNotification::class,
|
||||
function ($notification) {
|
||||
return $notification->documentType === 'Emirates ID' &&
|
||||
$notification->daysRemaining === 3 &&
|
||||
$notification->reminderLevel === '3_days';
|
||||
}
|
||||
);
|
||||
|
||||
// Assert Sponsor got Emirates ID expiry 1 day notification
|
||||
Notification::assertSentTo(
|
||||
$sponsor,
|
||||
DocumentExpiryNotification::class,
|
||||
function ($notification) {
|
||||
return $notification->documentType === 'Emirates ID' &&
|
||||
$notification->daysRemaining === 1 &&
|
||||
$notification->reminderLevel === '1_day';
|
||||
}
|
||||
);
|
||||
|
||||
// Assert reminder logs were created
|
||||
$this->assertDatabaseHas('reminder_logs', [
|
||||
'user_type' => get_class($worker),
|
||||
'user_id' => $worker->id,
|
||||
'event_type' => 'document_expiry',
|
||||
'reminder_level' => '7_days',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('reminder_logs', [
|
||||
'user_type' => get_class($employer),
|
||||
'user_id' => $employer->id,
|
||||
'event_type' => 'document_expiry',
|
||||
'reminder_level' => '3_days',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('reminder_logs', [
|
||||
'user_type' => get_class($sponsor),
|
||||
'user_id' => $sponsor->id,
|
||||
'event_type' => 'document_expiry_emirates',
|
||||
'reminder_level' => '1_day',
|
||||
]);
|
||||
|
||||
// Run the command again to ensure duplicate is prevented
|
||||
Notification::fake();
|
||||
Artisan::call('app:send-reminders');
|
||||
|
||||
Notification::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_hire_and_joining_confirmations_reminders()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
// Create Employer
|
||||
$employer = User::create([
|
||||
'name' => 'Alice Employer',
|
||||
'email' => 'employer@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
|
||||
// Create Worker
|
||||
$worker = $this->createTestWorker();
|
||||
|
||||
// Create a Job Post starting in 7 days
|
||||
$job = JobPost::create([
|
||||
'employer_id' => $employer->id,
|
||||
'title' => 'Housekeeper',
|
||||
'location' => 'Dubai',
|
||||
'salary' => 2000,
|
||||
'workers_needed' => 1,
|
||||
'job_type' => 'Full Time',
|
||||
'start_date' => now()->addDays(7)->toDateString(),
|
||||
'description' => 'Test job',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
// Create a Job Application with status 'selected' (needs Hire Confirmation)
|
||||
$appSelected = JobApplication::create([
|
||||
'job_id' => $job->id,
|
||||
'worker_id' => $worker->id,
|
||||
'status' => 'selected',
|
||||
]);
|
||||
|
||||
// Create another Job Post starting in 3 days
|
||||
$jobHired = JobPost::create([
|
||||
'employer_id' => $employer->id,
|
||||
'title' => 'Nanny',
|
||||
'location' => 'Dubai',
|
||||
'salary' => 2500,
|
||||
'workers_needed' => 1,
|
||||
'job_type' => 'Full Time',
|
||||
'start_date' => now()->addDays(3)->toDateString(),
|
||||
'description' => 'Test job hired',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
// Create a Job Application with status 'hired' (needs Joining Confirmation)
|
||||
$appHired = JobApplication::create([
|
||||
'job_id' => $jobHired->id,
|
||||
'worker_id' => $worker->id,
|
||||
'status' => 'hired',
|
||||
]);
|
||||
|
||||
// Run the command
|
||||
Artisan::call('app:send-reminders');
|
||||
|
||||
// Assert Employer got Hire Confirmation 7 days notification
|
||||
Notification::assertSentTo(
|
||||
$employer,
|
||||
HireConfirmationNotification::class,
|
||||
function ($notification) use ($appSelected) {
|
||||
return $notification->jobTitle === 'Housekeeper' &&
|
||||
$notification->workerName === 'John Worker' &&
|
||||
$notification->daysRemaining === 7 &&
|
||||
$notification->applicationId === $appSelected->id;
|
||||
}
|
||||
);
|
||||
|
||||
// Assert Worker got Joining Confirmation 3 days notification
|
||||
Notification::assertSentTo(
|
||||
$worker,
|
||||
JoiningConfirmationNotification::class,
|
||||
function ($notification) use ($appHired) {
|
||||
return $notification->jobTitle === 'Nanny' &&
|
||||
$notification->employerName === 'Alice Employer' &&
|
||||
$notification->daysRemaining === 3 &&
|
||||
$notification->applicationId === $appHired->id;
|
||||
}
|
||||
);
|
||||
|
||||
// Verify duplicate prevention
|
||||
Notification::fake();
|
||||
Artisan::call('app:send-reminders');
|
||||
Notification::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_pending_direct_job_offer_reminders()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
// Create Employer
|
||||
$employer = User::create([
|
||||
'name' => 'Alice Employer',
|
||||
'email' => 'employer@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
|
||||
// Create Worker
|
||||
$worker = $this->createTestWorker();
|
||||
|
||||
// Create pending direct Job Offer starting in 1 day
|
||||
$offer = JobOffer::create([
|
||||
'employer_id' => $employer->id,
|
||||
'worker_id' => $worker->id,
|
||||
'work_date' => now()->addDays(1)->toDateString(),
|
||||
'location' => 'Dubai Marina',
|
||||
'salary' => 3000,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
// Run the command
|
||||
Artisan::call('app:send-reminders');
|
||||
|
||||
// Assert Worker got Pending Direct Job Offer 1 day notification
|
||||
Notification::assertSentTo(
|
||||
$worker,
|
||||
PendingConfirmationNotification::class,
|
||||
function ($notification) use ($offer) {
|
||||
return $notification->actionType === 'accept_offer' &&
|
||||
$notification->daysRemaining === 1 &&
|
||||
$notification->entityId === $offer->id;
|
||||
}
|
||||
);
|
||||
|
||||
// Verify duplicate prevention
|
||||
Notification::fake();
|
||||
Artisan::call('app:send-reminders');
|
||||
Notification::assertNothingSent();
|
||||
}
|
||||
}
|
||||
271
tests/Feature/WorkerReviewTest.php
Normal file
271
tests/Feature/WorkerReviewTest.php
Normal file
@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Worker;
|
||||
use App\Models\JobPost;
|
||||
use App\Models\JobApplication;
|
||||
use App\Models\EmployerReview;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WorkerReviewTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $employer;
|
||||
protected $worker;
|
||||
protected $job;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create employer
|
||||
$this->employer = User::create([
|
||||
'name' => 'Test Employer',
|
||||
'email' => 'employer@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'role' => 'employer',
|
||||
]);
|
||||
|
||||
// Create worker
|
||||
$this->worker = Worker::create([
|
||||
'name' => 'Worker User',
|
||||
'email' => 'worker@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
'phone' => '971501234567',
|
||||
'nationality' => 'Indian',
|
||||
'status' => 'active',
|
||||
'api_token' => 'worker_api_token',
|
||||
'age' => 25,
|
||||
'salary' => 2000,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => '2 Years',
|
||||
'religion' => 'Christian',
|
||||
'bio' => 'Helper info',
|
||||
'passport_status' => 'valid',
|
||||
]);
|
||||
|
||||
// Create job post
|
||||
$this->job = JobPost::create([
|
||||
'employer_id' => $this->employer->id,
|
||||
'title' => 'Test Job',
|
||||
'location' => 'Dubai',
|
||||
'salary' => 2500,
|
||||
'workers_needed' => 1,
|
||||
'job_type' => 'Full Time',
|
||||
'start_date' => now()->subMonths(4)->toDateString(),
|
||||
'description' => 'Test Job Description',
|
||||
'status' => 'active',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_cannot_review_without_confirmed_hire_and_joining()
|
||||
{
|
||||
// Act: Try to review before any job application is made
|
||||
$response = $this->postJson('/api/workers/reviews', [
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 5,
|
||||
'comment' => 'Great employer!',
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertJsonPath('success', false);
|
||||
$response->assertJsonPath('message', 'You can only review employers with a confirmed hire and completed joining.');
|
||||
}
|
||||
|
||||
public function test_cannot_review_before_3_months_since_joining()
|
||||
{
|
||||
// Setup job application with hired status but joining_confirmed_at is only 2 months ago
|
||||
JobApplication::create([
|
||||
'job_id' => $this->job->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
'status' => 'hired',
|
||||
'joining_confirmed_at' => now()->subMonths(2)->toDateString(),
|
||||
]);
|
||||
|
||||
// Act: Try to review
|
||||
$response = $this->postJson('/api/workers/reviews', [
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 5,
|
||||
'comment' => 'Great employer!',
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertJsonPath('success', false);
|
||||
$response->assertJsonPath('message', 'You can write a review only after 3 months from the joining date.');
|
||||
}
|
||||
|
||||
public function test_can_review_after_3_months_since_joining()
|
||||
{
|
||||
// Setup job application with hired status and joining_confirmed_at 3.5 months ago
|
||||
JobApplication::create([
|
||||
'job_id' => $this->job->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
'status' => 'hired',
|
||||
'joining_confirmed_at' => now()->subMonths(3)->subDays(15)->toDateString(),
|
||||
]);
|
||||
|
||||
// Act: Post review
|
||||
$response = $this->postJson('/api/workers/reviews', [
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 4,
|
||||
'title' => 'Fair Employer',
|
||||
'comment' => 'Very good experience overall.',
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonPath('success', true);
|
||||
$response->assertJsonPath('data.review.rating', 4);
|
||||
$response->assertJsonPath('data.review.title', 'Fair Employer');
|
||||
$response->assertJsonPath('data.review.comment', 'Very good experience overall.');
|
||||
|
||||
$this->assertDatabaseHas('employer_reviews', [
|
||||
'worker_id' => $this->worker->id,
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 4,
|
||||
'title' => 'Fair Employer',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_cannot_duplicate_reviews()
|
||||
{
|
||||
// Setup job application
|
||||
JobApplication::create([
|
||||
'job_id' => $this->job->id,
|
||||
'worker_id' => $this->worker->id,
|
||||
'status' => 'hired',
|
||||
'joining_confirmed_at' => now()->subMonths(4)->toDateString(),
|
||||
]);
|
||||
|
||||
// Submit first review
|
||||
EmployerReview::create([
|
||||
'worker_id' => $this->worker->id,
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 5,
|
||||
'comment' => 'Nice',
|
||||
]);
|
||||
|
||||
// Act: Try to submit duplicate review
|
||||
$response = $this->postJson('/api/workers/reviews', [
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 4,
|
||||
'comment' => 'Another review',
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonPath('success', false);
|
||||
$response->assertJsonPath('message', 'You have already submitted a review for this job/employer.');
|
||||
}
|
||||
|
||||
public function test_edit_review_within_7_days()
|
||||
{
|
||||
// Setup existing review created now
|
||||
$review = EmployerReview::create([
|
||||
'worker_id' => $this->worker->id,
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 3,
|
||||
'title' => 'Initial Title',
|
||||
'comment' => 'Initial Comment',
|
||||
]);
|
||||
|
||||
// Act: Edit the review
|
||||
$response = $this->putJson("/api/workers/reviews/{$review->id}", [
|
||||
'rating' => 5,
|
||||
'title' => 'Updated Title',
|
||||
'comment' => 'Updated Comment Details',
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJsonPath('success', true);
|
||||
$response->assertJsonPath('data.review.rating', 5);
|
||||
$response->assertJsonPath('data.review.title', 'Updated Title');
|
||||
$response->assertJsonPath('data.review.comment', 'Updated Comment Details');
|
||||
|
||||
$this->assertDatabaseHas('employer_reviews', [
|
||||
'id' => $review->id,
|
||||
'rating' => 5,
|
||||
'title' => 'Updated Title',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_cannot_edit_review_after_7_days()
|
||||
{
|
||||
// Setup existing review created 8 days ago
|
||||
$review = EmployerReview::create([
|
||||
'worker_id' => $this->worker->id,
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 3,
|
||||
'comment' => 'Initial Comment',
|
||||
]);
|
||||
|
||||
// Force creation date to be 8 days ago
|
||||
$review->created_at = now()->subDays(8);
|
||||
$review->save();
|
||||
|
||||
// Act: Attempt to edit
|
||||
$response = $this->putJson("/api/workers/reviews/{$review->id}", [
|
||||
'rating' => 5,
|
||||
'comment' => 'Attempted Edit',
|
||||
], [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertJsonPath('success', false);
|
||||
$response->assertJsonPath('message', 'The 7-day edit window for this review has expired.');
|
||||
}
|
||||
|
||||
public function test_view_and_list_reviews()
|
||||
{
|
||||
// Create a review
|
||||
$review = EmployerReview::create([
|
||||
'worker_id' => $this->worker->id,
|
||||
'employer_id' => $this->employer->id,
|
||||
'job_id' => $this->job->id,
|
||||
'rating' => 5,
|
||||
'title' => 'Awesome place',
|
||||
'comment' => 'Had a wonderful time.',
|
||||
]);
|
||||
|
||||
// 1. Get Single Review
|
||||
$response = $this->getJson("/api/workers/reviews/{$review->id}", [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJsonPath('success', true);
|
||||
$response->assertJsonPath('data.review.title', 'Awesome place');
|
||||
$response->assertJsonPath('data.review.rating', 5);
|
||||
|
||||
// 2. List Reviews
|
||||
$response = $this->getJson('/api/workers/reviews', [
|
||||
'Authorization' => 'Bearer worker_api_token'
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertJsonPath('success', true);
|
||||
$response->assertJsonCount(1, 'data.reviews');
|
||||
$response->assertJsonPath('data.reviews.0.title', 'Awesome place');
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user