# 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:** ```php $shippingService = new \Acme\Dhl\Services\ShippingService($dhlClient); $result = $shippingService->createLabel($returnData); ``` **Nachher:** ```php $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: ```php 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: ```php 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:** ```json { "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:** ```json { "items": [{ "shipmentNo": "222201234567890", "label": { "b64": "JVBERi0xLj..." }, "routingCode": "..." }] } ``` ### Return Label (Retoure) **Endpunkt:** `POST /parcel/de/returns/v1/labels` **Payload:** ```json { "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:** ```json { "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 ```bash # Im Browser: Admin -> DHL Cockpit # 1. Outbound-Sendung auswählen # 2. "Retourenlabel erstellen" Button klicken # 3. Logs prüfen ``` ### Logs prüfen ```bash 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: ```php 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:** ```php 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:** ```php 'country' => $this->convertCountryCode($returnData['shipper']['country'] ?? 'DE') ``` **Validierung angepasst:** ```php '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** ```php 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:** - Returns API: https://developer.dhl.com/api-reference/parcel-de-returns-api **Wichtige Hinweise:** - Returns verwenden oft eine separate Billing-Nummer - Manche Accounts haben keine Returns-Berechtigung aktiviert - Sandbox vs. Production: Unterschiedliche Endpunkte verwenden