calculate($order)); // Get DHL configuration $settingController = new SettingController; $dhlConfig = $settingController->getDhlConfig(); Log::info('[DHL Service] Loaded DHL configuration', self::sanitizeDhlConfigForLog($dhlConfig)); // Check if queue should be used $useQueue = $dhlConfig['use_queue'] ?? false; if ($useQueue && $this->requiresSynchronousAddressValidation($options, $dhlConfig)) { Log::info('[DHL Service] Queue disabled for DHL mustEncode address validation', [ 'order_id' => $order->id, ]); $useQueue = false; } if ($useQueue) { return $this->createShipmentAsync($order, $weight, $options, $dhlConfig); } else { return $this->createShipmentSync($order, $weight, $options, $dhlConfig); } } private function requiresSynchronousAddressValidation(array $options, array $dhlConfig): bool { if (! (bool) ($options['print_only_if_codeable'] ?? $dhlConfig['print_only_if_codeable'] ?? config('dhl.print_only_if_codeable', true))) { return false; } $country = $options['shipping_address']['country'] ?? null; $countryCode = is_object($country) ? ($country->code ?? null) : ($country['code'] ?? null); return strtoupper((string) $countryCode) === DhlProductResolver::DOMESTIC_COUNTRY; } /** * Create shipment asynchronously using queue */ private function createShipmentAsync(ShoppingOrder $order, float $weight, array $options, array $dhlConfig): array { try { // Dispatch job with pre-loaded config CreateShipmentJob::dispatch($order, $weight, $options, $dhlConfig); Log::info('[DHL Service] Shipment creation dispatched to queue', [ 'order_id' => $order->id, 'weight' => $weight, ]); return [ 'success' => true, 'message' => 'Sendung wird erstellt. Sie erhalten eine Benachrichtigung, sobald das Versandlabel verfügbar ist.', 'queued' => true, 'order_id' => $order->id, ]; } catch (Exception $e) { Log::error('[DHL Service] Failed to dispatch shipment creation', [ 'error' => $e->getMessage(), 'order_id' => $order->id, ]); return [ 'success' => false, 'message' => 'Fehler beim Einreihen der Sendungserstellung: '.$e->getMessage(), 'queued' => false, ]; } } /** * Create shipment synchronously */ private function createShipmentSync(ShoppingOrder $order, float $weight, array $options, array $dhlConfig): array { try { Log::info('[DHL Service] Creating shipment synchronously', [ 'order_id' => $order->id, 'weight' => $weight, ]); // Create DHL client directly with correct base URL $dhlClient = new \Acme\Dhl\Support\DhlClient( $dhlConfig['base_url'], $dhlConfig['api_key'], $dhlConfig['username'], $dhlConfig['password'] ); $shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient); // Prepare order data using helper $orderData = DhlDataHelper::prepareOrderData($order, $weight, $options, $dhlConfig); Log::info('[DHL Service] Order data prepared for DHL API', self::sanitizeOrderDataForLog($orderData)); // Create the shipment directly $result = $shippingService->createLabel($orderData); Log::info('[DHL Service] Shipment created successfully (sync)', [ 'order_id' => $order->id, 'shipment_number' => $result['shipmentNumber'] ?? 'N/A', 'label_path' => $result['labelPath'] ?? 'N/A', ]); return [ 'success' => true, 'message' => 'Versandlabel erfolgreich erstellt!', 'queued' => false, 'order_id' => $order->id, 'shipment_number' => $result['shipmentNumber'] ?? null, 'tracking_number' => $result['trackingNumber'] ?? null, 'label_path' => $result['labelPath'] ?? null, 'label_url' => $result['labelUrl'] ?? null, ]; } catch (DhlAddressValidationException $e) { Log::warning('[DHL Service] Shipment address validation failed (sync)', [ 'order_id' => $order->id, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'type' => 'dhl_address_validation', 'message' => $e->getMessage(), 'errors' => [$e->getMessage()], 'queued' => false, 'order_id' => $order->id, ]; } catch (Exception $e) { Log::error('[DHL Service] Shipment creation failed (sync)', [ 'order_id' => $order->id, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => 'Fehler beim Erstellen des Versandlabels: '.$e->getMessage(), 'queued' => false, 'order_id' => $order->id, ]; } } /** * Cancel a DHL shipment (sync or async based on config) */ public function cancelShipment(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->cancelShipmentAsync($shipment, $options, $dhlConfig); } else { return $this->cancelShipmentSync($shipment, $options, $dhlConfig); } } /** * Cancel shipment asynchronously using queue */ private function cancelShipmentAsync(DhlShipment $shipment, array $options, array $dhlConfig): array { try { // Dispatch job CancelShipmentJob::dispatch($shipment, $options); Log::info('[DHL Service] Shipment cancellation dispatched to queue', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, ]); return [ 'success' => true, 'message' => 'Sendung wird storniert...', 'queued' => true, 'shipment_id' => $shipment->id, ]; } catch (Exception $e) { Log::error('[DHL Service] Failed to dispatch shipment cancellation', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return [ 'success' => false, 'message' => 'Fehler beim Einreihen der Stornierung: '.$e->getMessage(), 'queued' => false, ]; } } /** * Cancel shipment synchronously */ private function cancelShipmentSync(DhlShipment $shipment, array $options, array $dhlConfig): array { try { // Validate shipment has DHL number if (empty($shipment->dhl_shipment_no)) { $this->recordCancellationFailure($shipment, 'missing_shipment_number', 'Sendung hat keine DHL-Sendungsnummer und kann nicht storniert werden.'); return [ 'success' => false, 'message' => 'Sendung hat keine DHL-Sendungsnummer und kann nicht storniert werden.', 'queued' => false, 'shipment_id' => $shipment->id, ]; } // Validate shipment can be cancelled if (! $shipment->canCancel()) { $this->recordCancellationFailure($shipment, 'status_not_cancelable', 'Sendung kann im aktuellen Status "'.$shipment->status.'" nicht storniert werden.'); return [ 'success' => false, 'message' => 'Sendung kann im aktuellen Status "'.$shipment->getStatusTranslation().'" nicht storniert werden. Nur Status "Erstellt" oder "Wartend" sind stornierbar.', 'queued' => false, 'shipment_id' => $shipment->id, ]; } Log::info('[DHL Service] Cancelling shipment synchronously', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, 'status' => $shipment->status, 'base_url' => $dhlConfig['base_url'], ]); // Create DHL client $dhlClient = new \Acme\Dhl\Support\DhlClient( $dhlConfig['base_url'], $dhlConfig['api_key'], $dhlConfig['username'], $dhlConfig['password'] ); $shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient); // Cancel the shipment directly $success = $shippingService->cancelLabel($shipment->dhl_shipment_no); if ($success) { Log::info('[DHL Service] Shipment cancelled successfully (sync)', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, ]); return [ 'success' => true, 'message' => 'Sendung wurde erfolgreich storniert!', 'queued' => false, 'shipment_id' => $shipment->id, ]; } else { throw new Exception('Cancellation returned false'); } } catch (\InvalidArgumentException $e) { $this->recordCancellationFailure($shipment, 'validation_failed', $e->getMessage(), $e); Log::warning('[DHL Service] Shipment cancellation validation failed', [ 'shipment_id' => $shipment->id, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => $e->getMessage(), 'queued' => false, 'shipment_id' => $shipment->id, ]; } catch (Exception $e) { $this->recordCancellationFailure($shipment, 'api_failed', $e->getMessage(), $e); Log::error('[DHL Service] Shipment cancellation failed (sync)', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, 'status' => $shipment->status, 'error' => $e->getMessage(), 'error_trace' => $e->getTraceAsString(), ]); // Check if it's an API authentication/resource error $errorMessage = $e->getMessage(); if (strpos($errorMessage, 'RF-UndefinedResource') !== false) { return [ 'success' => false, 'message' => 'Die Sendung konnte bei DHL nicht gefunden werden. Mögliche Ursachen: Sendung wurde bereits storniert, ist zu alt, oder wurde in einem anderen Modus (Sandbox/Production) erstellt.', 'queued' => false, 'shipment_id' => $shipment->id, 'technical_error' => $errorMessage, ]; } return [ 'success' => false, 'message' => 'Fehler beim Stornieren der Sendung: '.$errorMessage, 'queued' => false, 'shipment_id' => $shipment->id, ]; } } private function recordCancellationFailure(DhlShipment $shipment, string $reason, string $detail, ?Exception $exception = null): void { $apiResponseData = $shipment->api_response_data ?? []; $apiResponseData['cancellation_error'] = [ 'status' => 'failed', 'reason' => $reason, 'http_status' => $exception ? $this->extractHttpStatus($exception->getMessage()) : null, 'dhl_code' => $exception ? $this->extractDhlErrorCode($exception->getMessage()) : null, 'detail' => $detail, 'exception_class' => $exception ? $exception::class : null, 'occurred_at' => now()->toISOString(), ]; $shipment->update(['api_response_data' => $apiResponseData]); } private function extractHttpStatus(string $message): ?int { if (preg_match('/\b([45][0-9]{2})\b/', $message, $matches)) { return (int) $matches[1]; } return null; } private function extractDhlErrorCode(string $message): ?string { if (preg_match('/\b([A-Z]{2}-[A-Za-z0-9_-]+)\b/', $message, $matches)) { return $matches[1]; } return null; } /** * Build a redacted view of the DHL configuration for safe logging. * * Never include `api_key`, `username`, `password`, `api_secret` or the * full billing number itself. Only return boolean presence flags and * non-sensitive metadata. * * @param array $dhlConfig * @return array */ public static function sanitizeDhlConfigForLog(array $dhlConfig): array { return [ 'base_url' => $dhlConfig['base_url'] ?? null, 'sandbox' => $dhlConfig['sandbox'] ?? null, 'test_mode' => $dhlConfig['test_mode'] ?? null, 'has_api_key' => ! empty($dhlConfig['api_key']), 'has_username' => ! empty($dhlConfig['username']), 'has_password' => ! empty($dhlConfig['password']), 'has_api_secret' => ! empty($dhlConfig['api_secret']), 'use_queue' => (bool) ($dhlConfig['use_queue'] ?? false), 'default_product' => $dhlConfig['default_product'] ?? null, 'label_format' => $dhlConfig['label_format'] ?? null, 'print_format' => $dhlConfig['print_format'] ?? null, 'print_only_if_codeable' => (bool) ($dhlConfig['print_only_if_codeable'] ?? false), 'international_countries' => $dhlConfig['international_countries'] ?? [], 'account_numbers_configured' => array_keys(array_filter($dhlConfig['account_numbers'] ?? [])), ]; } /** * Build a redacted view of the order data prepared for DHL. * * Strips personally identifiable information like full names, addresses, * phone numbers and e-mail addresses. Keeps the routing-relevant fields * needed to debug a failing label generation. * * @param array $orderData * @return array */ public static function sanitizeOrderDataForLog(array $orderData): array { $consigneeCountry = $orderData['consignee']['country'] ?? null; $consigneePostal = $orderData['consignee']['postalCode'] ?? null; return [ 'order_id' => $orderData['order_id'] ?? null, 'product_code' => $orderData['product_code'] ?? null, 'weight_kg' => $orderData['weight_kg'] ?? null, 'label_format' => $orderData['label_format'] ?? null, 'print_format' => $orderData['print_format'] ?? null, 'print_only_if_codeable' => (bool) ($orderData['print_only_if_codeable'] ?? false), 'consignee_country' => $consigneeCountry, 'consignee_postal_prefix' => is_string($consigneePostal) && $consigneePostal !== '' ? mb_substr($consigneePostal, 0, 2) : null, 'consignee_has_post_number' => ! empty($orderData['consignee']['postNumber']), 'has_reference' => ! empty($orderData['reference']), ]; } }