325 lines
11 KiB
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]);
|
|
}
|
|
}
|