Rapport-Laravel/app/Console/Commands/SendNeblogisticStatusEmail.php

325 lines
11 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Schema;
use App\Mail\NeblogisticStatusUpdate;
class SendNeblogisticStatusEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'neblogistic:send-status-email
{--record_id= : Neblogistic record ID to send for (test mode)}
{--email= : Override recipient email (test mode)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send Dispatched/Delivered status emails to TM and ABM with invoice and LR details (once per recipient per status)';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$recordId = $this->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<int, array{email:string,type:string}> $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]);
}
}