DHL: Auslandslabels mit V53WPAK, Feldlaengen und Tracking-Wiederholung
Log-Analyse 26.08. (P0-2, P1-5, P2-3): - Produktcode fuer DHL Paket International ist V53WPAK; mit V53PAK wurde kein einziges Auslandslabel erzeugt. Alte Codes werden gemappt (LEGACY_PRODUCT_CODE_ALIASES), Abrechnungsnummer und Masse suchen auch unter den alten Setting-Schluesseln (dhl_account_v53pak) und Env-Namen. - Zu lange Felder (Name, Stadt, Telefon, Referenz) werden auf die DHL-Grenzen gekuerzt bzw. bereinigt statt den Versand abzulehnen; refNo mindestens 8 Zeichen, sonst die Bestellreferenz. - Tracking wiederholt bei HTTP 5xx der Unified-API bis zu dreimal. Zolldaten fuer die Schweiz (P0-3) sind nicht Teil dieses Commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
323024f65d
commit
8f85c0107c
14 changed files with 508 additions and 68 deletions
|
|
@ -301,7 +301,9 @@ class DhlShipmentController extends Controller
|
||||||
'shipping_zipcode' => 'required|string|max:10',
|
'shipping_zipcode' => 'required|string|max:10',
|
||||||
'shipping_city' => 'required|string|max:50',
|
'shipping_city' => 'required|string|max:50',
|
||||||
'shipping_country_id' => 'required|exists:countries,id',
|
'shipping_country_id' => 'required|exists:countries,id',
|
||||||
'shipping_phone' => 'nullable|string|max:20',
|
// Telefonnummer wird im ShippingService auf die DHL-Grenzen normalisiert
|
||||||
|
// bzw. verworfen; sie ist optional und darf den Versand nicht blockieren.
|
||||||
|
'shipping_phone' => 'nullable|string|max:50',
|
||||||
'shipping_email' => 'required|email|max:100',
|
'shipping_email' => 'required|email|max:100',
|
||||||
'shipping_postnumber' => 'nullable|string|max:20',
|
'shipping_postnumber' => 'nullable|string|max:20',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ class SettingController extends Controller
|
||||||
'account_numbers' => [
|
'account_numbers' => [
|
||||||
'V01PAK' => $this->getConfigValue('dhl_account_v01pak', config('dhl.account_numbers.V01PAK'), $useEnvPriority),
|
'V01PAK' => $this->getConfigValue('dhl_account_v01pak', config('dhl.account_numbers.V01PAK'), $useEnvPriority),
|
||||||
'V62KP' => $this->getConfigValue('dhl_account_v62kp', $this->getConfigValue('dhl_account_v62wp', config('dhl.account_numbers.V62KP'), $useEnvPriority), $useEnvPriority),
|
'V62KP' => $this->getConfigValue('dhl_account_v62kp', $this->getConfigValue('dhl_account_v62wp', config('dhl.account_numbers.V62KP'), $useEnvPriority), $useEnvPriority),
|
||||||
'V53PAK' => $this->getConfigValue('dhl_account_v53pak', config('dhl.account_numbers.V53PAK'), $useEnvPriority),
|
'V53WPAK' => $this->getConfigValue('dhl_account_v53wpak', $this->getConfigValue('dhl_account_v53pak', config('dhl.account_numbers.V53WPAK'), $useEnvPriority), $useEnvPriority),
|
||||||
'V07PAK' => $this->getConfigValue('dhl_account_v07pak', config('dhl.account_numbers.V07PAK'), $useEnvPriority),
|
'V07PAK' => $this->getConfigValue('dhl_account_v07pak', config('dhl.account_numbers.V07PAK'), $useEnvPriority),
|
||||||
'default' => config('dhl.account_numbers.default'),
|
'default' => config('dhl.account_numbers.default'),
|
||||||
],
|
],
|
||||||
|
|
@ -149,7 +149,7 @@ class SettingController extends Controller
|
||||||
'dimensions' => [
|
'dimensions' => [
|
||||||
'V01PAK' => config('dhl.dimensions.V01PAK'),
|
'V01PAK' => config('dhl.dimensions.V01PAK'),
|
||||||
'V62KP' => config('dhl.dimensions.V62KP'),
|
'V62KP' => config('dhl.dimensions.V62KP'),
|
||||||
'V53PAK' => config('dhl.dimensions.V53PAK'),
|
'V53WPAK' => config('dhl.dimensions.V53WPAK'),
|
||||||
'V07PAK' => config('dhl.dimensions.V07PAK'),
|
'V07PAK' => config('dhl.dimensions.V07PAK'),
|
||||||
'default' => config('dhl.dimensions.default'),
|
'default' => config('dhl.dimensions.default'),
|
||||||
],
|
],
|
||||||
|
|
@ -183,6 +183,8 @@ class SettingController extends Controller
|
||||||
|
|
||||||
private function normalizeDhlProductCode(?string $productCode): string
|
private function normalizeDhlProductCode(?string $productCode): string
|
||||||
{
|
{
|
||||||
|
$productCode = DhlProductResolver::LEGACY_PRODUCT_CODE_ALIASES[$productCode] ?? $productCode;
|
||||||
|
|
||||||
return $productCode === 'V62WP' ? 'V62KP' : ($productCode ?: 'V01PAK');
|
return $productCode === 'V62WP' ? 'V62KP' : ($productCode ?: 'V01PAK');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ use Illuminate\Support\Facades\Log;
|
||||||
*/
|
*/
|
||||||
class DhlDataHelper
|
class DhlDataHelper
|
||||||
{
|
{
|
||||||
|
/** DHL Parcel DE Shipping API v2: refNo muss 8-35 Zeichen lang sein. */
|
||||||
|
public const REFERENCE_MIN_LENGTH = 8;
|
||||||
|
|
||||||
|
public const REFERENCE_MAX_LENGTH = 35;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare order data for DHL API v2
|
* Prepare order data for DHL API v2
|
||||||
*
|
*
|
||||||
|
|
@ -106,14 +111,19 @@ class DhlDataHelper
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DHL verlangt fuer refNo 8-35 Zeichen. Eine leere oder zu kurze Eingabe
|
||||||
|
* wird durch die Bestellreferenz ersetzt, damit die Sendung nicht an einer
|
||||||
|
* manuell erfassten Kurzreferenz scheitert.
|
||||||
|
*/
|
||||||
private static function normalizeReference(?string $reference, ShoppingOrder $order): string
|
private static function normalizeReference(?string $reference, ShoppingOrder $order): string
|
||||||
{
|
{
|
||||||
$reference = trim((string) $reference);
|
$reference = trim(preg_replace('/\s+/', ' ', (string) $reference));
|
||||||
|
|
||||||
if ($reference === '') {
|
if (mb_strlen($reference) < self::REFERENCE_MIN_LENGTH) {
|
||||||
return 'Order-'.$order->id;
|
return 'Order-'.$order->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mb_substr(preg_replace('/\s+/', ' ', $reference), 0, 35);
|
return mb_substr($reference, 0, self::REFERENCE_MAX_LENGTH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -319,8 +319,8 @@ class DhlModalService
|
||||||
$productCodes['V01PAK'] = 'DHL Paket National';
|
$productCodes['V01PAK'] = 'DHL Paket National';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! empty($accountNumbers['V53PAK'])) {
|
if (! empty($accountNumbers['V53WPAK'])) {
|
||||||
$productCodes['V53PAK'] = 'DHL Paket International';
|
$productCodes['V53WPAK'] = 'DHL Paket International';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! empty($accountNumbers['V62KP'])) {
|
if (! empty($accountNumbers['V62KP'])) {
|
||||||
|
|
@ -335,7 +335,7 @@ class DhlModalService
|
||||||
if (empty($productCodes)) {
|
if (empty($productCodes)) {
|
||||||
$productCodes = [
|
$productCodes = [
|
||||||
'V01PAK' => 'DHL Paket National',
|
'V01PAK' => 'DHL Paket National',
|
||||||
'V53PAK' => 'DHL Paket International',
|
'V53WPAK' => 'DHL Paket International',
|
||||||
'V62KP' => 'DHL Kleinpaket',
|
'V62KP' => 'DHL Kleinpaket',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,30 @@ class DhlProductResolver
|
||||||
|
|
||||||
public const DOMESTIC_PRODUCT_CODES = ['V01PAK', 'V62KP'];
|
public const DOMESTIC_PRODUCT_CODES = ['V01PAK', 'V62KP'];
|
||||||
|
|
||||||
public const INTERNATIONAL_PRODUCT_CODE = 'V53PAK';
|
public const INTERNATIONAL_PRODUCT_CODE = 'V53WPAK';
|
||||||
|
|
||||||
public const DEFAULT_INTERNATIONAL_COUNTRIES = ['AT', 'ES', 'CH', 'NL', 'BE', 'FR'];
|
/**
|
||||||
|
* Historisch verwendete Produktcodes, die die DHL Parcel DE Shipping API v2
|
||||||
|
* nicht kennt. Werden auf den gueltigen Code abgebildet, damit Alt-Settings
|
||||||
|
* und gespeicherte Sendungen weiter funktionieren.
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
public const LEGACY_PRODUCT_CODE_ALIASES = [
|
||||||
|
'V53PAK' => 'V53WPAK',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const DEFAULT_INTERNATIONAL_COUNTRIES = ['AT', 'ES', 'NL', 'BE', 'FR'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zielländer ausserhalb der EU-Zollunion. DHL verlangt fuer sie einen
|
||||||
|
* customs-Block (ExportDocument/CN23), den wir noch nicht aufbauen -
|
||||||
|
* ohne den wird die Sendung von DHL abgelehnt. Sie bleiben deshalb
|
||||||
|
* gesperrt, auch wenn sie im Setting dhl_international_countries stehen.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
public const CUSTOMS_REQUIRED_COUNTRIES = ['CH', 'GB', 'US', 'NO'];
|
||||||
|
|
||||||
public const DHL_COUNTRY_CODES = [
|
public const DHL_COUNTRY_CODES = [
|
||||||
'DE' => 'DEU',
|
'DE' => 'DEU',
|
||||||
|
|
@ -65,6 +86,10 @@ class DhlProductResolver
|
||||||
return $productCode;
|
return $productCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in_array($countryCode, self::CUSTOMS_REQUIRED_COUNTRIES, true)) {
|
||||||
|
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} benoetigt Zolldaten (ExportDocument/CN23), die das Modul noch nicht erzeugt. Sendung bitte manuell ueber das DHL-Geschaeftskundenportal anlegen.");
|
||||||
|
}
|
||||||
|
|
||||||
if (! in_array($countryCode, $this->getSupportedInternationalCountries(), true)) {
|
if (! in_array($countryCode, $this->getSupportedInternationalCountries(), true)) {
|
||||||
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} ist aktuell nicht freigegeben.");
|
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} ist aktuell nicht freigegeben.");
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +148,24 @@ class DhlProductResolver
|
||||||
return $billingNumber;
|
return $billingNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den gueltigen Produktcode zusammen mit seinen historischen Schreibweisen,
|
||||||
|
* damit Aufrufer Settings-Slugs und Config-Keys aus beiden Generationen aufloesen koennen.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getProductCodeCandidates(string $productCode): array
|
||||||
|
{
|
||||||
|
$productCode = $this->normalizeProductCode($productCode) ?? '';
|
||||||
|
|
||||||
|
$legacyCodes = array_keys(array_filter(
|
||||||
|
self::LEGACY_PRODUCT_CODE_ALIASES,
|
||||||
|
static fn (string $target): bool => $target === $productCode
|
||||||
|
));
|
||||||
|
|
||||||
|
return array_values(array_unique(array_merge([$productCode], $legacyCodes)));
|
||||||
|
}
|
||||||
|
|
||||||
public function getProductScope(string $productCode): string
|
public function getProductScope(string $productCode): string
|
||||||
{
|
{
|
||||||
$productCode = $this->normalizeProductCode($productCode);
|
$productCode = $this->normalizeProductCode($productCode);
|
||||||
|
|
@ -162,7 +205,9 @@ class DhlProductResolver
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return strtoupper(trim($productCode));
|
$productCode = strtoupper(trim($productCode));
|
||||||
|
|
||||||
|
return self::LEGACY_PRODUCT_CODE_ALIASES[$productCode] ?? $productCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -183,7 +228,12 @@ class DhlProductResolver
|
||||||
$countries = $storedCountries ?: $configCountries;
|
$countries = $storedCountries ?: $configCountries;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::normalizeCountryCodeList(is_array($countries) ? $countries : []);
|
// Zolldaten-pflichtige Laender werden hier hart herausgefiltert, damit ein
|
||||||
|
// gesetztes Setting sie nicht doch in den Versand laesst.
|
||||||
|
return array_values(array_diff(
|
||||||
|
self::normalizeCountryCodeList(is_array($countries) ? $countries : []),
|
||||||
|
self::CUSTOMS_REQUIRED_COUNTRIES
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class DhlShipmentWeightCalculator
|
||||||
|
|
||||||
public const PRODUCT_MAX_WEIGHTS_KG = [
|
public const PRODUCT_MAX_WEIGHTS_KG = [
|
||||||
'V01PAK' => 31.5,
|
'V01PAK' => 31.5,
|
||||||
'V53PAK' => 31.5,
|
'V53WPAK' => 31.5,
|
||||||
'V62KP' => 1.0,
|
'V62KP' => 1.0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ class DhlShipmentWeightCalculator
|
||||||
|
|
||||||
public function getMaxWeightKgForProduct(?string $productCode): float
|
public function getMaxWeightKgForProduct(?string $productCode): float
|
||||||
{
|
{
|
||||||
$productCode = strtoupper(trim((string) $productCode));
|
$productCode = (new DhlProductResolver)->normalizeProductCode($productCode) ?? '';
|
||||||
|
|
||||||
return self::PRODUCT_MAX_WEIGHTS_KG[$productCode] ?? self::DEFAULT_MAX_WEIGHT_KG;
|
return self::PRODUCT_MAX_WEIGHTS_KG[$productCode] ?? self::DEFAULT_MAX_WEIGHT_KG;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,14 @@ class DhlTrackingService
|
||||||
*/
|
*/
|
||||||
private const DEFAULT_QUOTA_PAUSE_SECONDS = 3600;
|
private const DEFAULT_QUOTA_PAUSE_SECONDS = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versuche insgesamt (inklusive Erstversuch) bei HTTP 5xx der Tracking-API,
|
||||||
|
* und die Wartezeit dazwischen. Die Verzoegerung waechst je Versuch.
|
||||||
|
*/
|
||||||
|
private const SERVER_ERROR_MAX_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
private const SERVER_ERROR_RETRY_DELAY_MS = 300;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum gap between two consecutive DHL tracking calls.
|
* Minimum gap between two consecutive DHL tracking calls.
|
||||||
*
|
*
|
||||||
|
|
@ -114,17 +122,7 @@ class DhlTrackingService
|
||||||
'has_api_key' => ! empty($this->apiKey),
|
'has_api_key' => ! empty($this->apiKey),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = Http::withHeaders([
|
$response = $this->requestTrackingWithRetry($trackingNumber);
|
||||||
'DHL-API-Key' => $this->apiKey,
|
|
||||||
'Accept' => 'application/json',
|
|
||||||
])
|
|
||||||
->withOptions($this->buildHttpOptions())
|
|
||||||
->get(self::TRACKING_ENDPOINT, [
|
|
||||||
'trackingNumber' => $trackingNumber,
|
|
||||||
'requesterCountryCode' => 'DE',
|
|
||||||
'originCountryCode' => 'DE',
|
|
||||||
'language' => 'de',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->processSingleShipmentResponse($trackingNumber, $response);
|
return $this->processSingleShipmentResponse($trackingNumber, $response);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|
@ -144,6 +142,47 @@ class DhlTrackingService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Unified Tracking API liefert sporadisch HTTP 5xx. Das ist eine
|
||||||
|
* Stoerung auf DHL-Seite und geht beim naechsten Versuch meist durch,
|
||||||
|
* darum wird kurz gewartet und wiederholt, statt jeden Fehlversuch
|
||||||
|
* einzeln zu protokollieren.
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
private function requestTrackingWithRetry(string $trackingNumber)
|
||||||
|
{
|
||||||
|
$attempt = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$attempt++;
|
||||||
|
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'DHL-API-Key' => $this->apiKey,
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
])
|
||||||
|
->withOptions($this->buildHttpOptions())
|
||||||
|
->get(self::TRACKING_ENDPOINT, [
|
||||||
|
'trackingNumber' => $trackingNumber,
|
||||||
|
'requesterCountryCode' => 'DE',
|
||||||
|
'originCountryCode' => 'DE',
|
||||||
|
'language' => 'de',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->status() < 500 || $attempt >= self::SERVER_ERROR_MAX_ATTEMPTS) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('[DHL Tracking Service] Unified API antwortete mit Serverfehler - erneuter Versuch', [
|
||||||
|
'tracking_number' => $trackingNumber,
|
||||||
|
'status_code' => $response->status(),
|
||||||
|
'attempt' => $attempt,
|
||||||
|
]);
|
||||||
|
|
||||||
|
usleep(self::SERVER_ERROR_RETRY_DELAY_MS * 1000 * $attempt);
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the standard cURL/Guzzle options used for every DHL tracking call.
|
* Build the standard cURL/Guzzle options used for every DHL tracking call.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ return [
|
||||||
'default' => env('DHL_ACCOUNT_NUMBER_DEFAULT', '63144073550101'),
|
'default' => env('DHL_ACCOUNT_NUMBER_DEFAULT', '63144073550101'),
|
||||||
'V01PAK' => env('DHL_ACCOUNT_NUMBER_V01PAK', '63144073550101'), // DHL Paket National
|
'V01PAK' => env('DHL_ACCOUNT_NUMBER_V01PAK', '63144073550101'), // DHL Paket National
|
||||||
'V62KP' => env('DHL_ACCOUNT_NUMBER_V62KP', '63144073556201'), // DHL Kleinpaket
|
'V62KP' => env('DHL_ACCOUNT_NUMBER_V62KP', '63144073556201'), // DHL Kleinpaket
|
||||||
'V53PAK' => env('DHL_ACCOUNT_NUMBER_V53PAK', '63144073555301'), // DHL Paket International
|
'V53WPAK' => env('DHL_ACCOUNT_NUMBER_V53WPAK', env('DHL_ACCOUNT_NUMBER_V53PAK', '63144073555301')), // DHL Paket International
|
||||||
'V07PAK' => env('DHL_ACCOUNT_NUMBER_V07PAK', '63144073550701'), // DHL Retoure Online
|
'V07PAK' => env('DHL_ACCOUNT_NUMBER_V07PAK', '63144073550701'), // DHL Retoure Online
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -104,7 +104,7 @@ return [
|
||||||
'default' => ['length' => 120, 'width' => 60, 'height' => 60],
|
'default' => ['length' => 120, 'width' => 60, 'height' => 60],
|
||||||
'V01PAK' => ['length' => 120, 'width' => 60, 'height' => 60], // DHL Paket National
|
'V01PAK' => ['length' => 120, 'width' => 60, 'height' => 60], // DHL Paket National
|
||||||
'V62KP' => ['length' => 35, 'width' => 25, 'height' => 8], // DHL Kleinpaket
|
'V62KP' => ['length' => 35, 'width' => 25, 'height' => 8], // DHL Kleinpaket
|
||||||
'V53PAK' => ['length' => 120, 'width' => 60, 'height' => 60], // DHL Paket International
|
'V53WPAK' => ['length' => 120, 'width' => 60, 'height' => 60], // DHL Paket International
|
||||||
'V07PAK' => ['length' => 120, 'width' => 60, 'height' => 60], // DHL Retoure Online
|
'V07PAK' => ['length' => 120, 'width' => 60, 'height' => 60], // DHL Retoure Online
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use Acme\Dhl\Jobs\CreateShipmentJob;
|
||||||
use Acme\Dhl\Models\DhlShipment;
|
use Acme\Dhl\Models\DhlShipment;
|
||||||
use Acme\Dhl\Support\DhlClient;
|
use Acme\Dhl\Support\DhlClient;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use App\Services\DhlDataHelper;
|
||||||
use App\Services\DhlProductResolver;
|
use App\Services\DhlProductResolver;
|
||||||
use App\Services\DhlShipmentService;
|
use App\Services\DhlShipmentService;
|
||||||
use App\Services\DhlShipmentWeightCalculator;
|
use App\Services\DhlShipmentWeightCalculator;
|
||||||
|
|
@ -254,10 +255,16 @@ class ShippingService
|
||||||
// Pre-process German addresses before validation
|
// Pre-process German addresses before validation
|
||||||
$data = $this->preprocessAddresses($data);
|
$data = $this->preprocessAddresses($data);
|
||||||
|
|
||||||
|
// Historische Produktcodes auf die gueltige API-Schreibweise heben,
|
||||||
|
// bevor die in:-Regel greift.
|
||||||
|
if (! empty($data['product_code'])) {
|
||||||
|
$data['product_code'] = (new DhlProductResolver)->normalizeProductCode($data['product_code']);
|
||||||
|
}
|
||||||
|
|
||||||
$validator = Validator::make($data, [
|
$validator = Validator::make($data, [
|
||||||
'order_id' => 'nullable|integer',
|
'order_id' => 'nullable|integer',
|
||||||
'weight_kg' => 'required|numeric|min:0.1|max:31.5', // DHL weight limit
|
'weight_kg' => 'required|numeric|min:0.1|max:31.5', // DHL weight limit
|
||||||
'product_code' => 'nullable|string|in:V01PAK,V53PAK,V62KP,V07PAK',
|
'product_code' => 'nullable|string|in:V01PAK,V53WPAK,V62KP,V07PAK',
|
||||||
'label_format' => 'nullable|string|in:PDF,ZPL',
|
'label_format' => 'nullable|string|in:PDF,ZPL',
|
||||||
'print_format' => 'nullable|string', // Values like A4, 910-300-700 etc.
|
'print_format' => 'nullable|string', // Values like A4, 910-300-700 etc.
|
||||||
'retoure_print_format' => 'nullable|string',
|
'retoure_print_format' => 'nullable|string',
|
||||||
|
|
@ -387,17 +394,122 @@ class ShippingService
|
||||||
{
|
{
|
||||||
// Process shipper address
|
// Process shipper address
|
||||||
if (isset($data['shipper'])) {
|
if (isset($data['shipper'])) {
|
||||||
$data['shipper'] = $this->parseAddressFields($data['shipper']);
|
$data['shipper'] = $this->normalizeOptionalAddressFields($this->parseAddressFields($data['shipper']));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process consignee address
|
// Process consignee address
|
||||||
if (isset($data['consignee'])) {
|
if (isset($data['consignee'])) {
|
||||||
$data['consignee'] = $this->parseAddressFields($data['consignee']);
|
$data['consignee'] = $this->normalizeOptionalAddressFields($this->parseAddressFields($data['consignee']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// refNo muss laut DHL 8-35 Zeichen lang sein. Eine zu kurze manuelle
|
||||||
|
// Referenz darf die Sendung nicht scheitern lassen.
|
||||||
|
$data['reference'] = $this->normalizeReference(
|
||||||
|
$data['reference'] ?? null,
|
||||||
|
$data['order_id'] ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($data['reference'] === null) {
|
||||||
|
unset($data['reference']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionale Felder auf die DHL-Grenzen bringen, statt die Sendung daran scheitern
|
||||||
|
* zu lassen. Telefonnummer und name2 sind bei DHL optional - eine unsauber erfasste
|
||||||
|
* Telefonnummer darf keinen Versand blockieren.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $addressData
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function normalizeOptionalAddressFields(array $addressData): array
|
||||||
|
{
|
||||||
|
if (isset($addressData['phone'])) {
|
||||||
|
$phone = $this->normalizePhoneNumber((string) $addressData['phone']);
|
||||||
|
|
||||||
|
if ($phone === null) {
|
||||||
|
unset($addressData['phone']);
|
||||||
|
} else {
|
||||||
|
$addressData['phone'] = $phone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['name', 'name2', 'city'] as $field) {
|
||||||
|
if (! empty($addressData[$field]) && is_string($addressData[$field])) {
|
||||||
|
$addressData[$field] = mb_substr(trim($addressData[$field]), 0, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $addressData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt Trenn- und Formatzeichen aus der Telefonnummer. Bleibt danach keine
|
||||||
|
* plausible Nummer uebrig, wird null zurueckgegeben, damit das optionale Feld
|
||||||
|
* ganz entfaellt.
|
||||||
|
*/
|
||||||
|
private function normalizePhoneNumber(string $phone): ?string
|
||||||
|
{
|
||||||
|
$phone = trim($phone);
|
||||||
|
|
||||||
|
if ($phone === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isInternational = str_starts_with($phone, '+');
|
||||||
|
$digits = preg_replace('/\D/', '', $phone) ?? '';
|
||||||
|
|
||||||
|
if (strlen($digits) < 6) {
|
||||||
|
Log::warning('DHL: Telefonnummer wird verworfen (unplausibel)', [
|
||||||
|
'digit_count' => strlen($digits),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $isInternational ? '+'.$digits : $digits;
|
||||||
|
|
||||||
|
if (strlen($normalized) > 20) {
|
||||||
|
Log::warning('DHL: Telefonnummer wird verworfen (zu lang fuer die DHL-API)', [
|
||||||
|
'length' => strlen($normalized),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DHL verlangt fuer refNo 8-35 Zeichen. Zu kurze Eingaben werden durch die
|
||||||
|
* Bestellreferenz ersetzt, zu lange gekuerzt.
|
||||||
|
*/
|
||||||
|
private function normalizeReference(?string $reference, mixed $orderId): ?string
|
||||||
|
{
|
||||||
|
$reference = trim((string) $reference);
|
||||||
|
|
||||||
|
if (mb_strlen($reference) >= DhlDataHelper::REFERENCE_MIN_LENGTH) {
|
||||||
|
return mb_substr($reference, 0, DhlDataHelper::REFERENCE_MAX_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($orderId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback = 'Order-'.$orderId;
|
||||||
|
|
||||||
|
if ($reference !== '') {
|
||||||
|
Log::info('DHL: Zu kurze Referenz durch Bestellreferenz ersetzt', [
|
||||||
|
'given_length' => mb_strlen($reference),
|
||||||
|
'reference' => $fallback,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_pad($fallback, DhlDataHelper::REFERENCE_MIN_LENGTH, '0');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse German address to extract street and house number
|
* Parse German address to extract street and house number
|
||||||
*/
|
*/
|
||||||
|
|
@ -752,36 +864,46 @@ class ShippingService
|
||||||
return $testBillingNumber;
|
return $testBillingNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from admin settings via Setting model first (database settings override config)
|
// Historische Schreibweisen mitpruefen, damit vor der Umstellung
|
||||||
$settingKey = 'dhl_account_'.strtolower($productCode);
|
// gespeicherte Settings (z. B. dhl_account_v53pak) weiter greifen.
|
||||||
|
$productCodeCandidates = (new DhlProductResolver)->getProductCodeCandidates($productCode);
|
||||||
|
|
||||||
try {
|
// Try to get from admin settings via Setting model first (database settings override config)
|
||||||
$accountNumber = Setting::getContentBySlug($settingKey);
|
foreach ($productCodeCandidates as $candidate) {
|
||||||
if ($accountNumber) {
|
$settingKey = 'dhl_account_'.strtolower($candidate);
|
||||||
Log::info('Using DHL account number from database settings', [
|
|
||||||
|
try {
|
||||||
|
$accountNumber = Setting::getContentBySlug($settingKey);
|
||||||
|
if ($accountNumber) {
|
||||||
|
Log::info('Using DHL account number from database settings', [
|
||||||
|
'product_code' => $productCode,
|
||||||
|
'setting_key' => $settingKey,
|
||||||
|
'account_number' => $accountNumber,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $accountNumber;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::warning('Could not load DHL account number from settings', [
|
||||||
'product_code' => $productCode,
|
'product_code' => $productCode,
|
||||||
|
'setting_key' => $settingKey,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get account number from config by product code
|
||||||
|
foreach ($productCodeCandidates as $candidate) {
|
||||||
|
$accountNumber = config("dhl.account_numbers.{$candidate}");
|
||||||
|
if ($accountNumber) {
|
||||||
|
Log::info('Using DHL account number from config file', [
|
||||||
|
'product_code' => $productCode,
|
||||||
|
'config_key' => $candidate,
|
||||||
'account_number' => $accountNumber,
|
'account_number' => $accountNumber,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $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(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get account number from config by product code
|
|
||||||
$accountNumber = config("dhl.account_numbers.{$productCode}");
|
|
||||||
if ($accountNumber) {
|
|
||||||
Log::info('Using DHL account number from config file', [
|
|
||||||
'product_code' => $productCode,
|
|
||||||
'account_number' => $accountNumber,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $accountNumber;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to default billing number
|
// Fallback to default billing number
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ it('resolves domestic DHL products for Germany', function () {
|
||||||
it('suggests international parcel for Austria and Spain', function (string $countryCode) {
|
it('suggests international parcel for Austria and Spain', function (string $countryCode) {
|
||||||
$resolver = new DhlProductResolver;
|
$resolver = new DhlProductResolver;
|
||||||
|
|
||||||
expect($resolver->resolveProductCode($countryCode, null, 'V01PAK'))->toBe('V53PAK');
|
expect($resolver->resolveProductCode($countryCode, null, 'V01PAK'))->toBe('V53WPAK');
|
||||||
})->with([
|
})->with([
|
||||||
'austria' => 'AT',
|
'austria' => 'AT',
|
||||||
'spain' => 'ES',
|
'spain' => 'ES',
|
||||||
|
|
@ -47,10 +47,10 @@ it('uses configured international destination countries', function () {
|
||||||
try {
|
try {
|
||||||
$resolver = new DhlProductResolver;
|
$resolver = new DhlProductResolver;
|
||||||
|
|
||||||
expect($resolver->resolveProductCode('FR', null, 'V01PAK'))->toBe('V53PAK')
|
expect($resolver->resolveProductCode('FR', null, 'V01PAK'))->toBe('V53WPAK')
|
||||||
->and($resolver->getProductSuggestionsByCountry())->toMatchArray([
|
->and($resolver->getProductSuggestionsByCountry())->toMatchArray([
|
||||||
'DE' => 'V01PAK',
|
'DE' => 'V01PAK',
|
||||||
'FR' => 'V53PAK',
|
'FR' => 'V53WPAK',
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
config([
|
config([
|
||||||
|
|
@ -133,8 +133,8 @@ it('describes DHL product scope for preflight checks', function () {
|
||||||
|
|
||||||
expect($resolver->getProductScope('V01PAK'))->toBe('national')
|
expect($resolver->getProductScope('V01PAK'))->toBe('national')
|
||||||
->and($resolver->getProductScopeLabel('V01PAK'))->toBe('DHL Paket National')
|
->and($resolver->getProductScopeLabel('V01PAK'))->toBe('DHL Paket National')
|
||||||
->and($resolver->getProductScope('V53PAK'))->toBe('international')
|
->and($resolver->getProductScope('V53WPAK'))->toBe('international')
|
||||||
->and($resolver->getProductScopeLabel('V53PAK'))->toBe('DHL Paket International');
|
->and($resolver->getProductScopeLabel('V53WPAK'))->toBe('DHL Paket International');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects domestic products for international shipments when explicitly selected', function () {
|
it('rejects domestic products for international shipments when explicitly selected', function () {
|
||||||
|
|
@ -146,5 +146,56 @@ it('does not fallback unknown countries to Germany', function () {
|
||||||
})->throws(InvalidArgumentException::class, 'DHL-Laendercode XX wird nicht unterstuetzt.');
|
})->throws(InvalidArgumentException::class, 'DHL-Laendercode XX wird nicht unterstuetzt.');
|
||||||
|
|
||||||
it('requires a billing number for the resolved product', function () {
|
it('requires a billing number for the resolved product', function () {
|
||||||
(new DhlProductResolver)->assertBillingNumber('V53PAK', null);
|
(new DhlProductResolver)->assertBillingNumber('V53WPAK', null);
|
||||||
})->throws(InvalidArgumentException::class, 'Keine DHL-Abrechnungsnummer fuer Produkt V53PAK konfiguriert.');
|
})->throws(InvalidArgumentException::class, 'Keine DHL-Abrechnungsnummer fuer Produkt V53WPAK konfiguriert.');
|
||||||
|
|
||||||
|
it('maps legacy product codes onto the codes the DHL API accepts', function () {
|
||||||
|
$resolver = new DhlProductResolver;
|
||||||
|
|
||||||
|
expect($resolver->normalizeProductCode('V53PAK'))->toBe('V53WPAK')
|
||||||
|
->and($resolver->normalizeProductCode('v53pak'))->toBe('V53WPAK')
|
||||||
|
->and($resolver->normalizeProductCode('V01PAK'))->toBe('V01PAK')
|
||||||
|
->and($resolver->normalizeProductCode(null))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts the legacy international code without rejecting the shipment', function () {
|
||||||
|
expect((new DhlProductResolver)->resolveProductCode('AT', 'V53PAK'))->toBe('V53WPAK')
|
||||||
|
->and((new DhlProductResolver)->getProductScope('V53PAK'))->toBe('international');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('offers legacy setting slugs as fallback candidates for a product', function () {
|
||||||
|
$resolver = new DhlProductResolver;
|
||||||
|
|
||||||
|
expect($resolver->getProductCodeCandidates('V53WPAK'))->toBe(['V53WPAK', 'V53PAK'])
|
||||||
|
->and($resolver->getProductCodeCandidates('V53PAK'))->toBe(['V53WPAK', 'V53PAK'])
|
||||||
|
->and($resolver->getProductCodeCandidates('V01PAK'))->toBe(['V01PAK']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks destinations outside the EU customs union with an actionable message', function () {
|
||||||
|
(new DhlProductResolver)->resolveProductCode('CH', null, 'V01PAK');
|
||||||
|
})->throws(
|
||||||
|
InvalidArgumentException::class,
|
||||||
|
'DHL-Versand in das Zielland CH benoetigt Zolldaten (ExportDocument/CN23), die das Modul noch nicht erzeugt.'
|
||||||
|
);
|
||||||
|
|
||||||
|
it('keeps customs-required countries out of the supported list even when configured', function () {
|
||||||
|
$previousConfigSource = config('dhl.config_source');
|
||||||
|
$previousCountries = config('dhl.international_countries');
|
||||||
|
|
||||||
|
config([
|
||||||
|
'dhl.config_source' => 'env',
|
||||||
|
'dhl.international_countries' => ['AT', 'CH', 'GB'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resolver = new DhlProductResolver;
|
||||||
|
|
||||||
|
expect($resolver->getSupportedInternationalCountries())->toBe(['AT'])
|
||||||
|
->and($resolver->getAllowedProductCodesForCountry('CH'))->toBe([]);
|
||||||
|
} finally {
|
||||||
|
config([
|
||||||
|
'dhl.config_source' => $previousConfigSource,
|
||||||
|
'dhl.international_countries' => $previousCountries,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ it('redacts DHL credentials from configuration logs', function () {
|
||||||
'account_numbers' => [
|
'account_numbers' => [
|
||||||
'V01PAK' => '63144073550101',
|
'V01PAK' => '63144073550101',
|
||||||
'V62KP' => '63144073556201',
|
'V62KP' => '63144073556201',
|
||||||
'V53PAK' => '63144073555301',
|
'V53WPAK' => '63144073555301',
|
||||||
'V07PAK' => null,
|
'V07PAK' => null,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
@ -32,7 +32,7 @@ it('redacts DHL credentials from configuration logs', function () {
|
||||||
->toHaveKey('has_api_secret', true)
|
->toHaveKey('has_api_secret', true)
|
||||||
->toHaveKey('base_url', 'https://api-eu.dhl.com')
|
->toHaveKey('base_url', 'https://api-eu.dhl.com')
|
||||||
->toHaveKey('international_countries', ['AT', 'ES'])
|
->toHaveKey('international_countries', ['AT', 'ES'])
|
||||||
->and($sanitized['account_numbers_configured'])->toEqualCanonicalizing(['V01PAK', 'V62KP', 'V53PAK']);
|
->and($sanitized['account_numbers_configured'])->toEqualCanonicalizing(['V01PAK', 'V62KP', 'V53WPAK']);
|
||||||
|
|
||||||
expect($serialized)
|
expect($serialized)
|
||||||
->not->toContain('super-secret-api-key')
|
->not->toContain('super-secret-api-key')
|
||||||
|
|
|
||||||
60
tests/Unit/Dhl/DhlTrackingServerErrorRetryTest.php
Normal file
60
tests/Unit/Dhl/DhlTrackingServerErrorRetryTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\SettingController;
|
||||||
|
use App\Services\DhlTrackingService;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$reflection = new ReflectionClass(SettingController::class);
|
||||||
|
$property = $reflection->getProperty('cachedDhlConfig');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue(null, [
|
||||||
|
'api_key' => 'cached-test-api-key-1234',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
SettingController::flushDhlConfigCache();
|
||||||
|
DhlTrackingService::clearQuotaPause();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retries a transient server error and returns the successful response', function () {
|
||||||
|
Http::fakeSequence()
|
||||||
|
->push(['title' => 'Internal Server Error'], 500)
|
||||||
|
->push(['shipments' => [[
|
||||||
|
'id' => '00340434292135100148',
|
||||||
|
'status' => ['statusCode' => 'delivered', 'description' => 'Zugestellt'],
|
||||||
|
]]], 200);
|
||||||
|
|
||||||
|
$result = (new DhlTrackingService)->trackShipment('00340434292135100148');
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('success', true)
|
||||||
|
->and($result['status'])->toBe('delivered');
|
||||||
|
|
||||||
|
Http::assertSentCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives up after the configured number of attempts and reports the status once', function () {
|
||||||
|
Http::fake([
|
||||||
|
'api-eu.dhl.com/track/shipments*' => Http::response(['title' => 'Internal Server Error'], 500),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = (new DhlTrackingService)->trackShipment('00340434292135100148');
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('success', false)
|
||||||
|
->toHaveKey('http_status', 500);
|
||||||
|
|
||||||
|
Http::assertSentCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not retry a client error', function () {
|
||||||
|
Http::fake([
|
||||||
|
'api-eu.dhl.com/track/shipments*' => Http::response(['title' => 'Not Found'], 404),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = (new DhlTrackingService)->trackShipment('00340434292135100148');
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('not_found', true);
|
||||||
|
|
||||||
|
Http::assertSentCount(1);
|
||||||
|
});
|
||||||
104
tests/Unit/Dhl/ShippingServiceFieldNormalizationTest.php
Normal file
104
tests/Unit/Dhl/ShippingServiceFieldNormalizationTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Acme\Dhl\Services\ShippingService;
|
||||||
|
use Acme\Dhl\Support\DhlClient;
|
||||||
|
use App\Services\DhlDataHelper;
|
||||||
|
|
||||||
|
function makeDhlNormalizationService(): ShippingService
|
||||||
|
{
|
||||||
|
return new ShippingService(new DhlClient('https://api-sandbox.dhl.com', null, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDhlOrderData(array $overrides = []): array
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'order_id' => 59996,
|
||||||
|
'weight_kg' => 0.5,
|
||||||
|
'product_code' => 'V01PAK',
|
||||||
|
'label_format' => 'PDF',
|
||||||
|
'shipper' => [
|
||||||
|
'name' => 'mivita care gmbh',
|
||||||
|
'street' => 'Leinfeld',
|
||||||
|
'houseNumber' => '2',
|
||||||
|
'postalCode' => '87755',
|
||||||
|
'city' => 'Kirchhaslach',
|
||||||
|
'country' => 'DE',
|
||||||
|
],
|
||||||
|
'consignee' => [
|
||||||
|
'name' => 'Max Mustermann',
|
||||||
|
'street' => 'Hauptstrasse',
|
||||||
|
'houseNumber' => '5',
|
||||||
|
'postalCode' => '10115',
|
||||||
|
'city' => 'Berlin',
|
||||||
|
'country' => 'DE',
|
||||||
|
],
|
||||||
|
'reference' => 'Order-59996',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($overrides as $key => $value) {
|
||||||
|
if ($key === 'consignee') {
|
||||||
|
$data['consignee'] = array_merge($data['consignee'], $value);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDhlOrderData(array $data): array
|
||||||
|
{
|
||||||
|
$service = makeDhlNormalizationService();
|
||||||
|
$method = new ReflectionMethod($service, 'validateOrderData');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
return $method->invoke($service, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('normalises a phone number that would otherwise exceed the DHL limit', function () {
|
||||||
|
$validated = validateDhlOrderData(normalizeDhlOrderData([
|
||||||
|
'consignee' => ['phone' => '+49 (0) 8333 / 94 61 767'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($validated['consignee']['phone'])->toBe('+49083339461767')
|
||||||
|
->and(strlen($validated['consignee']['phone']))->toBeLessThanOrEqual(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops an implausibly short phone number rather than failing the shipment', function () {
|
||||||
|
$validated = validateDhlOrderData(normalizeDhlOrderData([
|
||||||
|
'consignee' => ['phone' => '123'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($validated['consignee'])->not->toHaveKey('phone');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops a phone number that stays too long after normalisation', function () {
|
||||||
|
$validated = validateDhlOrderData(normalizeDhlOrderData([
|
||||||
|
'consignee' => ['phone' => '+49 8333 9461767 9461767 9461767'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($validated['consignee'])->not->toHaveKey('phone');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates name2 to the DHL limit instead of rejecting the shipment', function () {
|
||||||
|
$validated = validateDhlOrderData(normalizeDhlOrderData([
|
||||||
|
'consignee' => ['name2' => str_repeat('Firmenname ', 10)],
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect(mb_strlen($validated['consignee']['name2']))->toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces a reference shorter than the DHL minimum with the order reference', function () {
|
||||||
|
$validated = validateDhlOrderData(normalizeDhlOrderData(['reference' => 'NL']));
|
||||||
|
|
||||||
|
expect($validated['reference'])->toBe('Order-59996')
|
||||||
|
->and(mb_strlen($validated['reference']))->toBeGreaterThanOrEqual(DhlDataHelper::REFERENCE_MIN_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a reference that already satisfies the DHL length rule', function () {
|
||||||
|
$validated = validateDhlOrderData(normalizeDhlOrderData(['reference' => 'Kundenreferenz-4711']));
|
||||||
|
|
||||||
|
expect($validated['reference'])->toBe('Kundenreferenz-4711');
|
||||||
|
});
|
||||||
|
|
@ -48,7 +48,7 @@ it('accepts DHL Kleinpaket as a product code', function () {
|
||||||
|
|
||||||
it('builds an international DHL parcel payload for Austria', function () {
|
it('builds an international DHL parcel payload for Austria', function () {
|
||||||
config([
|
config([
|
||||||
'dhl.account_numbers.V53PAK' => '63144073555301',
|
'dhl.account_numbers.V53WPAK' => '63144073555301',
|
||||||
'dhl.legacy.sandbox' => false,
|
'dhl.legacy.sandbox' => false,
|
||||||
'dhl.legacy.test_mode' => false,
|
'dhl.legacy.test_mode' => false,
|
||||||
]);
|
]);
|
||||||
|
|
@ -57,9 +57,9 @@ it('builds an international DHL parcel payload for Austria', function () {
|
||||||
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
||||||
$method->setAccessible(true);
|
$method->setAccessible(true);
|
||||||
|
|
||||||
$payload = $method->invoke($service, validDhlPhaseOneOrderData('V53PAK', 'AT'));
|
$payload = $method->invoke($service, validDhlPhaseOneOrderData('V53WPAK', 'AT'));
|
||||||
|
|
||||||
expect($payload['shipments'][0]['product'])->toBe('V53PAK')
|
expect($payload['shipments'][0]['product'])->toBe('V53WPAK')
|
||||||
->and($payload['shipments'][0]['billingNumber'])->toBe('63144073555301')
|
->and($payload['shipments'][0]['billingNumber'])->toBe('63144073555301')
|
||||||
->and($payload['shipments'][0]['consignee']['country'])->toBe('AUT');
|
->and($payload['shipments'][0]['consignee']['country'])->toBe('AUT');
|
||||||
});
|
});
|
||||||
|
|
@ -69,7 +69,7 @@ it('rejects unsupported countries instead of falling back to Germany', function
|
||||||
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
$method = new ReflectionMethod($service, 'buildShipmentPayload');
|
||||||
$method->setAccessible(true);
|
$method->setAccessible(true);
|
||||||
|
|
||||||
$method->invoke($service, validDhlPhaseOneOrderData('V53PAK', 'FR'));
|
$method->invoke($service, validDhlPhaseOneOrderData('V53WPAK', 'FR'));
|
||||||
})->throws(InvalidArgumentException::class);
|
})->throws(InvalidArgumentException::class);
|
||||||
|
|
||||||
it('rejects legacy Warenpost for new labels', function () {
|
it('rejects legacy Warenpost for new labels', function () {
|
||||||
|
|
@ -104,7 +104,7 @@ it('uses mustEncode only for German consignee addresses', function () {
|
||||||
$method->setAccessible(true);
|
$method->setAccessible(true);
|
||||||
|
|
||||||
expect($method->invoke($service, validDhlPhaseOneOrderData('V01PAK', 'DE') + ['print_only_if_codeable' => true]))->toBeTrue()
|
expect($method->invoke($service, validDhlPhaseOneOrderData('V01PAK', 'DE') + ['print_only_if_codeable' => true]))->toBeTrue()
|
||||||
->and($method->invoke($service, validDhlPhaseOneOrderData('V53PAK', 'AT') + ['print_only_if_codeable' => true]))->toBeFalse()
|
->and($method->invoke($service, validDhlPhaseOneOrderData('V53WPAK', 'AT') + ['print_only_if_codeable' => true]))->toBeFalse()
|
||||||
->and($method->invoke($service, validDhlPhaseOneOrderData('V01PAK', 'DE') + ['print_only_if_codeable' => false]))->toBeFalse();
|
->and($method->invoke($service, validDhlPhaseOneOrderData('V01PAK', 'DE') + ['print_only_if_codeable' => false]))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue