mivita/dev/23-01-2026/dhl-return-label-api-fix.md
2026-01-23 17:35:23 +01:00

10 KiB

DHL Return Label API Fix

Datum: 23.01.2026
Problem: Return-Label-Erstellung schlug fehl mit "DHL API error (400): 0 of 1 shipment successfully printed."
Status: Behoben

Problem-Analyse

Ursprüngliches Problem

Der Code verwendete ShippingService::createLabel() für Return-Labels, aber:

  1. Falscher API-Endpunkt:

    • Normale Sendungen: POST /parcel/de/shipping/v2/orders
    • Return-Labels: POST /parcel/de/returns/v1/labels
  2. Falsche Payload-Struktur:

    • ShippingService nutzt product, shipments[], detaillierte Dimensions
    • ReturnsService benötigt shipper, receiver, billingNumber
  3. Falsche Adress-Felder:

    • ShippingService: name, street, houseNumber
    • Returns API: name1, addressStreet, addressHouse

Lösung

1. Controller Änderungen

Datei: app/Http/Controllers/DhlShipmentController.php

Vorher:

$shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient);
$result = $shippingService->createLabel($returnData);

Nachher:

$returnsService = new \Acme\Dhl\Services\ReturnsService($dhlClient);
$result = $returnsService->createReturn($returnData);

2. Job Änderungen

Datei: app/Jobs/CreateReturnLabelJob.php

Änderungen:

  • Import geändert: use Acme\Dhl\Services\ReturnsService;
  • Service gewechselt: new ReturnsService($dhlClient)
  • Methode geändert: $returnsService->createReturn($returnData)
  • Entfernt: product_code, dimensions, reference
  • Country-Format: 'DE''DEU' (ISO 3166-1 alpha-3)

3. ReturnsService Verbesserungen

Datei: packages/acme-laravel-dhl/src/Services/ReturnsService.php

Verbesserte Validierung:

private function validateReturnData(array $data): array
{
    $validator = Validator::make($data, [
        '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 validierung
        'shipper' => 'required|array',
        'shipper.name' => 'required|string|max:50',
        '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|size:3',
        
        // Consignee validierung
        'consignee' => 'required|array',
        'consignee.name' => 'required|string|max:50',
        // ... etc
    ]);
}

Korrekte API-Payload:

private function buildReturnPayload(array $returnData): array
{
    return [
        'receiverId' => 'DEDE',
        'customerReference' => 'Return-' . $order_id,
        'shipmentReference' => 'Return-Order-' . $order_id,
        'billingNumber' => $billingNumber,
        'shipper' => [
            'name1' => $customer_name,
            'addressStreet' => $street,
            'addressHouse' => $houseNumber,
            'postalCode' => $postalCode,
            'city' => $city,
            'country' => 'DEU',
            // ...
        ],
        'receiver' => [
            'name1' => $warehouse_name,
            'addressStreet' => $street,
            'addressHouse' => $houseNumber,
            // ...
        ],
    ];
}

API-Unterschiede

Normale Sendung (Outbound)

Endpunkt: POST /parcel/de/shipping/v2/orders

Payload:

{
  "profile": "STANDARD_GRUPPENPROFIL",
  "shipments": [{
    "product": "V01PAK",
    "billingNumber": "33333333330102",
    "shipper": {
      "name1": "mivita care gmbh",
      "addressStreet": "Leinfeld",
      "addressHouse": "2",
      "postalCode": "87755",
      "city": "Kirchhaslach",
      "country": "DEU"
    },
    "consignee": {
      "name": "Max Mustermann",
      "addressStreet": "Beispielstraße",
      "addressHouse": "10",
      "postalCode": "12345",
      "city": "Berlin",
      "country": "DEU"
    },
    "details": {
      "weight": { "value": 2500.0, "uom": "g" },
      "dim": { "uom": "mm", "length": 300, "width": 250, "height": 100 }
    },
    "print": { "format": "PDF" },
    "refNo": "Order-12345"
  }]
}

Response:

{
  "items": [{
    "shipmentNo": "222201234567890",
    "label": { "b64": "JVBERi0xLj..." },
    "routingCode": "..."
  }]
}

Return Label (Retoure)

Endpunkt: POST /parcel/de/returns/v1/labels

Payload:

{
  "receiverId": "DEDE",
  "customerReference": "Return-12345",
  "shipmentReference": "Return-Order-12345",
  "billingNumber": "33333333330107",
  "shipper": {
    "name1": "Max Mustermann",
    "addressStreet": "Beispielstraße",
    "addressHouse": "10",
    "postalCode": "12345",
    "city": "Berlin",
    "country": "DEU"
  },
  "receiver": {
    "name1": "mivita care gmbh",
    "addressStreet": "Leinfeld",
    "addressHouse": "2",
    "postalCode": "87755",
    "city": "Kirchhaslach",
    "country": "DEU"
  }
}

Response:

{
  "shipmentNumber": "222209876543210",
  "label": {
    "b64": "JVBERi0xLj..."
  }
}

Wichtige Unterschiede

Feature Outbound Return
API Endpunkt /shipping/v2/orders /returns/v1/labels
Adresse-Felder name, street, houseNumber name1, addressStreet, addressHouse
Produkt-Code Erforderlich (V01PAK) Nicht verwendet
Dimensions Erforderlich Nicht erforderlich
Response-Feld items[0].shipmentNo shipmentNumber
Label-Feld items[0].label.b64 label.b64
Billing Number Normale Abrechnungsnummer Oft separate Retouren-Nummer

Country Codes

Wichtig: DHL API verwendet ISO 3166-1 alpha-3 (3 Buchstaben):

  • DEU (Deutschland)
  • AUT (Österreich)
  • CHE (Schweiz)
  • DE, AT, CH (nicht unterstützt)

Testen

Test Return-Label erstellen

# Im Browser: Admin -> DHL Cockpit
# 1. Outbound-Sendung auswählen
# 2. "Retourenlabel erstellen" Button klicken
# 3. Logs prüfen

Logs prüfen

tail -f storage/logs/laravel.log | grep -A 5 "DHL\|Return"

Erwartete Logs:

[DHL Controller] Creating return label synchronously
[DHL API] Request POST /parcel/de/returns/v1/labels
[DHL API] Response received (200)
[DHL Controller] Return label created successfully (sync)

Bei Fehler

Häufige Fehler:

  1. "Invalid billing number"

    • Prüfen: Ist eine Retouren-Abrechnungsnummer konfiguriert?
    • Lösung: Billing-Nummer in DHL-Einstellungen prüfen
  2. "Invalid address format"

    • Prüfen: Sind alle Pflichtfelder vorhanden?
    • Prüfen: Country Code im Format DEU?
  3. "Missing shipper data"

    • Prüfen: Ist recipient JSON in der Original-Sendung vorhanden?
    • Prüfen: Sind alle Adress-Felder gesetzt?

Payload-Debugging

Falls Fehler auftreten, Payload loggen:

Log::info('[DHL Returns] Payload', [
    'payload' => $payload,
    'original_shipment' => $shipment->toArray(),
]);

Geänderte Dateien

  1. app/Http/Controllers/DhlShipmentController.php - ReturnsService verwenden
  2. app/Jobs/CreateReturnLabelJob.php - ReturnsService verwenden
  3. packages/acme-laravel-dhl/src/Services/ReturnsService.php - Verbesserte Validierung & Payload
  4. 📝 dev/23-01-2026/dhl-return-label-api-fix.md - Diese Dokumentation

Fix: Country Code Konvertierung (23.01.2026)

Problem: Fehler "shipper.country muss 3 Zeichen lang sein"

Ursache: Das recipient JSON speichert Country-Codes im 2-Buchstaben-Format (z.B. "DE"), aber DHL Returns API benötigt 3 Buchstaben ("DEU").

Lösung:

private function convertCountryCode(string $countryCode): string
{
    $code = strtoupper(trim($countryCode));
    
    // If already 3 letters, validate and return
    if (strlen($code) === 3) {
        $validThreeLetterCodes = ['DEU', 'AUT', 'CHE', 'FRA', ...];
        return in_array($code, $validThreeLetterCodes) ? $code : 'DEU';
    }
    
    // Convert 2-letter to 3-letter
    $countryMap = [
        'DE' => 'DEU',
        'AT' => 'AUT',
        'CH' => 'CHE',
        // ...
    ];
    
    return $countryMap[$code] ?? 'DEU';
}

Anwendung:

'country' => $this->convertCountryCode($returnData['shipper']['country'] ?? 'DE')

Validierung angepasst:

'shipper.country' => 'nullable|string|min:2|max:3', // Accept both formats

Unterstützt jetzt: "DE", "DEU", "AT", "AUT", etc.

Fix: Authentication Error - Fallback Implementierung (23.01.2026)

Problem: "DHL API authentication failed: Access to the resource is not allowed"

Ursache:

  • Viele DHL-Accounts haben keinen Zugriff auf den speziellen Returns-API-Endpunkt
  • Returns-API benötigt oft separate Berechtigungen oder Account-Freischaltung

Lösung: Automatischer Fallback

try {
    // Versuche Returns API
    return $this->createReturnViaReturnsAPI($returnData);
} catch (Exception $e) {
    // Bei Authentifizierungsfehler: Fallback
    if (str_contains($e->getMessage(), 'authentication') || 
        str_contains($e->getMessage(), 'not allowed')) {
        
        return $this->createReturnViaRegularShipment($returnData);
    }
    throw $e;
}

Fallback-Methode:

  1. Verwendet regulären Shipping-API-Endpunkt (/parcel/de/shipping/v2/orders)
  2. Erstellt normale Sendung mit V07PAK (DHL Retoure Online)
  3. Adressen sind bereits vertauscht (Kunde → Absender, Lager → Empfänger)
  4. Nach Erstellung wird type='return' gesetzt

Vorteile:

  • Funktioniert mit jedem Standard-DHL-Account
  • Automatischer Fallback ohne manuelle Konfiguration
  • Gleiche Funktionalität für den Benutzer
  • Logging zeigt verwendete Methode

Logging:

[DHL Returns] Returns API not available, falling back to regular shipment
[DHL Returns] Using regular Shipping API as fallback
[DHL Returns] Return label created successfully via Shipping API fallback

Nächste Schritte

  1. Return-Label testen mit echter Sendung
  2. Logs prüfen welche Methode verwendet wird (Returns API oder Fallback)
  3. Return-Label PDF herunterladen und prüfen
  4. Tracking für Return-Sendungen testen
  5. E-Mail für Return-Sendungen (falls gewünscht)

DHL API Dokumentation

Offizielle DHL Entwickler-Dokumentation:

Wichtige Hinweise:

  • Returns verwenden oft eine separate Billing-Nummer
  • Manche Accounts haben keine Returns-Berechtigung aktiviert
  • Sandbox vs. Production: Unterschiedliche Endpunkte verwenden