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('7. Checking Worker No Response...'); $this->checkWorkerNoResponse(); $this->info('8. Checking Review Reminders...'); $this->checkReviewReminders(); $this->info('Automated reminder system run complete!'); } private function getReminderLevel(int $days, string $configKey, string $default): ?string { $value = config($configKey, $default); $configuredDays = array_map('intval', array_filter(explode(',', $value), 'strlen')); if (in_array($days, $configuredDays)) { return $days === 1 ? '1_day' : "{$days}_days"; } 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, 'reminders.document_expiry_reminder_days', '30,7,3,1'); 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, 'reminders.document_expiry_reminder_days', '30,7,3,1'); 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, 'reminders.document_expiry_reminder_days', '30,7,3,1'); 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, 'reminders.document_expiry_reminder_days', '30,7,3,1'); 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, 'reminders.employer_hire_confirm_reminder_days', '3,7'); 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, 'reminders.worker_join_confirm_reminder_days', '3,7'); 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, 'reminders.worker_join_confirm_reminder_days', '3,7'); 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'); } } private function checkWorkerNoResponse() { $noResponseDays = (int) config('reminders.worker_no_response_reminder_days', 14); // 1. Pending direct Job Offers $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'; $daysSinceCreation = (int) Carbon::parse($offer->created_at)->startOfDay()->diffInDays(now()->startOfDay(), false); if ($daysSinceCreation !== $noResponseDays) continue; $level = $daysSinceCreation === 1 ? '1_day' : "{$daysSinceCreation}_days"; if ($this->alreadySent($worker, 'worker_no_response_offer', $offer, $level)) continue; $this->info("Sending no-response reminder to worker {$worker->name} for pending direct offer from {$employerName}."); $worker->notify(new \App\Notifications\WorkerNoResponseNotification( 'worker_no_response_offer', "Action Required: Pending Job Offer", "You have a pending job offer from {$employerName} sent on {$offer->created_at->toDateString()} that you have not responded to.", $daysSinceCreation, get_class($offer), $offer->id )); $this->logReminder($worker, 'worker_no_response_offer', $offer, $level, 'all'); } // 2. Chat conversations where the last message is from the employer $conversations = \App\Models\Conversation::with(['employer', 'worker'])->get(); foreach ($conversations as $conv) { $worker = $conv->worker; if (!$worker) continue; $employer = $conv->employer; $employerName = $employer ? $employer->name : 'Employer'; $lastMessage = $conv->messages()->latest()->first(); if (!$lastMessage) continue; $isEmployerSender = false; if ($lastMessage->sender_type === 'App\Models\User' || $lastMessage->sender_id == $conv->employer_id) { $isEmployerSender = true; } if (!$isEmployerSender) continue; $daysSinceMessage = (int) Carbon::parse($lastMessage->created_at)->startOfDay()->diffInDays(now()->startOfDay(), false); if ($daysSinceMessage !== $noResponseDays) continue; $level = $daysSinceMessage === 1 ? '1_day' : "{$daysSinceMessage}_days"; if ($this->alreadySent($worker, 'worker_no_response_chat', $lastMessage, $level)) continue; $this->info("Sending no-response chat reminder to worker {$worker->name} for conversation with {$employerName}."); $worker->notify(new \App\Notifications\WorkerNoResponseNotification( 'worker_no_response_chat', "New Messages: Reply to {$employerName}", "You have a message from {$employerName} that you have not responded to for {$daysSinceMessage} days.", $daysSinceMessage, get_class($lastMessage), $lastMessage->id )); $this->logReminder($worker, 'worker_no_response_chat', $lastMessage, $level, 'all'); } } private function checkReviewReminders() { $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); $eligibilityMinutes = config('reminders.review_eligibility_minutes'); $isMinutes = ($eligibilityMinutes !== null && $eligibilityMinutes > 0); $level = $isMinutes ? "{$eligibilityMinutes}_minutes" : "{$eligibilityMonths}_months"; // 1. Standard Job Applications with status 'hired' $applications = \App\Models\JobApplication::where('status', 'hired') ->whereNotNull('joining_confirmed_at') ->with(['jobPost.employer', 'worker']) ->get(); foreach ($applications as $app) { $job = $app->jobPost; if (!$job) continue; $employer = $job->employer; $worker = $app->worker; if (!$employer || !$worker) continue; $joiningDate = Carbon::parse($app->joining_confirmed_at); $eligible = false; if ($isMinutes) { $eligible = $joiningDate->diffInMinutes(now(), false) >= $eligibilityMinutes; } else { $months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false); $eligible = $months >= $eligibilityMonths; } if ($eligible) { // Remind worker to review employer if (!$this->alreadySent($worker, 'review_employer_reminder', $app, $level)) { $this->info("Sending review reminder to worker {$worker->name} to review employer {$employer->name}."); $worker->notify(new \App\Notifications\WorkerReviewReminderNotification( $employer->name, $app->id, $level )); $this->logReminder($worker, 'review_employer_reminder', $app, $level, 'all'); } // Remind employer to review worker if (!$this->alreadySent($employer, 'review_worker_reminder', $app, $level)) { $this->info("Sending review reminder to employer {$employer->name} to review worker {$worker->name}."); $employer->notify(new \App\Notifications\EmployerReviewReminderNotification( $worker->name, $app->id, $level )); $this->logReminder($employer, 'review_worker_reminder', $app, $level, 'all'); } } } // 2. Direct Job Offers with status 'accepted' $offers = \App\Models\JobOffer::where('status', 'accepted') ->with(['worker', 'employer']) ->get(); foreach ($offers as $offer) { $worker = $offer->worker; $employer = $offer->employer; if (!$worker || !$employer) continue; $joiningDate = $offer->work_date ? Carbon::parse($offer->work_date) : $offer->updated_at; $eligible = false; if ($isMinutes) { $eligible = $joiningDate->diffInMinutes(now(), false) >= $eligibilityMinutes; } else { $months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false); $eligible = $months >= $eligibilityMonths; } if ($eligible) { // Remind worker to review employer if (!$this->alreadySent($worker, 'review_employer_reminder', $offer, $level)) { $this->info("Sending review reminder to worker {$worker->name} to review employer {$employer->name} (direct offer)."); $worker->notify(new \App\Notifications\WorkerReviewReminderNotification( $employer->name, $offer->id, $level )); $this->logReminder($worker, 'review_employer_reminder', $offer, $level, 'all'); } // Remind employer to review worker if (!$this->alreadySent($employer, 'review_worker_reminder', $offer, $level)) { $this->info("Sending review reminder to employer {$employer->name} to review worker {$worker->name} (direct offer)."); $employer->notify(new \App\Notifications\EmployerReviewReminderNotification( $worker->name, $offer->id, $level )); $this->logReminder($employer, 'review_worker_reminder', $offer, $level, 'all'); } } } } }