# 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
