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_city' => 'required|string|max:50',
|
||||
'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_postnumber' => 'nullable|string|max:20',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class SettingController extends Controller
|
|||
'account_numbers' => [
|
||||
'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),
|
||||
'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),
|
||||
'default' => config('dhl.account_numbers.default'),
|
||||
],
|
||||
|
|
@ -149,7 +149,7 @@ class SettingController extends Controller
|
|||
'dimensions' => [
|
||||
'V01PAK' => config('dhl.dimensions.V01PAK'),
|
||||
'V62KP' => config('dhl.dimensions.V62KP'),
|
||||
'V53PAK' => config('dhl.dimensions.V53PAK'),
|
||||
'V53WPAK' => config('dhl.dimensions.V53WPAK'),
|
||||
'V07PAK' => config('dhl.dimensions.V07PAK'),
|
||||
'default' => config('dhl.dimensions.default'),
|
||||
],
|
||||
|
|
@ -183,6 +183,8 @@ class SettingController extends Controller
|
|||
|
||||
private function normalizeDhlProductCode(?string $productCode): string
|
||||
{
|
||||
$productCode = DhlProductResolver::LEGACY_PRODUCT_CODE_ALIASES[$productCode] ?? $productCode;
|
||||
|
||||
return $productCode === 'V62WP' ? 'V62KP' : ($productCode ?: 'V01PAK');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ use Illuminate\Support\Facades\Log;
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
|
@ -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
|
||||
{
|
||||
$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 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';
|
||||
}
|
||||
|
||||
if (! empty($accountNumbers['V53PAK'])) {
|
||||
$productCodes['V53PAK'] = 'DHL Paket International';
|
||||
if (! empty($accountNumbers['V53WPAK'])) {
|
||||
$productCodes['V53WPAK'] = 'DHL Paket International';
|
||||
}
|
||||
|
||||
if (! empty($accountNumbers['V62KP'])) {
|
||||
|
|
@ -335,7 +335,7 @@ class DhlModalService
|
|||
if (empty($productCodes)) {
|
||||
$productCodes = [
|
||||
'V01PAK' => 'DHL Paket National',
|
||||
'V53PAK' => 'DHL Paket International',
|
||||
'V53WPAK' => 'DHL Paket International',
|
||||
'V62KP' => 'DHL Kleinpaket',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,30 @@ class DhlProductResolver
|
|||
|
||||
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 = [
|
||||
'DE' => 'DEU',
|
||||
|
|
@ -65,6 +86,10 @@ class DhlProductResolver
|
|||
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)) {
|
||||
throw new InvalidArgumentException("DHL-Versand in das Zielland {$countryCode} ist aktuell nicht freigegeben.");
|
||||
}
|
||||
|
|
@ -123,6 +148,24 @@ class DhlProductResolver
|
|||
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
|
||||
{
|
||||
$productCode = $this->normalizeProductCode($productCode);
|
||||
|
|
@ -162,7 +205,9 @@ class DhlProductResolver
|
|||
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;
|
||||
}
|
||||
|
||||
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 = [
|
||||
'V01PAK' => 31.5,
|
||||
'V53PAK' => 31.5,
|
||||
'V53WPAK' => 31.5,
|
||||
'V62KP' => 1.0,
|
||||
];
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class DhlShipmentWeightCalculator
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ class DhlTrackingService
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -114,17 +122,7 @@ class DhlTrackingService
|
|||
'has_api_key' => ! empty($this->apiKey),
|
||||
]);
|
||||
|
||||
$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',
|
||||
]);
|
||||
$response = $this->requestTrackingWithRetry($trackingNumber);
|
||||
|
||||
return $this->processSingleShipmentResponse($trackingNumber, $response);
|
||||
} 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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue