23-01-2026

This commit is contained in:
Kevin Adametz 2026-01-23 17:35:23 +01:00
parent a939cd51ef
commit a8b395e20d
248 changed files with 29342 additions and 4805 deletions

View file

@ -2,10 +2,10 @@
namespace Acme\Dhl\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\ShoppingOrder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* DHL Shipment Model for both outbound shipments and returns
@ -29,17 +29,22 @@ class DhlShipment extends Model
'firstname',
'lastname',
'company',
'email',
'postnumber',
'recipient',
'tracking_status',
'last_tracked_at',
'api_response_data'
'tracking_email_sent_at',
'tracking_email_type',
'api_response_data',
];
protected $casts = [
'recipient' => 'array',
'api_response_data' => 'array',
'last_tracked_at' => 'datetime',
'weight_kg' => 'decimal:3'
'tracking_email_sent_at' => 'datetime',
'weight_kg' => 'decimal:3',
];
public const STATUS_MAP = [
@ -50,10 +55,9 @@ class DhlShipment extends Model
'exception' => 'exception',
'returned' => 'returned',
'failed' => 'failed',
'unknown' => 'unknown'
'unknown' => 'unknown',
];
/**
* Get the tracking events for this shipment
*/
@ -184,4 +188,91 @@ class DhlShipment extends Model
{
return __('dhl.product_codes.' . $this->product_code, [], $this->product_code);
}
/**
* Get DHL tracking URL for this shipment
*/
public function getTrackingUrl(): string
{
return 'https://www.dhl.de/de/privatkunden/pakete-empfangen/verfolgen.html?piececode=' . $this->dhl_shipment_no;
}
/**
* Check if tracking email can be sent
*/
public function canSendTrackingEmail(): bool
{
if (empty($this->dhl_shipment_no)) {
return false;
}
if (! $this->shoppingOrder) {
return false;
}
// Check if email is available in shipment record (new field)
if (! empty($this->email)) {
return true;
}
// Fallback: check shopping user email
$shoppingUser = $this->shoppingOrder->shopping_user;
if (! $shoppingUser || empty($shoppingUser->email)) {
return false;
}
return true;
}
/**
* Check if tracking email was already sent
*/
public function wasTrackingEmailSent(): bool
{
return $this->tracking_email_sent_at !== null;
}
/**
* Mark tracking email as sent
*/
public function markTrackingEmailSent(string $type = 'manual'): void
{
$this->update([
'tracking_email_sent_at' => now(),
'tracking_email_type' => $type,
]);
}
/**
* Get status badge class for Bootstrap
*/
public function getStatusBadgeClass(): string
{
return match ($this->status) {
'created', 'pending' => 'secondary',
'in_transit' => 'info',
'out_for_delivery' => 'primary',
'delivered' => 'success',
'exception', 'failed' => 'danger',
'returned', 'canceled' => 'warning',
default => 'secondary'
};
}
/**
* Scope for active shipments (not delivered or canceled)
*/
public function scopeActive($query)
{
return $query->whereNotIn('status', ['delivered', 'canceled', 'returned', 'failed']);
}
/**
* Scope for shipments that need tracking email
*/
public function scopeNeedsTrackingEmail($query)
{
return $query->where('status', 'in_transit')
->whereNull('tracking_email_sent_at');
}
}

View file

@ -4,6 +4,7 @@ namespace Acme\Dhl\Services;
use Acme\Dhl\Support\DhlClient;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Services\ShippingService;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Exception;
@ -34,22 +35,167 @@ class ReturnsService
return ['queued' => true];
}
Log::info('[DHL Returns] Creating return label', [
'order_id' => $returnData['order_id'] ?? null,
'original_shipment_id' => $returnData['original_shipment_id'] ?? null,
]);
// Try Returns API first, fallback to regular shipment if not authorized
try {
return $this->createReturnViaReturnsAPI($returnData);
} catch (Exception $e) {
// Check if it's an authentication/permission error
if (
str_contains($e->getMessage(), 'authentication') ||
str_contains($e->getMessage(), 'not allowed') ||
str_contains($e->getMessage(), '401') ||
str_contains($e->getMessage(), '403')
) {
Log::warning('[DHL Returns] Returns API not available, falling back to regular shipment', [
'error' => $e->getMessage(),
]);
return $this->createReturnViaRegularShipment($returnData);
}
// Re-throw other errors
throw $e;
}
}
/**
* Create return label using DHL Returns API
*/
private function createReturnViaReturnsAPI(array $returnData): array
{
$payload = $this->buildReturnPayload($returnData);
Log::info('[DHL Returns] Using Returns API endpoint');
$response = $this->client->request('post', '/parcel/de/returns/v1/labels', $payload);
Log::info('[DHL Returns] Returns API Response received', [
'response' => $response,
]);
$returnNumber = $this->extractReturnNumber($response);
$labelBase64 = $this->extractLabelData($response);
$labelPath = $this->saveLabelFile($returnNumber, $labelBase64, $payload['labelFormat']);
if (!$returnNumber) {
Log::error('[DHL Returns] Failed to extract return number', [
'response' => $response,
]);
throw new Exception('Failed to extract return shipment number from DHL API response');
}
if (!$labelBase64) {
Log::warning('[DHL Returns] No label data in response', [
'return_number' => $returnNumber,
]);
}
$labelPath = $this->saveLabelFile($returnNumber, $labelBase64, $returnData['label_format'] ?? 'PDF');
$returnShipment = $this->createReturnRecord($returnData, $returnNumber, $labelPath, $response);
Log::info('Created return label', ['returnNumber' => $returnNumber]);
Log::info('[DHL Returns] Return label created successfully via Returns API', [
'returnNumber' => $returnNumber,
'labelPath' => $labelPath,
]);
return [
'returnNumber' => $returnNumber,
'label_path' => $labelPath,
'returnShipment' => $returnShipment,
'raw' => $response
'raw' => $response,
'method' => 'returns_api'
];
}
/**
* Fallback: Create return label using regular shipping API with swapped addresses
*/
private function createReturnViaRegularShipment(array $returnData): array
{
Log::info('[DHL Returns] Using regular Shipping API as fallback', [
'original_data' => $returnData,
]);
// Use ShippingService with swapped addresses
$shippingService = new ShippingService($this->client);
// Convert addresses to ShippingService format (2-letter country codes)
$shipper = $this->convertAddressFor2LetterCountry($returnData['shipper']);
$consignee = $this->convertAddressFor2LetterCountry($returnData['consignee']);
// Get DHL config for dimensions
$settingController = new \App\Http\Controllers\SettingController();
$dhlConfig = $settingController->getDhlConfig();
// Prepare data for regular shipment (shipper and consignee are already swapped)
// NOTE: Use V01PAK instead of V07PAK since V07PAK might not be available
// The swapped addresses indicate it's a return
$shipmentData = [
'order_id' => $returnData['order_id'] ?? null,
'weight_kg' => $returnData['weight_kg'] ?? 2.5,
'product_code' => 'V01PAK', // Standard DHL Paket (V07PAK might not be available)
'label_format' => $returnData['label_format'] ?? 'PDF',
'print_format' => $dhlConfig['retoure_print_format'] ?? $dhlConfig['print_format'] ?? 'A4',
// Shipper = Customer (sending back)
'shipper' => $shipper,
// Consignee = Our warehouse (receiving return)
'consignee' => $consignee,
// Dimensions - use V01PAK dimensions
'dimensions' => $dhlConfig['dimensions']['V01PAK'] ?? $dhlConfig['dimensions']['default'] ?? [
'length' => 120,
'width' => 60,
'height' => 60,
],
'reference' => 'Return-' . ($returnData['order_id'] ?? time()),
];
Log::info('[DHL Returns] Prepared shipment data for fallback', [
'shipmentData' => $shipmentData,
]);
// Create regular shipment
$result = $shippingService->createLabel($shipmentData);
// Update the shipment record to mark it as a return
$returnShipment = null;
if (isset($result['shipment']) && $result['shipment'] instanceof DhlShipment) {
$result['shipment']->update([
'type' => 'return',
'related_shipment_id' => $returnData['original_shipment_id'] ?? null,
]);
$returnShipment = $result['shipment']->fresh(); // Reload from DB
Log::info('[DHL Returns] Updated shipment to type=return', [
'shipment_id' => $returnShipment->id,
'type' => $returnShipment->type,
'related_shipment_id' => $returnShipment->related_shipment_id,
]);
} else {
Log::warning('[DHL Returns] Could not update shipment type, shipment object not found in result', [
'result_keys' => array_keys($result),
]);
}
Log::info('[DHL Returns] Return label created successfully via Shipping API fallback', [
'shipmentNumber' => $result['shipmentNumber'] ?? 'N/A',
'shipmentId' => $returnShipment->id ?? null,
]);
return [
'returnNumber' => $result['shipmentNumber'] ?? null,
'label_path' => $result['label_path'] ?? $result['labelPath'] ?? null,
'returnShipment' => $returnShipment,
'raw' => $result,
'method' => 'shipping_api_fallback'
];
}
@ -98,21 +244,34 @@ class ReturnsService
private function validateReturnData(array $data): array
{
$validator = Validator::make($data, [
'original_shipment_id' => 'nullable|integer|exists:dhl_shipments,id',
'order_id' => 'nullable|integer',
'original_shipment_id' => 'nullable|integer',
'weight_kg' => 'nullable|numeric|min:0.1',
'label_format' => 'nullable|string|in:PDF,PNG,ZPL',
// Shipper (customer returning the package)
'shipper' => 'required|array',
'shipper.name' => 'required|string|max:50',
// Add similar rules as in ShippingService
'shipper.street' => 'required|string|max:50',
'shipper.houseNumber' => 'required|string|max:10',
'shipper.postalCode' => 'required|string|max:10',
'shipper.city' => 'required|string|max:50',
'shipper.country' => 'nullable|string|min:2|max:3', // Accept both 2 and 3 letter codes
// Consignee (our warehouse)
'consignee' => 'required|array',
'consignee.name' => 'required|string|max:50',
// ... more fields
'consignee.street' => 'required|string|max:50',
'consignee.houseNumber' => 'required|string|max:10',
'consignee.postalCode' => 'required|string|max:10',
'consignee.city' => 'required|string|max:50',
'consignee.country' => 'nullable|string|min:2|max:3', // Accept both 2 and 3 letter codes
]);
if ($validator->fails()) {
throw new InvalidArgumentException($validator->errors()->first());
}
if (empty(config('dhl.billing_number'))) {
throw new InvalidArgumentException('DHL billing number must be configured');
}
return $validator->validated();
}
@ -121,11 +280,51 @@ class ReturnsService
*/
private function buildReturnPayload(array $returnData): array
{
// Clean up address data and convert country codes
$shipper = array_filter([
'name1' => $returnData['shipper']['name'] ?? '',
'name2' => $returnData['shipper']['name2'] ?? null,
'addressStreet' => $returnData['shipper']['street'] ?? '',
'addressHouse' => $returnData['shipper']['houseNumber'] ?? '',
'postalCode' => $returnData['shipper']['postalCode'] ?? '',
'city' => $returnData['shipper']['city'] ?? '',
'country' => $this->convertCountryCode($returnData['shipper']['country'] ?? 'DE'),
'email' => $returnData['shipper']['email'] ?? null,
'phone' => $returnData['shipper']['phone'] ?? null,
], function ($value) {
return $value !== null && $value !== '';
});
$consignee = array_filter([
'name1' => $returnData['consignee']['name'] ?? '',
'name2' => $returnData['consignee']['name2'] ?? null,
'addressStreet' => $returnData['consignee']['street'] ?? '',
'addressHouse' => $returnData['consignee']['houseNumber'] ?? '',
'postalCode' => $returnData['consignee']['postalCode'] ?? '',
'city' => $returnData['consignee']['city'] ?? '',
'country' => $this->convertCountryCode($returnData['consignee']['country'] ?? 'DE'),
'email' => $returnData['consignee']['email'] ?? null,
'phone' => $returnData['consignee']['phone'] ?? null,
], function ($value) {
return $value !== null && $value !== '';
});
// Get billing number from config
$settingController = new \App\Http\Controllers\SettingController();
$dhlConfig = $settingController->getDhlConfig();
$billingNumber = $dhlConfig['billing_number'] ?? config('dhl.billing_number');
if (empty($billingNumber)) {
throw new InvalidArgumentException('DHL billing number must be configured');
}
return [
'labelFormat' => $returnData['label_format'] ?? 'PDF',
'shipper' => $returnData['shipper'],
'consignee' => $returnData['consignee'],
'billingNumber' => config('dhl.billing_number')
'receiverId' => 'DEDE',
'customerReference' => 'Return-' . ($returnData['order_id'] ?? time()),
'shipmentReference' => 'Return-Order-' . ($returnData['order_id'] ?? time()),
'billingNumber' => $billingNumber,
'shipper' => $shipper,
'receiver' => $consignee,
];
}
@ -134,7 +333,9 @@ class ReturnsService
*/
private function extractReturnNumber(array $response): ?string
{
return data_get($response, 'returnShipmentNo')
return data_get($response, 'shipmentNumber')
?? data_get($response, 'returnShipmentNo')
?? data_get($response, 'items.0.shipmentNumber')
?? data_get($response, 'shipments.0.returnShipmentNo');
}
@ -143,7 +344,10 @@ class ReturnsService
*/
private function extractLabelData(array $response): ?string
{
return data_get($response, 'label');
return data_get($response, 'label.b64')
?? data_get($response, 'items.0.label.b64')
?? data_get($response, 'label')
?? data_get($response, 'items.0.label');
}
/**
@ -166,6 +370,8 @@ class ReturnsService
*/
private function createReturnRecord(array $returnData, ?string $returnNumber, ?string $labelPath, array $response): DhlShipment
{
$shipper = $returnData['shipper'] ?? [];
return DhlShipment::create([
'order_id' => $returnData['order_id'] ?? null,
'dhl_shipment_no' => $returnNumber,
@ -174,7 +380,97 @@ class ReturnsService
'label_format' => $returnData['label_format'] ?? 'PDF',
'label_path' => $labelPath,
'status' => 'created',
'weight_kg' => $returnData['weight_kg'] ?? 0,
'firstname' => $shipper['name'] ?? '',
'lastname' => '',
'company' => $shipper['name2'] ?? '',
'email' => $shipper['email'] ?? '',
'recipient' => $returnData,
'api_response_data' => $response
]);
}
/**
* Convert 2-letter country code to 3-letter country code for DHL API
*
* @param string $countryCode 2-letter or 3-letter ISO country code (e.g., "DE" or "DEU")
* @return string 3-letter ISO country code (e.g., "DEU")
*/
private function convertCountryCode(string $countryCode): string
{
$code = strtoupper(trim($countryCode));
// If already 3 letters, check if valid and return
if (strlen($code) === 3) {
$validThreeLetterCodes = ['DEU', 'AUT', 'CHE', 'FRA', 'ITA', 'ESP', 'NLD', 'BEL', 'LUX', 'POL', 'CZE', 'DNK', 'SWE', 'NOR', 'GBR', 'USA'];
return in_array($code, $validThreeLetterCodes) ? $code : 'DEU';
}
// Convert 2-letter to 3-letter
$countryMap = [
'DE' => 'DEU',
'AT' => 'AUT',
'CH' => 'CHE',
'FR' => 'FRA',
'IT' => 'ITA',
'ES' => 'ESP',
'NL' => 'NLD',
'BE' => 'BEL',
'LU' => 'LUX',
'PL' => 'POL',
'CZ' => 'CZE',
'DK' => 'DNK',
'SE' => 'SWE',
'NO' => 'NOR',
'GB' => 'GBR',
'US' => 'USA',
];
return $countryMap[$code] ?? 'DEU';
}
/**
* Convert address with 3-letter country code back to 2-letter for ShippingService
*
* @param array $address Address with 3-letter country code
* @return array Address with 2-letter country code
*/
private function convertAddressFor2LetterCountry(array $address): array
{
$converted = $address;
// Convert 3-letter to 2-letter country code
if (isset($address['country'])) {
$reverseMap = [
'DEU' => 'DE',
'AUT' => 'AT',
'CHE' => 'CH',
'FRA' => 'FR',
'ITA' => 'IT',
'ESP' => 'ES',
'NLD' => 'NL',
'BEL' => 'BE',
'LUX' => 'LU',
'POL' => 'PL',
'CZE' => 'CZ',
'DNK' => 'DK',
'SWE' => 'SE',
'NOR' => 'NO',
'GBR' => 'GB',
'USA' => 'US',
];
$code = strtoupper($address['country']);
// If it's 3 letters, convert to 2
if (strlen($code) === 3) {
$converted['country'] = $reverseMap[$code] ?? 'DE';
} else {
// Already 2 letters, keep as is
$converted['country'] = $code;
}
}
return $converted;
}
}

View file

@ -2,15 +2,15 @@
namespace Acme\Dhl\Services;
use Acme\Dhl\Support\DhlClient;
use Acme\Dhl\Jobs\CreateShipmentJob;
use Acme\Dhl\Models\DhlShipment;
use Illuminate\Support\Facades\Storage;
use Acme\Dhl\Support\DhlClient;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use InvalidArgumentException;
use Exception;
use Acme\Dhl\Jobs\CreateShipmentJob;
use Illuminate\Support\Facades\Log;
/**
* DHL Shipping Service for creating and managing shipment labels
@ -22,8 +22,9 @@ class ShippingService
/**
* Create a new DHL shipment label
*
* @param array $orderData Order and shipping data
* @param array $orderData Order and shipping data
* @return array Shipment details including number and label path
*
* @throws InvalidArgumentException When required data is missing
* @throws Exception When API request fails
*/
@ -33,6 +34,7 @@ class ShippingService
$validatedData = $this->validateOrderData($orderData);
if (config('dhl.use_queue')) {
CreateShipmentJob::dispatch($validatedData);
return ['queued' => true];
}
@ -43,7 +45,7 @@ class ShippingService
Log::info('[DHL API] Sending payload to DHL', [
'endpoint' => '/parcel/de/shipping/v2/orders',
'payload' => $payload,
'payload_json' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
'payload_json' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
]);
try {
@ -56,12 +58,12 @@ class ShippingService
$response = $this->client->request('post', '/parcel/de/shipping/v2/orders', $payload, $query);
Log::info('[DHL API] Response received', [
'response' => $response
'response' => $response,
]);
} catch (Exception $e) {
Log::error('[DHL API] Request failed', [
'error' => $e->getMessage(),
'payload' => $payload
'payload' => $payload,
]);
throw $e;
}
@ -78,7 +80,7 @@ class ShippingService
'shipmentNumber' => $shipmentNumber,
'label_path' => $labelPath,
'shipment' => $shipment,
'raw' => $response
'raw' => $response,
];
});
}
@ -86,8 +88,9 @@ class ShippingService
/**
* Cancel an existing DHL shipment
*
* @param string $shipmentNumber DHL shipment number
* @param string $shipmentNumber DHL shipment number
* @return bool Success status
*
* @throws Exception When cancellation fails
*/
public function cancelLabel(string $shipmentNumber): bool
@ -95,14 +98,48 @@ class ShippingService
if (empty($shipmentNumber)) {
throw new InvalidArgumentException('Shipment number is required');
}
$shipment = DhlShipment::where('dhl_shipment_no', $shipmentNumber)->first();
if (!$shipment || !$shipment->canCancel()) {
throw new InvalidArgumentException('Shipment cannot be canceled');
if (! $shipment) {
throw new InvalidArgumentException('Shipment not found in database: ' . $shipmentNumber);
}
if (! $shipment->canCancel()) {
throw new InvalidArgumentException('Shipment cannot be canceled (current status: ' . $shipment->status . ')');
}
Log::info('[DHL Package] Attempting to cancel shipment', [
'shipmentNumber' => $shipmentNumber,
'shipment_id' => $shipment->id,
'status' => $shipment->status,
'endpoint' => "/parcel/de/shipping/v2/orders/{$shipmentNumber}"
]);
try {
$response = $this->client->request('delete', "/parcel/de/shipping/v2/orders/{$shipmentNumber}");
Log::info('[DHL Package] Shipment cancellation response', [
'shipmentNumber' => $shipmentNumber,
'response' => $response
]);
$shipment->update(['status' => 'canceled']);
Log::info('[DHL Package] Canceled shipment successfully', [
'shipmentNumber' => $shipmentNumber,
'shipment_id' => $shipment->id
]);
return true;
} catch (\Exception $e) {
Log::error('[DHL Package] Shipment cancellation failed', [
'shipmentNumber' => $shipmentNumber,
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
'error_class' => get_class($e)
]);
throw $e;
}
$this->client->request('delete', "/parcel/de/shipping/v2/orders/{$shipmentNumber}");
$shipment->update(['status' => 'canceled']);
Log::info('Canceled shipment', ['shipmentNumber' => $shipmentNumber]);
return true;
}
/**
@ -133,17 +170,18 @@ class ShippingService
'shipper.email' => 'nullable|email|max:100',
'shipper.phone' => 'nullable|string|max:20',
// Consignee validation (recipient)
// Consignee validation (recipient)
'consignee' => 'required|array',
'consignee.name' => 'required|string|max:50',
'consignee.name2' => 'nullable|string|max:50',
'consignee.street' => 'required|string|max:50',
'consignee.houseNumber' => 'required|string|max:10',
'consignee.houseNumber' => 'required_without:consignee.postNumber|string|max:10',
'consignee.postalCode' => 'required|string|max:10',
'consignee.city' => 'required|string|max:50',
'consignee.country' => 'required|string|size:2',
'consignee.email' => 'nullable|email|max:100',
'consignee.phone' => 'nullable|string|max:20',
'consignee.postNumber' => 'nullable|string|max:20', // DHL Postnummer für Packstation/Paketbox
// Optional dimensions
'dimensions' => 'nullable|array',
@ -173,7 +211,7 @@ class ShippingService
$data['shipper'] = $this->parseAddressFields($data['shipper']);
}
// Process consignee address
// Process consignee address
if (isset($data['consignee'])) {
$data['consignee'] = $this->parseAddressFields($data['consignee']);
}
@ -187,7 +225,7 @@ class ShippingService
private function parseAddressFields(array $addressData): array
{
// If houseNumber is already provided, use it
if (!empty($addressData['houseNumber'])) {
if (! empty($addressData['houseNumber'])) {
return $addressData;
}
@ -207,14 +245,14 @@ class ShippingService
Log::info('Parsed German address', [
'original' => $street,
'parsed_street' => $parsed['street'],
'parsed_houseNumber' => $parsed['houseNumber']
'parsed_houseNumber' => $parsed['houseNumber'],
]);
} elseif (!$parsed['houseNumber']) {
} elseif (! $parsed['houseNumber']) {
// If we can't parse house number, use a default
$addressData['houseNumber'] = '1';
Log::warning('Could not parse house number from address, using default', [
'street' => $street
'street' => $street,
]);
}
@ -234,7 +272,7 @@ class ShippingService
$patterns = [
// "Musterstraße 123a" -> street: "Musterstraße", number: "123a"
'/^(.+?)\s+([0-9]+[a-zA-Z]?)\s*$/',
// "Am Markt 1-3" -> street: "Am Markt", number: "1-3"
// "Am Markt 1-3" -> street: "Am Markt", number: "1-3"
'/^(.+?)\s+([0-9]+[-\/][0-9]+[a-zA-Z]?)\s*$/',
// "Muster Str. 123" -> street: "Muster Str.", number: "123"
'/^(.+?)\s+([0-9]+)\s*$/',
@ -244,7 +282,7 @@ class ShippingService
if (preg_match($pattern, $address, $matches)) {
return [
'street' => trim($matches[1]),
'houseNumber' => trim($matches[2])
'houseNumber' => trim($matches[2]),
];
}
}
@ -252,13 +290,13 @@ class ShippingService
// If no pattern matches, return original street with empty house number
return [
'street' => $address,
'houseNumber' => null
'houseNumber' => null,
];
}
/**
* Build DHL API v2 payload from order data
*
*
* Structure follows official DHL API v2 createOrders specification:
* https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2
*/
@ -276,48 +314,36 @@ class ShippingService
// Shipper information (sender) - separate street and house number as per official spec
'shipper' => array_filter([
'name1' => $orderData['shipper']['name'] ?? '',
'name2' => !empty($orderData['shipper']['name2']) ? $orderData['shipper']['name2'] : null,
'name2' => ! empty($orderData['shipper']['name2']) ? $orderData['shipper']['name2'] : null,
'addressStreet' => $orderData['shipper']['street'] ?? '',
'addressHouse' => $orderData['shipper']['houseNumber'] ?? null,
'postalCode' => $orderData['shipper']['postalCode'] ?? '',
'city' => $orderData['shipper']['city'] ?? '',
'country' => $this->convertCountryCode($orderData['shipper']['country'] ?? 'DE'),
'email' => !empty($orderData['shipper']['email']) ? $orderData['shipper']['email'] : null,
'phone' => !empty($orderData['shipper']['phone']) ? $orderData['shipper']['phone'] : null,
'email' => ! empty($orderData['shipper']['email']) ? $orderData['shipper']['email'] : null,
'phone' => ! empty($orderData['shipper']['phone']) ? $orderData['shipper']['phone'] : null,
], function ($value) {
return $value !== null;
}),
// Consignee information (recipient) - separate street and house number as per official spec
'consignee' => array_filter([
'name1' => $orderData['consignee']['name'] ?? '',
'name2' => !empty($orderData['consignee']['name2']) ? $orderData['consignee']['name2'] : null,
'addressStreet' => $orderData['consignee']['street'] ?? '',
'addressHouse' => $orderData['consignee']['houseNumber'] ?? null,
'postalCode' => $orderData['consignee']['postalCode'] ?? '',
'city' => $orderData['consignee']['city'] ?? '',
'country' => $this->convertCountryCode($orderData['consignee']['country'] ?? 'DE'),
'email' => !empty($orderData['consignee']['email']) ? $orderData['consignee']['email'] : null,
'phone' => !empty($orderData['consignee']['phone']) ? $orderData['consignee']['phone'] : null,
], function ($value) {
return $value !== null;
}),
// Consignee information (recipient)
'consignee' => $this->buildConsigneePayload($orderData['consignee']),
'details' => [
'weight' => [
'value' => ($orderData['weight_kg'] ?? 1.0) * 1000, // Convert kg to grams
'uom' => 'g'
]
'uom' => 'g',
],
],
'print' => [
'format' => $orderData['label_format'] ?? config('dhl.label_format', 'PDF')
]
]]
'format' => $orderData['label_format'] ?? config('dhl.label_format', 'PDF'),
],
]],
];
// Add dimensions if provided (convert cm to mm)
if (!empty($orderData['dimensions'])) {
if (! empty($orderData['dimensions'])) {
$payload['shipments'][0]['details']['dim'] = [
'uom' => 'mm',
'length' => ($orderData['dimensions']['length'] ?? 30) * 10, // cm to mm
@ -327,13 +353,182 @@ class ShippingService
}
// Add custom reference if provided
if (!empty($orderData['reference'])) {
if (! empty($orderData['reference'])) {
$payload['shipments'][0]['refNo'] = $orderData['reference'];
}
return $payload;
}
/**
* Build consignee payload - handles both regular addresses and Packstation/Paketbox
*
* @param array $consignee Consignee data from order
* @return array Formatted consignee payload for DHL API
*/
private function buildConsigneePayload(array $consignee): array
{
// Check if this is a Packstation/Paketbox delivery (has postNumber)
if (! empty($consignee['postNumber'])) {
return $this->buildPackstationConsignee($consignee);
}
// Regular address
return array_filter([
'name1' => $consignee['name'] ?? '',
'name2' => ! empty($consignee['name2']) ? $consignee['name2'] : null,
'addressStreet' => $consignee['street'] ?? '',
'addressHouse' => $consignee['houseNumber'] ?? null,
'postalCode' => $consignee['postalCode'] ?? '',
'city' => $consignee['city'] ?? '',
'country' => $this->convertCountryCode($consignee['country'] ?? 'DE'),
'email' => ! empty($consignee['email']) ? $consignee['email'] : null,
'phone' => ! empty($consignee['phone']) ? $consignee['phone'] : null,
], function ($value) {
return $value !== null;
});
}
/**
* Build Packstation/Paketbox consignee payload
*
* DHL API v2 uses the Locker schema for Packstation deliveries:
* - name: Recipient name (max 50 chars)
* - lockerID: Integer 100-999 (3-digit Packstation number)
* - postNumber: String 6-10 digits (DHL Postnummer)
* - postalCode, city, country: Location of the Packstation
*
* The street field should contain "Packstation XXX" or "Paketbox XXX"
*
* @see https://developer.dhl.com/api-reference/parcel-de-shipping-post-parcel-germany-v2#/components/schemas/Locker
*
* @param array $consignee Consignee data with postNumber
* @return array Formatted Locker consignee payload for DHL API
*/
private function buildPackstationConsignee(array $consignee): array
{
// Extract locker number from multiple sources:
// 1. From street field (e.g., "Packstation 145" -> "145")
// 2. From houseNumber field (e.g., houseNumber: "145")
// 3. Combined: street "Packstation" + houseNumber "145"
$lockerNumber = $this->extractLockerNumber(
$consignee['street'] ?? '',
$consignee['houseNumber'] ?? ''
);
// Convert to integer for DHL API (lockerID must be int 100-999)
$lockerID = (int) $lockerNumber;
Log::info('Building Packstation consignee payload (Locker schema)', [
'postNumber' => $consignee['postNumber'],
'lockerID' => $lockerID,
'originalStreet' => $consignee['street'] ?? '',
'originalHouseNumber' => $consignee['houseNumber'] ?? '',
]);
// Validate lockerID: must be integer between 100 and 999
if ($lockerID < 100 || $lockerID > 999) {
Log::error('Invalid Packstation lockerID - must be 100-999', [
'lockerID' => $lockerID,
'original_input' => $lockerNumber,
'street' => $consignee['street'] ?? '',
'houseNumber' => $consignee['houseNumber'] ?? '',
]);
$errorMessage = 'PACKSTATION-FEHLER: Die Packstation-Nummer muss 3-stellig sein (100-999).' . PHP_EOL . PHP_EOL;
$errorMessage .= 'Eingegeben wurde: "' . $lockerNumber . '"' . PHP_EOL . PHP_EOL;
$errorMessage .= 'HINWEISE:' . PHP_EOL;
$errorMessage .= '• Straße/Nr.: "Packstation 145" (nicht "Packstation 12345")' . PHP_EOL;
$errorMessage .= '• Die Packstation-NUMMER ist die 3-stellige Nummer auf dem gelben DHL-Schild' . PHP_EOL;
$errorMessage .= '• Die 5-10-stellige POSTNUMMER ist etwas anderes (kommt ins separate Feld!)' . PHP_EOL;
$errorMessage .= '• Beispiel: Packstation 145, PLZ 12345, Postnummer 1234567890';
throw new \InvalidArgumentException($errorMessage);
}
// Validate postNumber: must be 6-10 digits
$postNumber = $consignee['postNumber'] ?? '';
if (! preg_match('/^[0-9]{6,10}$/', $postNumber)) {
Log::error('Invalid DHL Postnummer - must be 6-10 digits', [
'postNumber' => $postNumber,
]);
throw new \InvalidArgumentException(
'DHL Postnummer muss 6-10 Ziffern enthalten. Bitte prüfen Sie die Postnummer.'
);
}
// DHL Locker schema - flat structure, not nested
return array_filter([
'name' => $consignee['name'] ?? '',
'lockerID' => $lockerID,
'postNumber' => $postNumber,
'postalCode' => $consignee['postalCode'] ?? '',
'city' => $consignee['city'] ?? '',
'country' => $this->convertCountryCode($consignee['country'] ?? 'DE'),
], function ($value) {
return $value !== null && $value !== '';
});
}
/**
* Extract locker number from address string or houseNumber field
*
* Examples:
* - "Packstation 145" -> "145"
* - "Paketbox 123" -> "123"
* - "PACKSTATION 987" -> "987"
* - street: "Packstation", houseNumber: "145" -> "145"
*
* @param string $address Address containing Packstation/Paketbox number
* @param string $houseNumber Optional house number field (may contain locker number)
* @return string The extracted locker number
*/
private function extractLockerNumber(string $address, string $houseNumber = ''): string
{
// Match patterns like "Packstation 145", "Paketbox 123", etc.
if (preg_match('/(?:packstation|paketbox)\s*(\d+)/i', $address, $matches)) {
return $matches[1];
}
// If address is just "Packstation" or "Paketbox" without number,
// check if houseNumber contains the locker number
if (preg_match('/^(?:packstation|paketbox)$/i', trim($address)) && ! empty($houseNumber)) {
// houseNumber might be the locker number directly
if (preg_match('/^\d+$/', trim($houseNumber))) {
Log::info('Using houseNumber as locker number', [
'address' => $address,
'houseNumber' => $houseNumber,
]);
return trim($houseNumber);
}
}
// If no pattern matches, try to extract any number from the address string
if (preg_match('/(\d+)/', $address, $matches)) {
return $matches[1];
}
// Last resort: check if houseNumber contains any number
if (! empty($houseNumber) && preg_match('/(\d+)/', $houseNumber, $matches)) {
Log::info('Extracted locker number from houseNumber field', [
'address' => $address,
'houseNumber' => $houseNumber,
'extracted' => $matches[1],
]);
return $matches[1];
}
// Fallback: return empty string (will trigger validation error with helpful message)
Log::warning('Could not extract locker number from address', [
'address' => $address,
'houseNumber' => $houseNumber,
]);
return '';
}
/**
* Convert 2-letter country code to 3-letter country code for DHL API
*/
@ -374,8 +569,9 @@ class ShippingService
Log::info('Using DHL test billing number (sandbox mode)', [
'product_code' => $productCode,
'billing_number' => $testBillingNumber,
'test_mode' => true
'test_mode' => true,
]);
return $testBillingNumber;
}
@ -386,15 +582,16 @@ class ShippingService
if ($accountNumber) {
Log::info('Using DHL account number from database settings', [
'product_code' => $productCode,
'account_number' => $accountNumber
'account_number' => $accountNumber,
]);
return $accountNumber;
}
} catch (\Exception $e) {
Log::warning('Could not load DHL account number from settings', [
'product_code' => $productCode,
'setting_key' => $settingKey,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
}
@ -403,8 +600,9 @@ class ShippingService
if ($accountNumber) {
Log::info('Using DHL account number from config file', [
'product_code' => $productCode,
'account_number' => $accountNumber
'account_number' => $accountNumber,
]);
return $accountNumber;
}
@ -413,7 +611,7 @@ class ShippingService
Log::warning('Using default billing number for product code', [
'product_code' => $productCode,
'billing_number' => $defaultBillingNumber
'billing_number' => $defaultBillingNumber,
]);
return $defaultBillingNumber;
@ -471,6 +669,10 @@ class ShippingService
$lastname = $consignee['lastname'] ?? '';
}
// Extract email and postnumber
$email = $consignee['email'] ?? '';
$postnumber = $consignee['postNumber'] ?? $consignee['postnumber'] ?? '';
// Prepare complete recipient address as JSON
$recipientData = [
'firstname' => $firstname,
@ -481,8 +683,9 @@ class ShippingService
'postalCode' => $consignee['postalCode'] ?? '',
'city' => $consignee['city'] ?? '',
'country' => $consignee['country'] ?? '',
'email' => $consignee['email'] ?? '',
'email' => $email,
'phone' => $consignee['phone'] ?? '',
'postnumber' => $postnumber,
];
return DhlShipment::create([
@ -502,7 +705,9 @@ class ShippingService
'firstname' => $firstname,
'lastname' => $lastname,
'company' => $consignee['name2'] ?? '',
'recipient' => $recipientData
'email' => $email,
'postnumber' => $postnumber,
'recipient' => $recipientData,
]);
}
@ -511,8 +716,9 @@ class ShippingService
*/
private function saveLabelFile(DhlShipment $shipment, ?string $labelBase64, string $format): ?string
{
if (!$labelBase64) {
if (! $labelBase64) {
Log::warning('No label data received for shipment', ['shipmentId' => $shipment->id]);
return null;
}
$path = 'dhl/labels/' . $shipment->dhl_shipment_no . '.' . strtolower($format);
@ -527,10 +733,11 @@ class ShippingService
usleep(1000000); // 1 second in microseconds
}
}
if (!$success) {
if (! $success) {
throw new \Exception('Failed to save label after 3 attempts');
}
$shipment->update(['label_path' => $path]);
return $path;
}
}

View file

@ -2,13 +2,13 @@
namespace Acme\Dhl\Support;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\ConnectionException;
use Acme\Dhl\Exceptions\DhlApiException;
use Acme\Dhl\Exceptions\DhlAuthenticationException;
use Acme\Dhl\Exceptions\DhlValidationException;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
@ -34,11 +34,12 @@ class DhlClient
/**
* Make HTTP request to DHL API
*
* @param string $method HTTP method (get, post, put, delete)
* @param string $uri API endpoint URI
* @param array $payload Request body data
* @param array $query Query parameters
* @param string $method HTTP method (get, post, put, delete)
* @param string $uri API endpoint URI
* @param array $payload Request body data
* @param array $query Query parameters
* @return array Response data as array
*
* @throws Exception When API request fails or returns error
*/
public function request(string $method, string $uri, array $payload = [], array $query = []): array
@ -59,14 +60,16 @@ class DhlClient
CURLOPT_CONNECTTIMEOUT => config('dhl.ssl.connect_timeout', 10),
CURLOPT_TIMEOUT => config('dhl.ssl.timeout', 30),
CURLOPT_USERAGENT => 'acme-laravel-dhl/1.0',
]
],
])
->retry(3, 300, function ($exception, $attempt) {
if ($exception instanceof RequestException && $exception->response->status() === 429) {
$delay = min(1000000 * $attempt, 10000000); // Max 10 seconds
usleep($delay); // Microseconds
return true;
}
return $exception instanceof ConnectionException ||
($exception instanceof RequestException && in_array($exception->response->status(), [500, 502, 503, 504]));
}, false);
@ -79,7 +82,7 @@ class DhlClient
// Make the request
$response = match (strtolower($method)) {
'get' => $request->get($uri, $query),
'post' => $request->post($uri . '?' . http_build_query($query), $payload),
'post' => $request->post($uri.'?'.http_build_query($query), $payload),
'put' => $request->put($uri, $payload),
'delete' => $request->delete($uri),
default => throw new Exception("Unsupported HTTP method: {$method}")
@ -96,7 +99,7 @@ class DhlClient
'response_body' => $response->body(),
'response_json' => $response->json(),
'status_code' => $response->status(),
'headers' => $response->headers()
'headers' => $response->headers(),
]);
}
@ -125,7 +128,7 @@ class DhlClient
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'User-Agent' => 'acme-laravel-dhl/1.0'
'User-Agent' => 'acme-laravel-dhl/1.0',
];
if ($this->apiKey) {
@ -150,7 +153,7 @@ class DhlClient
$status === 403 => throw new DhlAuthenticationException("DHL API access forbidden: {$errorMessage}"),
$status === 404 => throw new DhlApiException("DHL API endpoint not found: {$method} {$uri}"),
$status === 422 => throw new DhlValidationException("DHL API validation error: {$errorMessage}"),
$status === 429 => throw new DhlApiException("DHL API rate limit exceeded. Please try again later."),
$status === 429 => throw new DhlApiException('DHL API rate limit exceeded. Please try again later.'),
$status >= 500 => throw new DhlApiException("DHL API server error: {$errorMessage}"),
default => throw new DhlApiException("DHL API error ({$status}): {$errorMessage}")
};
@ -158,20 +161,48 @@ class DhlClient
/**
* Extract error message from DHL API response
*
* DHL API v2 returns errors in various formats:
* - {status: {detail: "..."}}
* - {items: [{sstatus: {detail: "..."}}]}
* - {items: [{validationMessages: [{...}]}]}
*/
private function extractErrorMessage(?array $body): ?string
{
if (!$body) {
if (! $body) {
return null;
}
// Try different possible error message fields
return $body['message']
// Try different possible error message fields for DHL API v2
$message = $body['message']
?? $body['error']
?? $body['detail']
?? data_get($body, 'status.detail')
?? data_get($body, 'status.title')
?? data_get($body, 'errors.0.message')
?? data_get($body, 'error.message')
?? data_get($body, 'items.0.sstatus.detail')
?? data_get($body, 'items.0.sstatus.title')
?? null;
// Check for validation messages in items
if (! $message && isset($body['items'][0]['validationMessages'])) {
$validationMessages = $body['items'][0]['validationMessages'];
if (is_array($validationMessages) && ! empty($validationMessages)) {
$messages = [];
foreach ($validationMessages as $vm) {
$vmMessage = $vm['validationMessage'] ?? $vm['message'] ?? $vm['property'] ?? null;
if ($vmMessage) {
$messages[] = $vmMessage;
}
}
if (! empty($messages)) {
$message = implode('; ', $messages);
}
}
}
return $message;
}
/**
@ -202,9 +233,9 @@ class DhlClient
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'dhl_config' => [
'base_url' => $this->baseUrl,
'has_api_key' => !empty($this->apiKey),
'has_username' => !empty($this->username),
'has_password' => !empty($this->password),
'has_api_key' => ! empty($this->apiKey),
'has_username' => ! empty($this->username),
'has_password' => ! empty($this->password),
'ssl_verify_peer' => config('dhl.ssl.verify_peer', true),
'ssl_verify_host' => config('dhl.ssl.verify_host', true),
'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'),
@ -215,7 +246,7 @@ class DhlClient
'APP_ENV' => config('app.env'),
'APP_DEBUG' => config('app.debug'),
'APP_URL' => config('app.url'),
]
],
];
$this->getDhlLogger()->info('DHL Server Environment Debug Info', $info);
@ -235,14 +266,14 @@ class DhlClient
$methods = [
'method1' => 'Laravel HTTP with enhanced SSL',
'method2' => 'Laravel HTTP with relaxed SSL',
'method3' => 'Direct cURL fallback'
'method3' => 'Direct cURL fallback',
];
foreach ($methods as $methodKey => $methodName) {
try {
$this->getDhlLogger()->info("DHL API connection test - trying {$methodName}", [
'method' => $methodKey,
'base_url' => $this->baseUrl
'base_url' => $this->baseUrl,
]);
$success = $this->testConnectionWithMethod($methodKey);
@ -250,23 +281,25 @@ class DhlClient
if ($success) {
$this->getDhlLogger()->info("DHL API connection test successful with {$methodName}", [
'method' => $methodKey,
'base_url' => $this->baseUrl
'base_url' => $this->baseUrl,
]);
return true;
}
} catch (Exception $e) {
$this->getDhlLogger()->warning("DHL API connection test failed with {$methodName}", [
'method' => $methodKey,
'error' => $e->getMessage(),
'base_url' => $this->baseUrl
'base_url' => $this->baseUrl,
]);
}
}
$this->getDhlLogger()->error('DHL API connection test failed with all methods', [
'base_url' => $this->baseUrl,
'tried_methods' => array_keys($methods)
'tried_methods' => array_keys($methods),
]);
return false;
}
@ -310,7 +343,7 @@ class DhlClient
];
// Only use HTTP/2 for newer cURL versions
if (!$isOldCurl && defined('CURL_HTTP_VERSION_2_0')) {
if (! $isOldCurl && defined('CURL_HTTP_VERSION_2_0')) {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
} else {
$curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
@ -327,11 +360,11 @@ class DhlClient
// Log complete cURL request details
$this->getDhlLogger()->info('DHL Enhanced Connection Test - Complete Request Details', [
'method' => 'Enhanced SSL',
'url' => $this->baseUrl . '/',
'url' => $this->baseUrl.'/',
'headers' => $this->buildHeaders(),
'auth' => [
'username' => $this->username,
'password' => '***hidden***'
'password' => '***hidden***',
],
'curl_options' => $this->formatCurlOptions($curlOptions),
'timeout' => 10,
@ -339,7 +372,7 @@ class DhlClient
'verify_peer' => config('dhl.ssl.verify_peer', true),
'verify_host' => config('dhl.ssl.verify_host', true),
'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'),
]
],
]);
$response = Http::baseUrl($this->baseUrl)
@ -349,7 +382,7 @@ class DhlClient
->withOptions([
'verify' => config('dhl.ssl.verify_peer', true),
'http_errors' => false,
'curl' => $curlOptions
'curl' => $curlOptions,
])
->get('/');
@ -358,7 +391,7 @@ class DhlClient
'status_code' => $response->status(),
'headers' => $response->headers(),
'body' => $response->body(),
'success' => $this->validateResponse($response)
'success' => $this->validateResponse($response),
]);
return $this->validateResponse($response);
@ -400,11 +433,11 @@ class DhlClient
// Log complete cURL request details
$this->getDhlLogger()->info('DHL Relaxed Connection Test - Complete Request Details', [
'method' => 'Relaxed SSL',
'url' => $this->baseUrl . '/',
'url' => $this->baseUrl.'/',
'headers' => $this->buildHeaders(),
'auth' => [
'username' => $this->username,
'password' => '***hidden***'
'password' => '***hidden***',
],
'curl_options' => $this->formatCurlOptions($curlOptions),
'timeout' => 15,
@ -412,7 +445,7 @@ class DhlClient
'verify_peer' => false,
'verify_host' => false,
'ssl_version' => 'DEFAULT',
]
],
]);
$response = Http::baseUrl($this->baseUrl)
@ -422,7 +455,7 @@ class DhlClient
->withOptions([
'verify' => false, // Disable SSL verification as fallback
'http_errors' => false,
'curl' => $curlOptions
'curl' => $curlOptions,
])
->get('/');
@ -431,7 +464,7 @@ class DhlClient
'status_code' => $response->status(),
'headers' => $response->headers(),
'body' => $response->body(),
'success' => $this->validateResponse($response)
'success' => $this->validateResponse($response),
]);
return $this->validateResponse($response);
@ -445,16 +478,16 @@ class DhlClient
$ch = curl_init();
$curlOptions = [
CURLOPT_URL => $this->baseUrl . '/',
CURLOPT_URL => $this->baseUrl.'/',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'User-Agent: acme-laravel-dhl/1.0'
'User-Agent: acme-laravel-dhl/1.0',
],
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_USERPWD => $this->username.':'.$this->password,
CURLOPT_SSL_VERIFYPEER => config('dhl.ssl.verify_peer', true),
CURLOPT_SSL_VERIFYHOST => config('dhl.ssl.verify_host', true) ? 2 : 0,
CURLOPT_SSLVERSION => $this->getSslVersion(),
@ -467,15 +500,15 @@ class DhlClient
// Log complete cURL request details
$this->getDhlLogger()->info('DHL Direct cURL Connection Test - Complete Request Details', [
'method' => 'Direct cURL',
'url' => $this->baseUrl . '/',
'url' => $this->baseUrl.'/',
'headers' => [
'Accept: application/json',
'User-Agent: acme-laravel-dhl/1.0'
'User-Agent: acme-laravel-dhl/1.0',
],
'auth' => [
'username' => $this->username,
'password' => '***hidden***',
'auth_type' => 'CURLAUTH_BASIC'
'auth_type' => 'CURLAUTH_BASIC',
],
'curl_options' => $this->formatCurlOptions($curlOptions),
'timeout' => 15,
@ -483,7 +516,7 @@ class DhlClient
'verify_peer' => config('dhl.ssl.verify_peer', true),
'verify_host' => config('dhl.ssl.verify_host', true),
'ssl_version' => config('dhl.ssl.ssl_version', 'TLSv1_2'),
]
],
]);
curl_setopt_array($ch, $curlOptions);
@ -507,7 +540,7 @@ class DhlClient
'curl_info' => $curlInfo,
'verbose_output' => $verboseOutput,
'curl_error' => $error,
'success' => $httpCode >= 200 && $httpCode < 500
'success' => $httpCode >= 200 && $httpCode < 500,
]);
if ($error) {
@ -583,11 +616,13 @@ class DhlClient
{
if ($response->status() === 401) {
$this->getDhlLogger()->error('DHL API authentication failed: Invalid username/password');
return false;
}
if ($response->status() === 403 && str_contains($response->body(), 'api-key')) {
$this->getDhlLogger()->error('DHL API authentication failed: Invalid API key');
return false;
}