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:
Kevin 2026-09-23 15:41:35 +00:00
parent 323024f65d
commit 8f85c0107c
14 changed files with 508 additions and 68 deletions

View file

@ -8,6 +8,7 @@ use Acme\Dhl\Jobs\CreateShipmentJob;
use Acme\Dhl\Models\DhlShipment;
use Acme\Dhl\Support\DhlClient;
use App\Models\Setting;
use App\Services\DhlDataHelper;
use App\Services\DhlProductResolver;
use App\Services\DhlShipmentService;
use App\Services\DhlShipmentWeightCalculator;
@ -254,10 +255,16 @@ class ShippingService
// Pre-process German addresses before validation
$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, [
'order_id' => 'nullable|integer',
'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',
'print_format' => 'nullable|string', // Values like A4, 910-300-700 etc.
'retoure_print_format' => 'nullable|string',
@ -387,17 +394,122 @@ class ShippingService
{
// Process shipper address
if (isset($data['shipper'])) {
$data['shipper'] = $this->parseAddressFields($data['shipper']);
$data['shipper'] = $this->normalizeOptionalAddressFields($this->parseAddressFields($data['shipper']));
}
// Process consignee address
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;
}
/**
* 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
*/
@ -752,36 +864,46 @@ class ShippingService
return $testBillingNumber;
}
// Try to get from admin settings via Setting model first (database settings override config)
$settingKey = 'dhl_account_'.strtolower($productCode);
// Historische Schreibweisen mitpruefen, damit vor der Umstellung
// gespeicherte Settings (z. B. dhl_account_v53pak) weiter greifen.
$productCodeCandidates = (new DhlProductResolver)->getProductCodeCandidates($productCode);
try {
$accountNumber = Setting::getContentBySlug($settingKey);
if ($accountNumber) {
Log::info('Using DHL account number from database settings', [
// Try to get from admin settings via Setting model first (database settings override config)
foreach ($productCodeCandidates as $candidate) {
$settingKey = 'dhl_account_'.strtolower($candidate);
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,
'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,
]);
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