option('record_id'); $overrideEmail = $this->option('email'); $isTestMode = !empty($recordId) || !empty($overrideEmail); if ($isTestMode && empty($recordId)) { $this->error('When using --email for testing, --record_id is required.'); return 1; } if (!Schema::hasTable('neblogistic_status_email_log') && !$isTestMode) { $this->error('Missing table neblogistic_status_email_log. Run migrations first.'); return 1; } if (!empty($recordId)) { $record = DB::table('neblogistic')->where('id', $recordId)->first(); if (!$record) { $this->error("Record not found for ID: {$recordId}"); return 1; } if (!in_array($record->status, ['Dispatched', 'Delivered'], true)) { $this->warn("Record ID {$recordId} status is '{$record->status}' (expected Dispatched or Delivered). Sending anyway for test."); } $this->info("Test mode: record_id={$recordId}" . ($overrideEmail ? ", email={$overrideEmail}" : '')); return $this->sendStatusEmail($record, $overrideEmail, true) ? 0 : 1; } $records = DB::table('neblogistic') ->whereIn('status', ['Dispatched', 'Delivered']) ->where(function ($query) { $query->whereNull('status_email_sent') ->orWhereColumn('status_email_sent', '!=', 'status'); }) ->orderBy('updated_at', 'asc') ->limit(20) ->get(); if ($records->isEmpty()) { $this->info('No Dispatched/Delivered records pending status email.'); return 0; } $sent = 0; $failed = 0; $skipped = 0; foreach ($records as $record) { $result = $this->sendStatusEmail($record); if ($result === true) { $sent++; } elseif ($result === null) { $skipped++; } else { $failed++; } } $this->info("Done. Sent: {$sent}, failed: {$failed}, skipped(already attempted): {$skipped}."); return 0; } /** * @param object $record * @param string|null $overrideEmail * @param bool $isTestMode * @return bool|null true=at least one new send succeeded, false=attempted with failure(s), null=nothing new to send */ protected function sendStatusEmail($record, $overrideEmail = null, $isTestMode = false) { $tmEmail = $overrideEmail ?: ($record->emp_email ?? ($record->tm_email ?? null)); $abmEmail = $overrideEmail ?: ($record->abm_email ?? null); $tmEmail = $this->normalizeEmail($tmEmail); $abmEmail = $this->normalizeEmail($abmEmail); $recipients = []; if (!empty($overrideEmail)) { $recipients[] = ['email' => $tmEmail, 'type' => 'tm']; $recipients[] = ['email' => $abmEmail, 'type' => 'abm']; } else { if (!empty($tmEmail)) { $recipients[] = ['email' => $tmEmail, 'type' => 'tm']; } if (!empty($abmEmail)) { $recipients[] = ['email' => $abmEmail, 'type' => 'abm']; } } if (count($recipients) === 0) { $this->warn("Skipping record ID {$record->id}: no TM or ABM email."); Log::warning('Neblogistic status email skipped: no recipients', [ 'id' => $record->id, 'status' => $record->status, ]); if (!$isTestMode) { $this->markStatusEmailSent($record); } return null; } $emailData = [ 'id' => $record->id, 'status' => $record->status, 'order_no' => $record->order_no ?? null, 'invoice_no' => $record->invoice_no ?? null, 'courier_name' => $record->courier_name ?? null, 'lr_no' => $record->lr_no ?? null, 'lr_date' => !empty($record->lr_date) ? date('d M Y', strtotime($record->lr_date)) : null, 'received_date' => $record->received_date ?? null, 'doctor_name' => $record->doctor_name ?? null, 'msl_code' => $record->msl_code ?? null, 'emp_name' => $record->emp_name ?? null, 'abm_name' => $record->abm_name ?? null, ]; $newSuccess = 0; $newFailed = 0; $alreadyAttempted = 0; foreach ($recipients as $recipient) { $email = $recipient['email']; $type = $recipient['type']; if (!$isTestMode && $this->alreadyAttempted($record->id, $record->status, $email, $type)) { $alreadyAttempted++; $this->line("Skip {$type} {$email} for record ID {$record->id} ({$record->status}): already attempted."); continue; } if (!$isTestMode && !$this->claimAttempt($record->id, $record->status, $email, $type)) { // Another worker claimed it (unique key race) or claim failed. $alreadyAttempted++; $this->line("Skip {$type} {$email} for record ID {$record->id} ({$record->status}): claim conflict."); continue; } try { Mail::to($email)->send(new NeblogisticStatusUpdate($emailData, $type)); $this->info("Status email sent to {$type} {$email} for record ID {$record->id} ({$record->status})"); if (!$isTestMode) { $this->markAttemptResult($record->id, $record->status, $email, $type, true, null); } $newSuccess++; } catch (\Exception $e) { $this->error("Failed status email to {$type} {$email} for record ID {$record->id}: " . $e->getMessage()); Log::error('Neblogistic status email failed', [ 'id' => $record->id, 'status' => $record->status, 'recipient_type' => $type, 'recipient_email' => $email, 'error' => $e->getMessage(), ]); if (!$isTestMode) { // Persist failure so this recipient/status is never retried. $this->markAttemptResult($record->id, $record->status, $email, $type, false, $e->getMessage()); } $newFailed++; } } if (!$isTestMode) { // Always clear the cron queue for this status once every intended recipient // has an attempt row (success or fail). Also clear when all were already attempted. if ($this->allRecipientsAttempted($record->id, $record->status, $recipients)) { $this->markStatusEmailSent($record); } } else { $this->info('Test mode: status_email_sent / email log were not updated.'); } Log::info('Neblogistic status email processing done', [ 'id' => $record->id, 'status' => $record->status, 'new_success' => $newSuccess, 'new_failed' => $newFailed, 'already_attempted' => $alreadyAttempted, 'test_mode' => $isTestMode, ]); if ($newSuccess > 0) { return true; } if ($newFailed > 0) { return false; } return null; } /** * @param mixed $email */ protected function normalizeEmail($email): ?string { if ($email === null) { return null; } $email = trim((string) $email); return $email === '' ? null : strtolower($email); } protected function alreadyAttempted($neblogisticId, string $status, string $email, string $type): bool { return DB::table('neblogistic_status_email_log') ->where('neblogistic_id', $neblogisticId) ->where('status', $status) ->where('recipient_email', $email) ->where('recipient_type', $type) ->exists(); } /** * Insert attempt row before send. Returns false if unique key already exists. */ protected function claimAttempt($neblogisticId, string $status, string $email, string $type): bool { try { DB::table('neblogistic_status_email_log')->insert([ 'neblogistic_id' => $neblogisticId, 'status' => $status, 'recipient_email' => $email, 'recipient_type' => $type, 'success' => false, 'error' => null, 'attempted_at' => now(), 'created_at' => now(), ]); return true; } catch (\Exception $e) { // Duplicate unique key (or other insert race) => treat as already claimed. Log::warning('Neblogistic status email claim failed', [ 'neblogistic_id' => $neblogisticId, 'status' => $status, 'recipient_email' => $email, 'recipient_type' => $type, 'error' => $e->getMessage(), ]); return false; } } protected function markAttemptResult($neblogisticId, string $status, string $email, string $type, bool $success, ?string $error): void { DB::table('neblogistic_status_email_log') ->where('neblogistic_id', $neblogisticId) ->where('status', $status) ->where('recipient_email', $email) ->where('recipient_type', $type) ->update([ 'success' => $success, 'error' => $error ? mb_substr($error, 0, 2000) : null, 'attempted_at' => now(), ]); } /** * @param array $recipients */ protected function allRecipientsAttempted($neblogisticId, string $status, array $recipients): bool { foreach ($recipients as $recipient) { if (!$this->alreadyAttempted($neblogisticId, $status, $recipient['email'], $recipient['type'])) { return false; } } return true; } /** * @param object $record */ protected function markStatusEmailSent($record): void { DB::table('neblogistic') ->where('id', $record->id) ->update(['status_email_sent' => $record->status]); } }