1312 lines
No EOL
52 KiB
PHP
1312 lines
No EOL
52 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\DhlShipment;
|
|
use App\Models\ShoppingOrder;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\MultiClient;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\ShippingClient;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\TrackingClient;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Credentials\ShippingClientCredentials;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Credentials\TrackingClientCredentials;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Request\Shipping\createShipmentOrder;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Request\Shipping\deleteShipmentOrder;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Request\Tracking\getPieceDetail;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Request\Shipping\getVersion;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Response\Shipping\createShipmentOrder as CreateShipmentOrderResponse;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Response\Shipping\deleteShipmentOrder as DeleteShipmentOrderResponse;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Exception\DhlException;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Shipper;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Shipper\Name as ShipperName;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Shipper\Address as ShipperAddress;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Shipper\Address\Origin as ShipperOrigin;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Receiver;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Receiver\Name as ReceiverName;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Receiver\Address as ReceiverAddress;
|
|
use ChristophSchaeffer\Dhl\BusinessShipping\Resource\ShipmentOrder\Shipment\Receiver\Address\Origin as ReceiverOrigin;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* DHL API Service
|
|
*
|
|
* Central service class that wraps the DHL SDK and provides a clean interface
|
|
* for all DHL operations including shipping, tracking, and return labels.
|
|
*/
|
|
class DhlApiService
|
|
{
|
|
/**
|
|
* @var ShippingClient
|
|
*/
|
|
private $shippingClient;
|
|
|
|
/**
|
|
* @var TrackingClient
|
|
*/
|
|
private $trackingClient;
|
|
|
|
/**
|
|
* @var array DHL configuration
|
|
*/
|
|
private $config;
|
|
|
|
/**
|
|
* @var bool Whether to use sandbox mode
|
|
*/
|
|
private $isSandbox;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->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(),
|
|
];
|
|
}
|
|
}
|
|
} |