mivita/app/Console/Commands/DhlUpdateTracking.php
2026-02-20 17:55:06 +01:00

323 lines
12 KiB
PHP

<?php
namespace App\Console\Commands;
use Acme\Dhl\Models\DhlShipment;
use App\Mail\MailDhlTracking;
use App\Services\DhlTrackingService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class DhlUpdateTracking extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dhl:update-tracking
{--days=30 : Sendungen der letzten X Tage aktualisieren}
{--send-emails : Automatisch E-Mails bei Transit-Status senden}
{--dry-run : Nur simulieren, keine Änderungen}
{--test-email= : Test-E-Mail an angegebene Adresse senden}
{--order= : Nur für bestimmte Bestellung (Order-ID)}
{--force : Intervall-Filter überspringen, alle aktiven Sendungen aktualisieren}
{--stale-days=30 : Sendungen ohne Statusänderung nach X Tagen als abgeschlossen markieren}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Aktualisiert Tracking-Status für DHL Sendungen (status-basierte Intervalle, Batch-API)';
/**
* Execute the console command.
*/
public function handle(): int
{
$days = (int) $this->option('days');
$sendEmails = $this->option('send-emails');
$dryRun = $this->option('dry-run');
$testEmail = $this->option('test-email');
$orderId = $this->option('order');
$force = $this->option('force');
$staleDays = (int) $this->option('stale-days');
$this->info('DHL Tracking Update gestartet');
$this->info("Optionen: --days={$days}, --send-emails=".($sendEmails ? 'ja' : 'nein')
.', --dry-run='.($dryRun ? 'ja' : 'nein')
.', --force='.($force ? 'ja' : 'nein')
.", --stale-days={$staleDays}");
if ($testEmail) {
$this->info("Test-Modus: E-Mails werden an {$testEmail} gesendet");
}
if ($orderId) {
$this->info("Filter: Nur Order-ID {$orderId}");
}
$this->newLine();
// Step 1: Mark stale shipments as completed (before main query)
$staleCompleted = $this->markStaleShipmentsCompleted($staleDays, $dryRun);
// Step 2: Build query for shipments that need tracking update
$query = $this->buildShipmentQuery($days, $orderId, $force);
$shipments = $query->orderBy('created_at', 'desc')->get();
// Count total active shipments for statistics (before interval filter)
$totalActive = DhlShipment::active()
->whereNull('tracking_completed_at')
->where('created_at', '>=', now()->subDays($days))
->whereNotNull('dhl_shipment_no')
->count();
$total = $shipments->count();
$skippedByInterval = $totalActive - $total;
$this->info("Aktive Sendungen gesamt: {$totalActive}");
$this->info('Übersprungen (Intervall): '.max(0, $skippedByInterval));
$this->info("Zu aktualisieren: {$total}");
if ($total === 0) {
$this->info('Keine Sendungen zum Aktualisieren gefunden.');
$this->printSummary(0, ['updated' => 0, 'failed' => 0, 'completed' => 0, 'emails_sent' => 0, 'skipped' => 0], $staleCompleted, max(0, $skippedByInterval));
return self::SUCCESS;
}
$trackingService = new DhlTrackingService;
$stats = [
'updated' => 0,
'failed' => 0,
'completed' => 0,
'emails_sent' => 0,
'skipped' => 0,
];
if ($dryRun) {
$stats['skipped'] = $total;
$this->info("Dry-Run: {$total} Sendungen würden aktualisiert.");
} else {
// Collect old statuses for email decision
$oldStatuses = $shipments->pluck('status', 'id')->toArray();
// Use batch API for efficient processing
$this->info('Starte Batch-Tracking-Update...');
$bar = $this->output->createProgressBar($total);
$bar->start();
$batchResult = $trackingService->updateTrackingBatch($shipments);
$stats['updated'] = $batchResult['updated'];
$stats['failed'] = $batchResult['failed'];
$stats['completed'] = $batchResult['completed'];
$bar->advance($total);
$bar->finish();
$this->newLine(2);
// Send tracking emails if enabled
if ($sendEmails) {
$this->info('Prüfe E-Mail-Versand...');
foreach ($shipments as $shipment) {
$shipment->refresh();
$oldStatus = $oldStatuses[$shipment->id] ?? '';
if ($this->shouldSendEmail($shipment, $oldStatus)) {
try {
$this->sendTrackingEmail($shipment, $testEmail);
$stats['emails_sent']++;
} catch (\Exception $e) {
Log::error('[DHL Cron] Failed to send tracking email', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
}
}
}
}
$this->printSummary($total, $stats, $staleCompleted, max(0, $skippedByInterval));
Log::info('[DHL Cron] Tracking update completed', array_merge($stats, [
'total' => $total,
'stale_completed' => $staleCompleted,
'skipped_interval' => max(0, $skippedByInterval),
]));
return self::SUCCESS;
}
/**
* Build the shipment query with or without interval filtering.
*/
private function buildShipmentQuery(int $days, ?string $orderId, bool $force)
{
if ($force) {
// --force: Alle aktiven Sendungen ohne Intervall-Filter
$query = DhlShipment::active()
->whereNull('tracking_completed_at')
->where('created_at', '>=', now()->subDays($days))
->whereNotNull('dhl_shipment_no');
} else {
// Normal: Status-basierte Intervalle beachten
$query = DhlShipment::needsTrackingUpdate()
->where('created_at', '>=', now()->subDays($days));
}
// Filter nach Order-ID wenn angegeben
if ($orderId) {
$query->where('order_id', $orderId);
}
return $query;
}
/**
* Mark shipments as tracking-completed if they haven't changed status
* for a given number of days (stale shipments).
*/
private function markStaleShipmentsCompleted(int $staleDays, bool $dryRun): int
{
$staleShipments = DhlShipment::active()
->whereNull('tracking_completed_at')
->whereNotNull('last_tracked_at')
->where('last_tracked_at', '<', now()->subDays($staleDays))
->where('created_at', '<', now()->subDays($staleDays))
->get();
$count = $staleShipments->count();
if ($count > 0) {
$this->warn("Veraltete Sendungen gefunden: {$count} (>{$staleDays} Tage ohne Änderung)");
if (! $dryRun) {
foreach ($staleShipments as $shipment) {
$shipment->markTrackingCompleted();
Log::info('[DHL Cron] Stale shipment tracking completed', [
'shipment_id' => $shipment->id,
'dhl_shipment_no' => $shipment->dhl_shipment_no,
'status' => $shipment->status,
'last_tracked_at' => $shipment->last_tracked_at?->toDateTimeString(),
]);
}
$this->info("{$count} Sendungen als Tracking-abgeschlossen markiert.");
} else {
$this->info(" → Dry-Run: {$count} Sendungen würden als abgeschlossen markiert.");
}
}
$this->newLine();
return $count;
}
/**
* Print the final summary table.
*/
private function printSummary(int $total, array $stats, int $staleCompleted, int $skippedByInterval): void
{
$this->info('Zusammenfassung:');
$this->table(
['Metrik', 'Anzahl'],
[
['Zu aktualisieren', $total],
['Aktualisiert', $stats['updated']],
['Fehlgeschlagen', $stats['failed']],
['Tracking abgeschlossen', $stats['completed']],
['E-Mails gesendet', $stats['emails_sent']],
['Übersprungen (Dry-Run)', $stats['skipped']],
['Übersprungen (Intervall)', $skippedByInterval],
['Veraltet → abgeschlossen', $staleCompleted],
]
);
}
/**
* Prüft ob eine E-Mail gesendet werden soll
*/
private function shouldSendEmail(DhlShipment $shipment, string $oldStatus): bool
{
// E-Mail nur senden wenn:
// 1. Status ist jetzt "in_transit"
// 2. Vorheriger Status war NICHT "in_transit" (also Status hat sich geändert)
// 3. Noch keine E-Mail gesendet wurde
return $shipment->status === 'in_transit'
&& $oldStatus !== 'in_transit'
&& ! $shipment->wasTrackingEmailSent()
&& $shipment->canSendTrackingEmail();
}
/**
* Sendet die Tracking-E-Mail (mit Unterstützung für mehrere Sendungen pro Bestellung)
*/
private function sendTrackingEmail(DhlShipment $shipment, ?string $testEmail = null): void
{
try {
$order = $shipment->shoppingOrder;
// Determine recipient email: test email > shipment email > shopping user email
$recipientEmail = null;
if ($testEmail) {
$recipientEmail = $testEmail;
} elseif (! empty($shipment->email)) {
$recipientEmail = $shipment->email;
} elseif ($order->shopping_user && ! empty($order->shopping_user->email)) {
$recipientEmail = $order->shopping_user->email;
}
if (! $recipientEmail) {
Log::warning('[DHL Cron] Cannot send email - no recipient', [
'shipment_id' => $shipment->id,
]);
return;
}
// Sammle alle Sendungen für diese Bestellung, die noch keine E-Mail erhalten haben
$allShipments = DhlShipment::where('order_id', $order->id)
->where('status', 'in_transit')
->whereNotNull('dhl_shipment_no')
->whereNull('tracking_email_sent_at')
->get();
// Wenn keine Sendungen gefunden, nutze nur die aktuelle
if ($allShipments->isEmpty()) {
$allShipments = collect([$shipment]);
}
// Sende E-Mail mit allen Sendungen
Mail::to($recipientEmail)->send(new MailDhlTracking($allShipments, $order));
// Markiere alle Sendungen als versendet
foreach ($allShipments as $s) {
$s->markTrackingEmailSent('auto');
}
Log::info('[DHL Cron] Tracking email sent automatically', [
'shipment_ids' => $allShipments->pluck('id')->toArray(),
'shipments_count' => $allShipments->count(),
'dhl_shipment_nos' => $allShipments->pluck('dhl_shipment_no')->toArray(),
'email' => $recipientEmail,
'is_test' => ! is_null($testEmail),
]);
if ($allShipments->count() > 1) {
$this->line(" → E-Mail mit {$allShipments->count()} Sendungen gesendet an: {$recipientEmail}");
} else {
$this->line(" → E-Mail gesendet an: {$recipientEmail}");
}
} catch (\Exception $e) {
Log::error('[DHL Cron] Failed to send tracking email', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
}
}