config = config('dhl'); $this->isSandbox = $this->config['api']['sandbox'] ?? true; $this->initializeClients(); } /** * Initialize DHL API clients * * @throws Exception */ private function initializeClients(): void { try { // Get all credentials from the config. $apiKey = $this->config['api']['api_key'] ?? null; $apiSecret = $this->config['api']['api_secret'] ?? null; $username = $this->config['api']['username'] ?? null; $password = $this->config['api']['password'] ?? null; // The ChristophSchaeffer library for the "Geschäftskundenversand API" (SOAP) // always requires both sets of credentials: // 1. API Key/Secret for the Application's identity. // 2. Username/Password for the Business Customer Portal user's identity. // We must validate that all four are present. if (empty($apiKey) || empty($apiSecret)) { throw new Exception('DHL API Key (DHL_API_KEY) and API Secret (DHL_API_SECRET) are missing. Please get them from the DHL Developer Portal.'); } if (empty($username) || empty($password)) { throw new Exception('DHL API Username (DHL_API_USERNAME) and Password (DHL_API_PASSWORD) are missing. Please get them from your customer\'s DHL Business Customer Portal.'); } $shippingCredentials = new ShippingClientCredentials( $apiKey, $apiSecret, $username, $password ); $this->shippingClient = new ShippingClient( $shippingCredentials, $this->isSandbox, MultiClient::LANGUAGE_LOCALE_GERMAN_DE ); // Initialize Tracking Client (if enabled) if ($this->config['tracking']['enabled']) { // For tracking, the ZT-Token is often used as the "user" context $trackingUser = $this->config['tracking']['username'] ?? 'zt12345'; // Sandbox default $trackingPass = $this->config['tracking']['password'] ?? 'geheim'; // Sandbox default $trackingCredentials = new TrackingClientCredentials( $apiKey, $apiSecret, $trackingUser, $trackingPass ); $this->trackingClient = new TrackingClient( $trackingCredentials, $this->isSandbox ); } } catch (Exception $e) { $this->logError('Failed to initialize DHL clients', $e); throw new Exception('DHL API initialization failed: ' . $e->getMessage()); } } /** * Create a shipment for an order * * @param ShoppingOrder $order The shopping order * @param float $weight Package weight in kg * @param array $options Additional options * @return DhlShipment The created shipment * @throws Exception */ public function createShipment(ShoppingOrder $order, float $weight = 1.0, array $options = []): DhlShipment { try { $this->logInfo('Creating DHL shipment', [ 'order_id' => $order->id, 'weight' => $weight, 'options' => $options ]); // Build shipment data from order, including address parsing $shipmentData = $this->buildShipmentData($order, $weight, $options); \Log::info('shipmentData', $shipmentData); // Validate the built data before proceeding $this->validateShipmentData($shipmentData, $order->id); // Create DHL shipment record first $dhlShipment = $this->createDhlShipmentRecord($order, $shipmentData); // Create shipment order for DHL API $shipmentOrder = $this->buildShipmentOrder($shipmentData, $dhlShipment); // Call DHL API $request = new createShipmentOrder([$shipmentOrder]); // Constructor expects array of ShipmentOrder objects try { $response = $this->shippingClient->createShipmentOrder($request); // Enhanced debug logging for troubleshooting $this->logInfo('DHL API Response received', [ 'response_class' => get_class($response), 'has_errors' => !$response->hasNoErrors(), 'creation_states_count' => count($response->CreationStates ?? []) ]); // Log detailed response structure for debugging if (!$response->hasNoErrors()) { $this->logResponseStructure($response); } } catch (\ErrorException $e) { if (str_contains($e->getMessage(), 'Attempt to read property "Version" on null')) { throw new \Exception( 'The DHL API returned an unexpected response. This often indicates an authentication problem with the Business Customer API. ' . 'Please double-check your DHL_API_USERNAME and DHL_API_PASSWORD in the .env file. ' . 'Original error: ' . $e->getMessage(), 0, $e ); } throw $e; } // Process response $this->processShipmentResponse($dhlShipment, $response, $request); $this->logInfo('DHL shipment created successfully', [ 'shipment_id' => $dhlShipment->id, 'shipment_number' => $dhlShipment->shipment_number ]); return $dhlShipment->fresh(); } catch (DhlException $e) { $this->logError('DHL API error during shipment creation', $e, ['order_id' => $order->id]); throw new Exception('DHL shipment creation failed: ' . $e->getMessage()); } catch (Exception $e) { $this->logError('General error during shipment creation', $e, ['order_id' => $order->id]); throw $e; } } /** * Test DHL API Login Credentials by creating a dummy shipment. * * @return array */ public function testLogin(): array { try { $this->logInfo('Starting DHL API connection test.'); // Check basic configuration first if (!$this->isConfigured()) { return [ 'success' => false, 'message' => 'DHL API ist nicht vollständig konfiguriert. Prüfen Sie die .env Einstellungen.', ]; } // 1. Find a test order (preferably with complete shipping data) $testOrder = ShoppingOrder::with(['shopping_user', 'shopping_order_items']) ->whereHas('shopping_user') ->where('id', 35511) // Use specific test order ->first(); if (!$testOrder) { return [ 'success' => false, 'message' => 'Test-Bestellung (ID: 35511) nicht gefunden. Bitte verwenden Sie eine existierende Bestellung für den Test.', ]; } $this->logInfo('Using test order for API test', [ 'order_id' => $testOrder->id, 'has_user' => !is_null($testOrder->shopping_user), 'has_items' => $testOrder->shopping_order_items->count() > 0 ]); // 2. Define test options with realistic data $weight = 1.0; $options = [ 'product_code' => 'V01PAK', 'is_test' => true // Mark as test to avoid any side effects ]; // 3. Wrap in transaction to avoid database pollution DB::beginTransaction(); try { $dhlShipment = $this->createShipment($testOrder, $weight, $options); // If we get here, the API call was successful $success = !empty($dhlShipment->shipment_number); if ($success) { $this->logInfo('DHL API test successful', [ 'shipment_number' => $dhlShipment->shipment_number, 'status' => $dhlShipment->status ]); $result = [ 'success' => true, 'message' => 'DHL API Test erfolgreich! Verbindung und Authentifizierung funktionieren.', 'details' => [ 'shipment_number' => $dhlShipment->shipment_number, 'status' => $dhlShipment->status, 'api_type' => $this->config['api']['api_type'] ?? 'unknown', 'sandbox' => $this->isSandbox ] ]; } else { $result = [ 'success' => false, 'message' => 'DHL API Test unvollständig: Sendung erstellt aber keine Sendungsnummer erhalten.', 'details' => [ 'status' => $dhlShipment->status ?? 'unknown', 'errors' => $dhlShipment->api_errors ?? 'No specific errors' ] ]; } } catch (Exception $apiException) { $this->logError('DHL API test failed during createShipment', $apiException, [ 'order_id' => $testOrder->id ]); $result = [ 'success' => false, 'message' => 'DHL API Test fehlgeschlagen: ' . $apiException->getMessage(), 'details' => [ 'error_type' => get_class($apiException), 'api_type' => $this->config['api']['api_type'] ?? 'unknown', 'sandbox' => $this->isSandbox ] ]; } // Always rollback transaction for tests DB::rollBack(); return $result; } catch (Exception $e) { DB::rollBack(); // Ensure rollback on any failure $this->logError('DHL API test failed with general error', $e); return [ 'success' => false, 'message' => 'DHL API Test fehlgeschlagen: ' . $e->getMessage(), 'details' => [ 'error_type' => get_class($e), 'configured' => $this->isConfigured(), 'sandbox' => $this->isSandbox ] ]; } } /** * Validates required shipment data fields to prevent API errors. * * @param array $shipmentData * @param int $orderId * @throws \Exception */ private function validateShipmentData(array $shipmentData, int $orderId): void { $requiredFields = [ 'recipient_street' => 'streetName', 'recipient_street_number' => 'streetNumber', 'recipient_postal_code' => 'postal code', 'recipient_city' => 'city', 'recipient_country' => 'countryISOCode', ]; $errors = []; foreach ($requiredFields as $field => $errorName) { if (empty(trim($shipmentData[$field]))) { $errors[] = $errorName; } } if (empty(trim($shipmentData['recipient_name'])) && empty(trim($shipmentData['recipient_company']))) { $errors[] = 'recipient name or company'; } if (!empty($errors)) { $errorMessage = 'Shipment data is missing required fields for Order ID ' . $orderId . ': ' . implode(', ', $errors); throw new \Exception($errorMessage); } } /** * Cancel a shipment * * @param DhlShipment $shipment * @return bool Success status * @throws Exception */ public function cancelShipment(DhlShipment $shipment): bool { try { if (!$shipment->canBeCancelled()) { throw new Exception('Shipment cannot be cancelled in current status: ' . $shipment->status); } $this->logInfo('Cancelling DHL shipment', [ 'shipment_id' => $shipment->id, 'shipment_number' => $shipment->shipment_number ]); $request = new deleteShipmentOrder([$shipment->shipment_number]); // Constructor expects array of shipment numbers $response = $this->shippingClient->deleteShipmentOrder($request); if ($this->isResponseSuccessful($response)) { $shipment->update([ 'status' => DhlShipment::STATUS_CANCELLED, 'api_response_data' => $this->extractResponseData($response), ]); $this->logInfo('DHL shipment cancelled successfully', [ 'shipment_id' => $shipment->id ]); return true; } else { $errorMessage = $this->extractErrorMessage($response); $shipment->update([ 'api_errors' => $errorMessage, ]); throw new Exception('DHL cancellation failed: ' . $errorMessage); } } catch (DhlException $e) { $this->logError('DHL API error during cancellation', $e, ['shipment_id' => $shipment->id]); throw new Exception('DHL shipment cancellation failed: ' . $e->getMessage()); } catch (Exception $e) { $this->logError('General error during cancellation', $e, ['shipment_id' => $shipment->id]); throw $e; } } /** * Create a return label * * @param DhlShipment $originalShipment The original outbound shipment * @param array $options Additional options * @return DhlShipment The return shipment * @throws Exception */ public function createReturnLabel(DhlShipment $originalShipment, array $options = []): DhlShipment { try { if (!$this->config['returns']['enabled']) { throw new Exception('Return labels are disabled in configuration'); } $this->logInfo('Creating DHL return label', [ 'original_shipment_id' => $originalShipment->id, 'options' => $options ]); // Create return shipment using regular createShipment but with return data $returnShipment = $this->createShipment( $originalShipment->shoppingOrder, $originalShipment->weight, array_merge($options, ['is_return' => true, 'original_shipment' => $originalShipment]) ); $this->logInfo('DHL return label created successfully', [ 'return_shipment_id' => $returnShipment->id, 'original_shipment_id' => $originalShipment->id ]); return $returnShipment; } catch (Exception $e) { $this->logError('Error creating return label', $e, [ 'original_shipment_id' => $originalShipment->id ]); throw $e; } } /** * Get tracking details for a shipment * * @param DhlShipment $shipment * @return array Tracking details * @throws Exception */ public function getTrackingDetails(DhlShipment $shipment): array { try { if (!$this->config['tracking']['enabled']) { throw new Exception('Tracking is disabled in configuration'); } if (!$shipment->hasTracking()) { throw new Exception('Shipment has no tracking number'); } if (!$this->trackingClient) { throw new Exception('Tracking client not initialized'); } $this->logInfo('Getting tracking details', [ 'shipment_id' => $shipment->id, 'tracking_number' => $shipment->tracking_number ]); $request = new getPieceDetail(); $request->pieceCode = $shipment->tracking_number; $response = $this->trackingClient->getPieceDetail($request); if ($response && is_array($response) && count($response) > 0) { $trackingData = method_exists($response[0], 'toArray') ? $response[0]->toArray() : (array)$response[0]; // Update shipment with latest tracking info $shipment->update([ 'tracking_details' => $trackingData, 'last_tracked_at' => now(), 'tracking_status' => $trackingData['status'] ?? null, ]); $this->logInfo('Tracking details updated', [ 'shipment_id' => $shipment->id, 'status' => $trackingData['status'] ?? 'unknown' ]); return $trackingData; } else { throw new Exception('No tracking data available'); } } catch (DhlException $e) { $this->logError('DHL API error during tracking', $e, ['shipment_id' => $shipment->id]); throw new Exception('DHL tracking failed: ' . $e->getMessage()); } catch (Exception $e) { $this->logError('General error during tracking', $e, ['shipment_id' => $shipment->id]); throw $e; } } /** * Build shipment data from order * * @param ShoppingOrder $order * @param float $weight * @param array $options * @return array */ private function buildShipmentData(ShoppingOrder $order, float $weight, array $options): array { $isReturn = $options['is_return'] ?? false; $originalShipment = $options['original_shipment'] ?? null; $shoppingUser = $order->shopping_user; // Data placeholders $recipientName = ''; $recipientCompany = null; $streetName = ''; $streetNumber = ''; $recipientPostalCode = ''; $recipientCity = ''; $recipientCountryCode = ''; $recipientEmail = $shoppingUser->email ?? null; // Fallback to user's main email $recipientPhone = null; $customAddress = $options['shipping_address'] ?? null; if ($customAddress) { // Use custom address from options (e.g., modal) $recipientName = trim(($customAddress['firstname'] ?? '') . ' ' . ($customAddress['lastname'] ?? '')); $recipientCompany = $customAddress['company'] ?? null; $streetName = $customAddress['address'] ?? ''; $streetNumber = $customAddress['address_2'] ?? ''; $recipientPostalCode = $customAddress['zipcode'] ?? ''; $recipientCity = $customAddress['city'] ?? ''; $recipientPhone = $customAddress['phone'] ?? null; if (!empty($customAddress['country_id'])) { $country = \App\Models\Country::find($customAddress['country_id']); if ($country) { $recipientCountryCode = $country->code; } } } else { // Fallback to shopping_user data $useShipping = !($shoppingUser->same_as_billing ?? true); $firstname = $useShipping ? ($shoppingUser->shipping_firstname ?? '') : ($shoppingUser->billing_firstname ?? ''); $lastname = $useShipping ? ($shoppingUser->shipping_lastname ?? '') : ($shoppingUser->billing_lastname ?? ''); $company = $useShipping ? ($shoppingUser->shipping_company ?? '') : ($shoppingUser->billing_company ?? ''); $address = $useShipping ? ($shoppingUser->shipping_address ?? '') : ($shoppingUser->billing_address ?? ''); $address_2 = $useShipping ? ($shoppingUser->shipping_address_2 ?? '') : ($shoppingUser->billing_address_2 ?? ''); $zipcode = $useShipping ? ($shoppingUser->shipping_zipcode ?? '') : ($shoppingUser->billing_zipcode ?? ''); $city = $useShipping ? ($shoppingUser->shipping_city ?? '') : ($shoppingUser->billing_city ?? ''); $country = $useShipping ? ($shoppingUser->shipping_country ?? null) : ($shoppingUser->billing_country ?? null); $phone = $useShipping ? ($shoppingUser->shipping_phone ?? '') : ($shoppingUser->billing_phone ?? ''); $email = $shoppingUser->billing_email ?? ''; $recipientName = trim($firstname . ' ' . $lastname); $recipientCompany = $company; $streetName = $address; //$streetNumber = $address_2; $recipientPostalCode = $zipcode; $recipientCity = $city; $recipientCountryCode = $country->code ?? ''; $recipientPhone = $phone; $recipientEmail = $email; } // Universal address parsing for combined street/number fields if (empty($streetNumber) && !empty($streetName)) { if (preg_match('/^([^\d]*[^\d\s])\s*(\d.*)$/', $streetName, $matches) && count($matches) === 3) { $streetName = trim($matches[1]); $streetNumber = trim($matches[2]); } } // --- START FIX: Automatically determine product code based on destination country --- $isDomestic = strtoupper($recipientCountryCode) === 'DE'; $productCode = $options['product_code'] ?? ($isDomestic ? $this->config['defaults']['product'] : $this->config['defaults']['product_international']); // --- END FIX --- return [ 'order_id' => $order->id, 'type' => $isReturn ? DhlShipment::TYPE_RETURN : DhlShipment::TYPE_OUTBOUND, 'related_shipment_id' => $originalShipment?->id, 'weight' => $weight, 'length' => $options['length'] ?? $this->config['defaults']['dimensions']['length'], 'width' => $options['width'] ?? $this->config['defaults']['dimensions']['width'], 'height' => $options['height'] ?? $this->config['defaults']['dimensions']['height'], 'product_code' => $productCode, 'services' => $options['services'] ?? [], // Recipient address 'recipient_name' => $recipientName, 'recipient_company' => $recipientCompany, 'recipient_street' => $streetName, 'recipient_street_number' => $streetNumber, 'recipient_postal_code' => $recipientPostalCode, 'recipient_city' => $recipientCity, 'recipient_state' => null, // Not used 'recipient_country' => $recipientCountryCode, 'recipient_email' => $recipientEmail, 'recipient_phone' => $recipientPhone, ]; } /** * Create DHL shipment record in database * * @param ShoppingOrder $order * @param array $shipmentData * @return DhlShipment */ private function createDhlShipmentRecord(ShoppingOrder $order, array $shipmentData): DhlShipment { return DhlShipment::create([ 'shopping_order_id' => $order->id, 'type' => $shipmentData['type'], 'related_shipment_id' => $shipmentData['related_shipment_id'], 'weight' => $shipmentData['weight'], 'length' => $shipmentData['length'], 'width' => $shipmentData['width'], 'height' => $shipmentData['height'], 'product_code' => $shipmentData['product_code'], 'services' => $shipmentData['services'], 'status' => DhlShipment::STATUS_CREATED, // Recipient data 'recipient_name' => $shipmentData['recipient_name'], 'recipient_company' => $shipmentData['recipient_company'], 'recipient_street' => $shipmentData['recipient_street'], 'recipient_street_number' => $shipmentData['recipient_street_number'], 'recipient_postal_code' => $shipmentData['recipient_postal_code'], 'recipient_city' => $shipmentData['recipient_city'], 'recipient_state' => $shipmentData['recipient_state'], 'recipient_country' => $shipmentData['recipient_country'], 'recipient_email' => $shipmentData['recipient_email'], 'recipient_phone' => $shipmentData['recipient_phone'], ]); } /** * Build ShipmentOrder object for DHL API * * @param array $shipmentData * @param DhlShipment $dhlShipment * @return ShipmentOrder */ private function buildShipmentOrder(array $shipmentData, DhlShipment $dhlShipment): ShipmentOrder { $shipmentOrder = new ShipmentOrder(); $isReturn = $shipmentData['type'] === DhlShipment::TYPE_RETURN; $productCode = $shipmentData['product_code']; // --- DYNAMIC ACCOUNT NUMBER SELECTION --- $accountNumber = $this->getAccountNumberForProduct($productCode); // --- VALIDATE COUNTRY CODES --- // Ensure sender country code is valid $senderCountry = 'DE'; if (!empty($this->config['sender']['country']) && strlen(trim($this->config['sender']['country'])) === 2) { $senderCountry = strtoupper(trim($this->config['sender']['country'])); } // Ensure recipient country code is valid $recipientCountry = 'DE'; if (!empty($shipmentData['recipient_country']) && strlen(trim($shipmentData['recipient_country'])) === 2) { $recipientCountry = strtoupper(trim($shipmentData['recipient_country'])); } // --- END VALIDATION --- // Set basic shipment details $shipmentOrder->sequenceNumber = $dhlShipment->id; // Set shipment details $shipmentOrder->Shipment->ShipmentDetails->product = $productCode; $shipmentOrder->Shipment->ShipmentDetails->accountNumber = $accountNumber; $shipmentOrder->Shipment->ShipmentDetails->customerReference = ($isReturn ? 'Return-' : 'Order-') . $shipmentData['order_id']; $shipmentOrder->Shipment->ShipmentDetails->shipmentDate = date('Y-m-d'); // --- ROBUST NAME & ADDRESS HANDLING --- if ($isReturn) { // RETURN LABEL: Customer ships TO warehouse // Shipper = Customer (original recipient) $this->setAddressBlock( $shipmentOrder->Shipment->Shipper, $shipmentData['recipient_name'], $shipmentData['recipient_company'], $shipmentData['recipient_street'], $shipmentData['recipient_street_number'], $shipmentData['recipient_postal_code'], $shipmentData['recipient_city'], $recipientCountry ); // Receiver = Warehouse (original sender) $this->setAddressBlock( $shipmentOrder->Shipment->Receiver, $this->config['sender']['name'] ?? '', $this->config['sender']['company'] ?? '', $this->config['sender']['street'] ?? '', $this->config['sender']['street_number'] ?? '', $this->config['sender']['postal_code'] ?? '', $this->config['sender']['city'] ?? '', $senderCountry, true // isSender=true to throw exception on config error ); } else { // OUTBOUND LABEL: Warehouse ships TO customer (normal flow) // Shipper = Warehouse $this->setAddressBlock( $shipmentOrder->Shipment->Shipper, $this->config['sender']['name'] ?? '', $this->config['sender']['company'] ?? '', $this->config['sender']['street'] ?? '', $this->config['sender']['street_number'] ?? '', $this->config['sender']['postal_code'] ?? '', $this->config['sender']['city'] ?? '', $senderCountry, true // isSender=true to throw exception on config error ); // Receiver = Customer $this->setAddressBlock( $shipmentOrder->Shipment->Receiver, $shipmentData['recipient_name'], $shipmentData['recipient_company'], $shipmentData['recipient_street'], $shipmentData['recipient_street_number'], $shipmentData['recipient_postal_code'], $shipmentData['recipient_city'], $recipientCountry ); } // Set package details $shipmentOrder->Shipment->ShipmentDetails->ShipmentItem->weightInKG = $shipmentData['weight']; $shipmentOrder->Shipment->ShipmentDetails->ShipmentItem->lengthInCM = $shipmentData['length']; $shipmentOrder->Shipment->ShipmentDetails->ShipmentItem->widthInCM = $shipmentData['width']; $shipmentOrder->Shipment->ShipmentDetails->ShipmentItem->heightInCM = $shipmentData['height']; // Configure minimal services to avoid SDK issues with dynamic properties $this->configureShipmentServices($shipmentOrder, $shipmentData); return $shipmentOrder; } /** * Sets the address and name details for a shipper or receiver block. * * @param Shipper|Receiver $addressBlock The Shipper or Receiver object from the SDK * @param string $name * @param string|null $company * @param string $street * @param string $streetNumber * @param string $postalCode * @param string $city * @param string $countryCode * @param bool $isSender * @throws \Exception */ private function setAddressBlock(Shipper|Receiver &$addressBlock, string $name, ?string $company, string $street, string $streetNumber, string $postalCode, string $city, string $countryCode, bool $isSender = false): void { $name = trim($name); $company = trim($company ?? ''); $name1 = $name; $name2 = $company; if (empty($name1)) { // If personal name is empty, company MUST be name1 $name1 = $name2; $name2 = ''; } if (empty($name1)) { if ($isSender) { throw new \Exception('DHL Sender Name (name1) is not configured. Please set DHL_SENDER_NAME or DHL_SENDER_COMPANY in your .env file.'); } return; } // Handle the structural difference between Shipper (has ->Name object) and Receiver (has ->name1 directly) if ($addressBlock instanceof Shipper) { if (is_null($addressBlock->Name)) $addressBlock->Name = new ShipperName(); $addressBlock->Name->name1 = $name1; if (!empty($name2)) $addressBlock->Name->name2 = $name2; if (is_null($addressBlock->Address)) $addressBlock->Address = new ShipperAddress(); if (is_null($addressBlock->Address->Origin)) $addressBlock->Address->Origin = new ShipperOrigin(); } elseif ($addressBlock instanceof Receiver) { $addressBlock->name1 = $name1; // Assign directly if (is_null($addressBlock->Address)) $addressBlock->Address = new ReceiverAddress(); if (is_null($addressBlock->Address->Origin)) $addressBlock->Address->Origin = new ReceiverOrigin(); } $addressBlock->Address->streetName = $street; $addressBlock->Address->streetNumber = $streetNumber; $addressBlock->Address->zip = $postalCode; $addressBlock->Address->city = $city; $addressBlock->Address->Origin->countryISOCode = $countryCode; } /** * Configure shipment services to avoid SDK dynamic property issues * * @param ShipmentOrder $shipmentOrder * @param array $shipmentData */ private function configureShipmentServices(ShipmentOrder $shipmentOrder, array $shipmentData): void { // Initialize basic services that are commonly used and properly supported $services = $shipmentData['services'] ?? []; // Only configure services that are explicitly requested and known to work // This prevents the SDK from creating dynamic properties like ShipmentHandling if (isset($services['premium']) && $services['premium']) { $shipmentOrder->Shipment->ShipmentDetails->Service->Premium->active = true; } if (isset($services['endorsement']) && $services['endorsement']) { $shipmentOrder->Shipment->ShipmentDetails->Service->Endorsement->active = true; $shipmentOrder->Shipment->ShipmentDetails->Service->Endorsement->type = $services['endorsement']; } if (isset($services['bulky_goods']) && $services['bulky_goods']) { $shipmentOrder->Shipment->ShipmentDetails->Service->BulkyGoods->active = true; } if (isset($services['return_receipt']) && $services['return_receipt']) { $shipmentOrder->Shipment->ShipmentDetails->Service->ReturnReceipt->active = true; } // Avoid problematic services that cause dynamic property warnings // Do NOT set ShipmentHandling or other services that the SDK dynamically creates $this->logInfo('Configured DHL services', [ 'requested_services' => array_keys($services), 'product_code' => $shipmentData['product_code'] ]); } /** * Get the correct DHL account number for a given product code. * * @param string $productCode * @return string * @throws \Exception */ private function getAccountNumberForProduct(string $productCode): string { $productAccounts = $this->config['api']['product_accounts'] ?? []; if (isset($productAccounts[$productCode]) && !empty($productAccounts[$productCode])) { return $productAccounts[$productCode]; } $defaultAccount = $this->config['api']['account_number_default'] ?? null; if (!empty($defaultAccount)) { return $defaultAccount; } throw new \Exception("DHL Abrechnungsnummer for product '{$productCode}' is not configured, and no default account number is set."); } /** * Process shipment response from DHL API * * @param DhlShipment $dhlShipment * @param CreateShipmentOrderResponse $response * @param createShipmentOrder $request * @throws Exception */ private function processShipmentResponse(DhlShipment $dhlShipment, $response, $request): void { // Store request and response data $dhlShipment->update([ 'api_request_data' => $this->extractRequestData($request), 'api_response_data' => $this->extractResponseData($response), ]); $isSuccessful = $this->isResponseSuccessful($response); $this->logInfo('Processing DHL shipment response', [ 'shipment_id' => $dhlShipment->id, 'is_successful' => $isSuccessful, 'has_creation_states' => !empty($response->CreationStates), 'creation_states_count' => count($response->CreationStates ?? []) ]); if ($isSuccessful) { // Extract shipment and tracking information $shipmentData = $this->extractShipmentData($response); $this->logInfo('Extracted shipment data', [ 'shipment_id' => $dhlShipment->id, 'shipment_number' => $shipmentData['shipment_number'] ?? 'not_found', 'has_label_data' => !empty($shipmentData['label_data']) ]); // Validate that we got essential data if (empty($shipmentData['shipment_number'])) { $this->logError('No shipment number in successful response', new Exception('Missing shipment number'), [ 'shipment_id' => $dhlShipment->id, 'response_data' => $this->extractResponseData($response) ]); $dhlShipment->update([ 'status' => DhlShipment::STATUS_FAILED, 'api_errors' => 'DHL API reported success but no shipment number was provided', ]); throw new Exception('DHL API reported success but no shipment number was provided'); } // Save label if provided $labelPath = null; if (isset($shipmentData['label_data']) && $shipmentData['label_data']) { try { $labelPath = $this->saveLabelFile($dhlShipment, $shipmentData['label_data']); } catch (Exception $e) { $this->logError('Failed to save label file', $e, ['shipment_id' => $dhlShipment->id]); // Don't fail the whole process for label save issues } } // Update shipment with success data $dhlShipment->update([ 'shipment_number' => $shipmentData['shipment_number'], 'tracking_number' => $shipmentData['tracking_number'] ?? $shipmentData['shipment_number'], 'label_path' => $labelPath, 'status' => DhlShipment::STATUS_SUBMITTED, 'shipped_at' => now(), ]); } else { // Handle API error $errorMessage = $this->extractErrorMessage($response); $this->logInfo('DHL API returned errors', [ 'shipment_id' => $dhlShipment->id, 'error_message' => $errorMessage ]); $dhlShipment->update([ 'status' => DhlShipment::STATUS_FAILED, 'api_errors' => $errorMessage, ]); throw new Exception('DHL API error: ' . $errorMessage); } } /** * Extract shipment data from response * * @param CreateShipmentOrderResponse $response * @return array */ private function extractShipmentData($response): array { $data = []; // Use the SDK's proper structure with CreationStates if ($response instanceof CreateShipmentOrderResponse && !empty($response->CreationStates)) { foreach ($response->CreationStates as $creationState) { if (isset($creationState->shipmentNumber)) { $data['shipment_number'] = $creationState->shipmentNumber; } if (isset($creationState->LabelData->labelData)) { $data['label_data'] = $creationState->LabelData->labelData; } // Only process the first creation state for now (single shipment) break; } } return $data; } /** * Check if response indicates success * * @param CreateShipmentOrderResponse|DeleteShipmentOrderResponse $response * @return bool */ private function isResponseSuccessful($response): bool { // Use the SDK's built-in success check methods if ($response instanceof CreateShipmentOrderResponse) { return $response->hasNoErrors(); } elseif ($response instanceof DeleteShipmentOrderResponse) { return $response->hasNoErrors(); } return false; } /** * Extract error message from response * * @param CreateShipmentOrderResponse|DeleteShipmentOrderResponse $response * @return string */ private function extractErrorMessage($response): string { $messages = []; // Check for top-level status messages if (is_object($response) && property_exists($response, 'Status') && !empty($response->Status)) { $statusArray = is_array($response->Status) ? $response->Status : [$response->Status]; foreach ($statusArray as $status) { if (is_object($status)) { // Try different status property names $statusProps = ['statusText', 'statusMessage', 'statusCode']; foreach ($statusProps as $prop) { if (property_exists($status, $prop) && !empty($status->{$prop})) { $value = $status->{$prop}; $messages[] = is_array($value) ? implode(', ', $value) : (string)$value; break; } } } } } // Check for messages within CreationStates if ($response instanceof CreateShipmentOrderResponse && !empty($response->CreationStates)) { foreach ($response->CreationStates as $creationState) { // Check LabelData Status if (isset($creationState->LabelData->Status)) { $statusArray = is_array($creationState->LabelData->Status) ? $creationState->LabelData->Status : [$creationState->LabelData->Status]; foreach ($statusArray as $status) { if (is_object($status)) { $statusProps = ['statusText', 'statusMessage', 'statusCode']; foreach ($statusProps as $prop) { if (property_exists($status, $prop) && !empty($status->{$prop})) { $value = $status->{$prop}; $messages[] = is_array($value) ? implode('; ', $value) : (string)$value; break; } } } } } // Check direct status on CreationState if (isset($creationState->sequenceNumber) && isset($creationState->LabelData)) { // This might be a successful creation state, not an error continue; } } } // If we still have no messages, check if this might actually be a success if (empty($messages) && $response instanceof CreateShipmentOrderResponse) { if ($response->hasNoErrors()) { return 'No errors found - response appears successful'; } else { // Try to extract any available information from the response $responseData = $this->extractResponseData($response); $this->logInfo('Could not extract error message, full response data:', $responseData ?? []); return 'DHL API returned errors but no specific error message could be extracted. Check logs for full response.'; } } return !empty($messages) ? implode('; ', array_unique($messages)) : 'Unknown DHL API error'; } /** * Extract request data for logging * * @param createShipmentOrder|deleteShipmentOrder $request * @return array|null */ private function extractRequestData($request): ?array { // Safely convert complex SDK objects to arrays for logging return json_decode(json_encode($request), true); } /** * Extract response data for logging * * @param CreateShipmentOrderResponse|DeleteShipmentOrderResponse $response * @return array|null */ private function extractResponseData($response): ?array { // Safely convert complex SDK objects to arrays for logging return json_decode(json_encode($response), true); } /** * Save label file to storage * * @param DhlShipment $dhlShipment * @param string $labelData Base64 encoded label data * @return string The saved file path */ private function saveLabelFile(DhlShipment $dhlShipment, string $labelData): string { $filename = 'dhl_label_' . $dhlShipment->id . '_' . time() . '.pdf'; $path = 'dhl/labels/' . $filename; Storage::put($path, base64_decode($labelData)); return $path; } /** * Log info message * * @param string $message * @param array $context */ private function logInfo(string $message, array $context = []): void { if ($this->config['logging']['enabled']) { Log::info('[DHL API] ' . $message, $context); } } /** * Log error message * * @param string $message * @param Exception $exception * @param array $context */ private function logError(string $message, Exception $exception, array $context = []): void { if ($this->config['logging']['enabled']) { Log::error('[DHL API] ' . $message, array_merge($context, [ 'exception' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), ])); } } /** * Log detailed response structure for debugging * * @param CreateShipmentOrderResponse $response */ private function logResponseStructure($response): void { if (!$this->config['logging']['enabled']) { return; } $structure = [ 'class' => get_class($response), 'hasNoErrors' => $response->hasNoErrors(), 'properties' => [] ]; // Get all public properties $reflection = new \ReflectionClass($response); foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); try { $value = $property->getValue($response); if (is_object($value)) { $structure['properties'][$name] = [ 'type' => 'object', 'class' => get_class($value), 'properties' => $this->extractObjectProperties($value, 2) // Limit depth ]; } elseif (is_array($value)) { $structure['properties'][$name] = [ 'type' => 'array', 'count' => count($value), 'sample' => count($value) > 0 ? $this->extractObjectProperties($value[0], 1) : null ]; } else { $structure['properties'][$name] = [ 'type' => gettype($value), 'value' => $value ]; } } catch (\Exception $e) { $structure['properties'][$name] = [ 'type' => 'error', 'error' => $e->getMessage() ]; } } Log::info('[DHL API] Response structure analysis:', $structure); } /** * Extract object properties for debugging (with depth limit) * * @param mixed $object * @param int $maxDepth * @return array|mixed */ private function extractObjectProperties($object, int $maxDepth = 1) { if ($maxDepth <= 0 || !is_object($object)) { return is_object($object) ? get_class($object) : $object; } $properties = []; $reflection = new \ReflectionClass($object); foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { $name = $property->getName(); try { $value = $property->getValue($object); if (is_object($value)) { $properties[$name] = $this->extractObjectProperties($value, $maxDepth - 1); } elseif (is_array($value)) { $properties[$name] = [ 'type' => 'array', 'count' => count($value) ]; } else { $properties[$name] = $value; } } catch (\Exception $e) { $properties[$name] = 'Error: ' . $e->getMessage(); } } return $properties; } /** * Check if service is properly configured * * @return bool */ public function isConfigured(): bool { $apiType = $this->config['api']['api_type']; if ($apiType === 'developer') { return !empty($this->config['api']['api_key']) && !empty($this->config['api']['api_secret']); } else { return !empty($this->config['api']['username']) && !empty($this->config['api']['password']) && !empty($this->config['api']['account_number']); } } /** * Test API connection * * @return array Test results */ public function testConnection(): array { try { // Simple test - check if clients are initialized if ($this->shippingClient) { return [ 'success' => true, 'message' => 'DHL API clients initialized successfully', 'sandbox' => $this->isSandbox, 'configured' => $this->isConfigured(), 'api_type' => $this->config['api']['api_type'], ]; } else { throw new Exception('Shipping client not initialized'); } } catch (Exception $e) { return [ 'success' => false, 'message' => 'DHL API connection failed: ' . $e->getMessage(), 'sandbox' => $this->isSandbox, 'configured' => $this->isConfigured(), ]; } } }