getDhlConfig(); $this->apiKey = $dhlConfig['api_key'] ?? config('dhl.api_key'); $this->apiSecret = $dhlConfig['api_secret'] ?? config('dhl.legacy.api_secret'); $this->isSandbox = ($dhlConfig['sandbox'] ?? config('dhl.legacy.sandbox', true)); } /** * Track shipment using DHL Unified Tracking API (recommended for international) */ public function trackShipment(string $trackingNumber, array $options = []): array { try { Log::info('[DHL Tracking Service] Tracking shipment with Unified API', [ 'tracking_number' => $trackingNumber, 'is_sandbox' => $this->isSandbox, 'has_api_key' => ! empty($this->apiKey), ]); $response = Http::withHeaders([ 'DHL-API-Key' => $this->apiKey, 'Accept' => 'application/json', ]) ->withOptions([ 'verify' => config('dhl.ssl.verify_peer', true), 'http_errors' => false, 'curl' => [ CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true), CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0, CURLOPT_SSLVERSION => $this->getSslVersion(), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10), CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30), CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0', ], ]) ->get('https://api-eu.dhl.com/track/shipments', [ 'trackingNumber' => $trackingNumber, 'requesterCountryCode' => 'DE', 'originCountryCode' => 'DE', 'language' => 'de', ]); Log::info('[DHL Tracking Service] Unified API response', [ 'tracking_number' => $trackingNumber, 'status_code' => $response->status(), 'successful' => $response->successful(), ]); if ($response->successful()) { $data = $response->json(); if (isset($data['shipments']) && count($data['shipments']) > 0) { $shipment = $data['shipments'][0]; return [ 'success' => true, 'tracking_number' => $shipment['id'], 'status' => $shipment['status']['statusCode'] ?? 'unknown', 'status_text' => $shipment['status']['description'] ?? ($shipment['status']['status'] ?? 'Unbekannt'), 'description' => $shipment['status']['remark'] ?? ($shipment['status']['description'] ?? ''), 'last_update' => $shipment['status']['timestamp'] ?? null, 'origin' => $shipment['origin']['address']['addressLocality'] ?? null, 'destination' => $shipment['destination']['address']['addressLocality'] ?? null, 'events' => $shipment['events'] ?? [], 'api_used' => 'unified', ]; } } Log::warning('[DHL Tracking Service] Unified API did not find shipment, trying Parcel DE API', [ 'tracking_number' => $trackingNumber, 'status_code' => $response->status(), 'response_snippet' => mb_substr($response->body(), 0, 500), ]); // If Unified API fails, try Parcel DE API return $this->trackShipmentDE($trackingNumber, $options); } catch (Exception $e) { Log::error('[DHL Tracking Service] Unified API failed', [ 'tracking_number' => $trackingNumber, 'error' => $e->getMessage(), ]); // Fallback to Parcel DE API return $this->trackShipmentDE($trackingNumber, $options); } } /** * Track shipment using DHL Parcel DE Tracking API (optimized for Germany) */ public function trackShipmentDE(string $trackingNumber, array $options = []): array { try { Log::info('[DHL Tracking Service] Tracking shipment with Parcel DE API', [ 'tracking_number' => $trackingNumber, 'is_sandbox' => $this->isSandbox, 'has_api_key' => ! empty($this->apiKey), 'has_api_secret' => ! empty($this->apiSecret), ]); $response = Http::withHeaders([ 'DHL-API-Key' => $this->apiKey, 'Accept' => 'application/json', ]) ->withOptions([ 'verify' => config('dhl.ssl.verify_peer', true), 'http_errors' => false, 'curl' => [ CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true), CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0, CURLOPT_SSLVERSION => $this->getSslVersion(), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10), CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30), CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0', ], ]) ->get('https://api-eu.dhl.com/parcel/de/tracking/v0/shipments', [ 'shipmentId' => $trackingNumber, 'language' => 'de', ]); Log::info('[DHL Tracking Service] Parcel DE API response', [ 'tracking_number' => $trackingNumber, 'status_code' => $response->status(), 'successful' => $response->successful(), 'response_body' => $response->body(), ]); if ($response->successful()) { $data = $response->json(); if (isset($data['shipments']) && count($data['shipments']) > 0) { $shipment = $data['shipments'][0]; return [ 'success' => true, 'tracking_number' => $shipment['id'], 'status' => $shipment['status']['statusCode'] ?? 'unknown', 'status_text' => $shipment['status']['description'] ?? 'Unbekannt', 'description' => $shipment['status']['description'] ?? '', 'last_update' => $shipment['status']['timestamp'] ?? null, 'events' => $shipment['events'] ?? [], 'api_used' => 'parcel_de', ]; } } // Log detailed error information Log::warning('[DHL Tracking Service] Shipment not found or not yet tracked', [ 'tracking_number' => $trackingNumber, 'status_code' => $response->status(), 'response_body' => $response->body(), ]); return [ 'success' => false, 'message' => 'Sendung nicht gefunden oder noch nicht im System erfasst. HTTP Status: '.$response->status(), 'tracking_number' => $trackingNumber, 'api_used' => 'parcel_de', 'debug_info' => [ 'status_code' => $response->status(), 'response' => $response->json(), ], ]; } catch (Exception $e) { Log::error('[DHL Tracking Service] Parcel DE API failed', [ 'tracking_number' => $trackingNumber, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return [ 'success' => false, 'message' => 'Fehler beim Abrufen der Tracking-Informationen: '.$e->getMessage(), 'tracking_number' => $trackingNumber, 'api_used' => 'parcel_de', ]; } } /** * Track multiple shipments at once (up to 10 for Unified API) */ public function trackMultipleShipments(array $trackingNumbers): array { if (count($trackingNumbers) > 10) { return [ 'success' => false, 'message' => 'Maximal 10 Sendungen können gleichzeitig getrackt werden.', ]; } try { $response = Http::withHeaders([ 'DHL-API-Key' => $this->apiKey, 'Accept' => 'application/json', ]) ->withOptions([ 'verify' => config('dhl.ssl.verify_peer', true), 'http_errors' => false, 'curl' => [ CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true), CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0, CURLOPT_SSLVERSION => $this->getSslVersion(), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10), CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30), CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0', ], ]) ->get('https://api-eu.dhl.com/track/shipments', [ 'trackingNumber' => implode(',', $trackingNumbers), 'requesterCountryCode' => 'DE', 'language' => 'de', ]); if ($response->successful()) { $data = $response->json(); $results = []; foreach ($data['shipments'] ?? [] as $shipment) { $results[] = [ 'tracking_number' => $shipment['id'], 'status' => $shipment['status']['statusCode'] ?? 'unknown', 'status_text' => $shipment['status']['status'] ?? 'Unbekannt', 'last_update' => $shipment['status']['timestamp'] ?? null, 'events' => $shipment['events'] ?? [], ]; } return [ 'success' => true, 'shipments' => $results, 'api_used' => 'unified', ]; } return [ 'success' => false, 'message' => 'Fehler beim Abrufen der Tracking-Informationen.', ]; } catch (Exception $e) { Log::error('[DHL Tracking Service] Multiple tracking failed', [ 'tracking_numbers' => $trackingNumbers, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => 'Fehler beim Abrufen der Tracking-Informationen: '.$e->getMessage(), ]; } } /** * Update tracking for a DHL shipment (sync or async based on config) */ public function updateTracking(DhlShipment $shipment, array $options = []): array { // Get DHL configuration $settingController = new SettingController; $dhlConfig = $settingController->getDhlConfig(); // Check if queue should be used $useQueue = $dhlConfig['use_queue'] ?? false; if ($useQueue) { return $this->updateTrackingAsync($shipment, $options, $dhlConfig); } else { return $this->updateTrackingSync($shipment, $options, $dhlConfig); } } /** * Update tracking asynchronously using queue */ private function updateTrackingAsync(DhlShipment $shipment, array $options, array $dhlConfig): array { try { // Dispatch job with pre-loaded config TrackShipmentJob::dispatch($shipment, $options); Log::info('[DHL Tracking Service] Tracking update dispatched to queue', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, ]); return [ 'success' => true, 'message' => 'Tracking-Update wird verarbeitet. Sie erhalten eine Benachrichtigung, sobald die Informationen aktualisiert sind.', 'queued' => true, 'shipment_id' => $shipment->id, ]; } catch (Exception $e) { Log::error('[DHL Tracking Service] Failed to dispatch tracking update', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return [ 'success' => false, 'message' => 'Fehler beim Einreihen des Tracking-Updates: '.$e->getMessage(), 'queued' => false, ]; } } /** * Update tracking synchronously using new DHL APIs */ private function updateTrackingSync(DhlShipment $shipment, array $options, array $dhlConfig): array { try { Log::info('[DHL Tracking Service] Updating tracking synchronously', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, ]); // Check if shipment has tracking number if (! $shipment->dhl_shipment_no) { return [ 'success' => false, 'message' => 'Keine DHL-Sendungsnummer verfügbar für Tracking.', 'queued' => false, 'shipment_id' => $shipment->id, ]; } // Use new tracking API $result = $this->trackShipment($shipment->dhl_shipment_no); if ($result['success']) { $internalStatus = $this->mapDhlStatusToInternal($result['status']); // Update shipment with tracking data $updateData = [ 'status' => $internalStatus, 'tracking_status' => $result['status_text'], 'last_tracked_at' => now(), ]; // Mark tracking as completed if terminal status reached if (in_array($internalStatus, DhlShipment::TERMINAL_STATUSES)) { $updateData['tracking_completed_at'] = now(); } $shipment->update($updateData); // Save tracking events $this->saveTrackingEvents($shipment, $result['events'] ?? []); Log::info('[DHL Tracking Service] Tracking updated successfully (sync)', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, 'tracking_status' => $result['status'], 'tracking_completed' => in_array($internalStatus, DhlShipment::TERMINAL_STATUSES), 'events_count' => count($result['events'] ?? []), 'api_used' => $result['api_used'], ]); return [ 'success' => true, 'message' => 'Tracking-Informationen erfolgreich aktualisiert!', 'queued' => false, 'shipment_id' => $shipment->id, 'tracking_status' => $result['status'], 'tracking_completed' => in_array($internalStatus, DhlShipment::TERMINAL_STATUSES), 'tracking_details' => $result, ]; } else { return [ 'success' => false, 'message' => $result['message'] ?? 'Fehler beim Abrufen der Tracking-Informationen.', 'queued' => false, 'shipment_id' => $shipment->id, ]; } } catch (Exception $e) { Log::error('[DHL Tracking Service] Tracking update failed (sync)', [ 'shipment_id' => $shipment->id, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: '.$e->getMessage(), 'queued' => false, 'shipment_id' => $shipment->id, ]; } } /** * Update tracking for a batch of DHL shipments using the multi-tracking API. * Processes shipments in chunks of 10 (DHL API limit) with rate-limiting pauses. * * @param Collection $shipments * @return array{updated: int, failed: int, completed: int, results: array} */ public function updateTrackingBatch(Collection $shipments): array { $stats = [ 'updated' => 0, 'failed' => 0, 'completed' => 0, 'results' => [], ]; // Process in chunks of 10 (DHL API limit) $chunks = $shipments->chunk(10); $chunkIndex = 0; foreach ($chunks as $chunk) { // Rate limiting: pause 1 second between batch API calls if ($chunkIndex > 0) { sleep(1); } $chunkIndex++; // Build tracking number => shipment mapping $shipmentMap = []; foreach ($chunk as $shipment) { $shipmentMap[$shipment->dhl_shipment_no] = $shipment; } $trackingNumbers = array_keys($shipmentMap); try { $batchResult = $this->trackMultipleShipments($trackingNumbers); if ($batchResult['success'] && ! empty($batchResult['shipments'])) { // Process each result from the batch API foreach ($batchResult['shipments'] as $trackingResult) { $trackingNo = $trackingResult['tracking_number']; $shipment = $shipmentMap[$trackingNo] ?? null; if (! $shipment) { Log::warning('[DHL Tracking Service] Batch: tracking number not mapped', [ 'tracking_number' => $trackingNo, ]); continue; } // Remove from map so we can detect missing ones later unset($shipmentMap[$trackingNo]); $internalStatus = $this->mapDhlStatusToInternal($trackingResult['status']); $updateData = [ 'status' => $internalStatus, 'tracking_status' => $trackingResult['status_text'], 'last_tracked_at' => now(), ]; // Mark tracking as completed if terminal status reached $isCompleted = in_array($internalStatus, DhlShipment::TERMINAL_STATUSES); if ($isCompleted) { $updateData['tracking_completed_at'] = now(); $stats['completed']++; } $shipment->update($updateData); // Save tracking events $this->saveTrackingEvents($shipment, $trackingResult['events'] ?? []); $stats['updated']++; $stats['results'][] = [ 'shipment_id' => $shipment->id, 'tracking_number' => $trackingNo, 'status' => $internalStatus, 'completed' => $isCompleted, 'success' => true, ]; } // Any remaining shipments in the map were not returned by the API foreach ($shipmentMap as $trackingNo => $shipment) { // Update last_tracked_at so we don't immediately retry $shipment->update(['last_tracked_at' => now()]); $stats['failed']++; $stats['results'][] = [ 'shipment_id' => $shipment->id, 'tracking_number' => $trackingNo, 'success' => false, 'message' => 'Nicht in Batch-Antwort enthalten', ]; } } else { // Entire batch failed - fall back to individual tracking Log::warning('[DHL Tracking Service] Batch tracking failed, falling back to individual tracking', [ 'tracking_numbers' => $trackingNumbers, 'message' => $batchResult['message'] ?? 'Unknown error', ]); foreach ($chunk as $shipment) { try { $result = $this->updateTracking($shipment, ['auto_retrack' => false]); if ($result['success']) { $stats['updated']++; if (! empty($result['tracking_completed'])) { $stats['completed']++; } } else { $stats['failed']++; } $stats['results'][] = [ 'shipment_id' => $shipment->id, 'tracking_number' => $shipment->dhl_shipment_no, 'success' => $result['success'], 'fallback' => true, ]; } catch (Exception $e) { $stats['failed']++; $stats['results'][] = [ 'shipment_id' => $shipment->id, 'tracking_number' => $shipment->dhl_shipment_no, 'success' => false, 'message' => $e->getMessage(), 'fallback' => true, ]; } } } } catch (Exception $e) { Log::error('[DHL Tracking Service] Batch tracking exception', [ 'tracking_numbers' => $trackingNumbers, 'error' => $e->getMessage(), ]); // Mark all as failed but update last_tracked_at foreach ($chunk as $shipment) { $shipment->update(['last_tracked_at' => now()]); $stats['failed']++; $stats['results'][] = [ 'shipment_id' => $shipment->id, 'tracking_number' => $shipment->dhl_shipment_no, 'success' => false, 'message' => $e->getMessage(), ]; } } } Log::info('[DHL Tracking Service] Batch tracking completed', [ 'total' => $shipments->count(), 'updated' => $stats['updated'], 'failed' => $stats['failed'], 'completed' => $stats['completed'], 'chunks' => $chunks->count(), ]); return $stats; } /** * Map DHL status codes to internal status */ private function mapDhlStatusToInternal(string $dhlStatus): string { $statusMap = [ 'pre-transit' => 'created', 'transit' => 'in_transit', 'out-for-delivery' => 'out_for_delivery', 'delivered' => 'delivered', 'failure' => 'failed', 'returned' => 'returned', 'exception' => 'exception', ]; return $statusMap[$dhlStatus] ?? 'unknown'; } /** * Get status description in German */ public function getStatusDescription(string $statusCode): string { $descriptions = [ 'pre-transit' => 'Auftrag elektronisch übermittelt', 'transit' => 'Sendung in Zustellung', 'out-for-delivery' => 'Wird heute zugestellt', 'delivered' => 'Erfolgreich zugestellt', 'failure' => 'Zustellung fehlgeschlagen', 'returned' => 'Sendung wird zurückgeschickt', 'exception' => 'Zustellausnahme', ]; return $descriptions[$statusCode] ?? 'Unbekannter Status'; } /** * Save tracking events from API response to database */ private function saveTrackingEvents(DhlShipment $shipment, array $events): void { if (empty($events)) { return; } foreach ($events as $event) { $eventTime = isset($event['timestamp']) ? \Carbon\Carbon::parse($event['timestamp']) : now(); // Upsert: avoid duplicates based on shipment + event_time + status_code DhlTrackingEvent::updateOrCreate( [ 'shipment_id' => $shipment->id, 'event_time' => $eventTime, 'status_code' => $event['statusCode'] ?? ($event['status'] ?? 'unknown'), ], [ 'status_text' => $event['description'] ?? ($event['remark'] ?? 'Unbekannt'), 'location' => $event['location']['address']['addressLocality'] ?? null, 'raw' => $event, ] ); } Log::info('[DHL Tracking Service] Tracking events saved', [ 'shipment_id' => $shipment->id, 'events_saved' => count($events), ]); } /** * Get SSL version constant based on configuration */ private function getSslVersion(): int { $sslVersion = config('dhl.ssl.ssl_version', 'TLSv1_2'); return match ($sslVersion) { 'TLSv1_0' => CURL_SSLVERSION_TLSv1_0, 'TLSv1_1' => CURL_SSLVERSION_TLSv1_1, 'TLSv1_2' => CURL_SSLVERSION_TLSv1_2, 'TLSv1_3' => defined('CURL_SSLVERSION_TLSv1_3') ? CURL_SSLVERSION_TLSv1_3 : CURL_SSLVERSION_TLSv1_2, default => CURL_SSLVERSION_TLSv1_2, }; } }