mivita/dev/2026-01-23/packstation-return-label-restriction.md
2026-02-20 17:55:06 +01:00

12 KiB

Packstation Return-Label Restriction

Datum: 23.01.2026
Status: Implementiert

Problem

User-Feedback:
"Wenn ich ein Label habe aus einer Packstation, kann ich kein Return Label erzeugen."

Fehler:

Fehler beim Erstellen des Retourenlabels: DHL API error (400): 0 of 1 shipment successfully printed.

Root Cause

DHL Packstation Limitierung

Packstationen sind Einwegadressen:

  • Sendungen AN eine Packstation: Erlaubt
  • Sendungen VON einer Packstation: Nicht erlaubt

Return-Label Logik

Bei einem Return-Label werden Sender/Empfänger vertauscht:

Original-Label:
  Sender:     Shop (Musterstraße 1, Berlin)
  Empfänger:  Kunde (Packstation 145, Hamburg) ✅

Return-Label (vertauscht):
  Sender:     Kunde (Packstation 145, Hamburg) ❌
  Empfänger:  Shop (Musterstraße 1, Berlin)

Problem: Packstation kann nicht als Sender verwendet werden!

DHL API Verhalten

Request Payload (Fallback via ShippingService)

{
  "profile": "STANDARD_GRUPPENPROFIL",
  "shipments": [
    {
      "product": "V01PAK",
      "shipper": {
        "name1": "Max Mustermann",
        "addressStreet": "Packstation",
        "addressHouse": "145",
        "postalCode": "20095",
        "city": "Hamburg",
        "country": "DEU",
        "postNumber": "1234567890" // ❌ Packstation als Sender!
      },
      "consignee": {
        "name1": "Mein Shop",
        "addressStreet": "Musterstraße",
        "addressHouse": "1",
        "postalCode": "10115",
        "city": "Berlin",
        "country": "DEU"
      }
    }
  ]
}

DHL API Response

{
  "Status": {
    "statusCode": 400,
    "statusText": "Bad Request"
  },
  "labelData": "",
  "shipmentNumber": "",
  "errors": [
    {
      "message": "0 of 1 shipment successfully printed."
    }
  ]
}

Lösung

1. Backend-Validation (Controller)

File: app/Http/Controllers/DhlShipmentController.php

public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse
{
    // ... existing checks ...

    // Check if recipient is a Packstation (cannot be used as return sender)
    $recipient = is_string($shipment->recipient)
        ? json_decode($shipment->recipient, true)
        : $shipment->recipient;

    $hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');

    if ($hasPostNumber) {
        return response()->json([
            'success' => false,
            'message' => 'Retourenlabels können nicht für Packstation-Sendungen erstellt werden. ' .
                         'DHL erlaubt keine Sendungen VON einer Packstation. ' .
                         'Der Kunde muss die Retoure über eine normale Adresse versenden.',
        ], 422);
    }

    // ... continue with return label creation ...
}

Prüflogik:

  • Prüft ob postnumber oder postNumber im Empfänger-JSON existiert
  • Gibt HTTP 422 (Unprocessable Entity) zurück
  • Klare, verständliche Fehlermeldung

2. UI - DataTable (Cockpit)

File: app/Http/Controllers/DhlShipmentController.php

->addColumn('actions', function ($shipment) {
    // ...

    // Return label button (only for non-Packstation outbound shipments)
    if ($shipment->type == 'outbound' && ! $shipment->returns()->count()) {
        $recipient = is_string($shipment->recipient)
            ? json_decode($shipment->recipient, true)
            : $shipment->recipient;

        $hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');

        if (!$hasPostNumber) {
            // Regular address: Show active button
            $buttons .= '<button type="button" class="btn btn-sm btn-outline-info create-return-btn"
                            data-shipment-id="' . $shipment->id . '"
                            data-toggle="tooltip"
                            title="Retourenlabel erstellen">
                            <i class="fas fa-undo"></i>
                        </button>';
        } else {
            // Packstation: Show disabled button with explanation
            $buttons .= '<button type="button" class="btn btn-sm btn-outline-secondary"
                            disabled
                            data-toggle="tooltip"
                            title="Retourenlabels nicht möglich für Packstation-Sendungen">
                            <i class="fas fa-undo text-muted"></i>
                        </button>';
        }
    }

    // ...
})

UI-Verhalten:

  • Packstation: Disabled Button mit Tooltip
  • Normale Adresse: Aktiver Button
  • Icon wird grau dargestellt bei Packstation

3. UI - Order Detail View

File: resources/views/admin/sales/_detail_dhl_shipments.blade.php

@if($shipment->type === 'outbound' && !$shipment->returns->count())
    @php
        $recipient = is_string($shipment->recipient)
            ? json_decode($shipment->recipient, true)
            : $shipment->recipient;
        $hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
    @endphp

    @if(!$hasPostNumber)
        <button type="button"
                class="btn btn-outline-info dhl-create-return-btn"
                data-shipment-id="{{ $shipment->id }}"
                title="Retourenlabel erstellen">
            <i class="fas fa-undo"></i>
        </button>
    @else
        <button type="button"
                class="btn btn-outline-secondary"
                disabled
                title="Retourenlabels nicht möglich für Packstation-Sendungen">
            <i class="fas fa-undo text-muted"></i>
        </button>
    @endif
@endif

4. UI - Shipment Detail View

File: resources/views/admin/dhl/show.blade.php

@if($shipment->type == 'outbound' && !$shipment->returns->count())
    @php
        $recipient = is_string($shipment->recipient)
            ? json_decode($shipment->recipient, true)
            : $shipment->recipient;
        $hasPostNumber = !empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? '');
    @endphp

    @if(!$hasPostNumber)
        <button type="button"
                class="btn btn-info mr-2"
                id="create-return-btn"
                data-shipment-id="{{ $shipment->id }}">
            <i class="fas fa-undo"></i> Retourenlabel erstellen
        </button>
    @else
        <button type="button"
                class="btn btn-secondary mr-2"
                disabled
                title="Retourenlabels nicht möglich für Packstation-Sendungen">
            <i class="fas fa-undo"></i> Packstation-Sendung
        </button>
        <small class="text-muted d-block mb-2">
            <i class="fas fa-info-circle"></i>
            Retourenlabels können nicht für Packstation-Sendungen erstellt werden,
            da DHL keine Sendungen VON einer Packstation erlaubt.
        </small>
    @endif
@endif

Besonderheit Detail-View:

  • Zusätzlicher Info-Text unter dem Button
  • Icon-Indikator für besseres Verständnis

Packstation-Erkennung

Datenstruktur

DhlShipment Model:

$shipment->recipient = '{
    "firstname": "Max",
    "lastname": "Mustermann",
    "street": "Packstation 145",
    "postalcode": "20095",
    "city": "Hamburg",
    "country": "DE",
    "postnumber": "1234567890"  // ← Identifikator!
}'

Detection Logic

$recipient = is_string($shipment->recipient)
    ? json_decode($shipment->recipient, true)
    : $shipment->recipient;

// Check both possible keys (case-insensitive)
$hasPostNumber = !empty(
    $recipient['postnumber'] ??
    $recipient['postNumber'] ??
    ''
);

if ($hasPostNumber) {
    // This is a Packstation delivery
}

Keys geprüft:

  • postnumber (lowercase)
  • postNumber (camelCase)

Testing

Test 1: Packstation-Sendung - API Call blockiert

# Via Tinker
$shipment = DhlShipment::find(24); // Packstation-Sendung
$controller = new App\Http\Controllers\DhlShipmentController();
$response = $controller->createReturnLabel(new Illuminate\Http\Request(), $shipment);

# Expected Response:
{
    "success": false,
    "message": "Retourenlabels können nicht für Packstation-Sendungen erstellt werden.
                DHL erlaubt keine Sendungen VON einer Packstation.
                Der Kunde muss die Retoure über eine normale Adresse versenden."
}
# HTTP Status: 422

Test 2: Packstation-Sendung - Button disabled

Schritte:

  1. Cockpit öffnen
  2. Packstation-Sendung suchen (ID 24)
  3. Return-Button ist grau/disabled
  4. Tooltip zeigt "Retourenlabels nicht möglich für Packstation-Sendungen"
  5. Icon ist grau (text-muted)

Test 3: Normale Sendung - Return funktioniert

Schritte:

  1. Normale Outbound-Sendung auswählen (kein postNumber)
  2. Return-Button ist aktiv/blau
  3. Button klicken
  4. Return-Label wird erfolgreich erstellt

Test 4: Detail-View mit Info-Text

Schritte:

  1. Packstation-Sendung Detail-View öffnen (ID 24)
  2. "Packstation-Sendung" Button ist disabled
  3. Info-Text sichtbar unter dem Button
  4. Erklärt warum Return nicht möglich ist

Database Check

-- Find all Packstation shipments
SELECT
    id,
    type,
    dhl_shipment_no,
    JSON_EXTRACT(recipient, '$.postnumber') as postnumber,
    JSON_EXTRACT(recipient, '$.street') as street
FROM dhl_package_shipments
WHERE
    type = 'outbound'
    AND (
        JSON_EXTRACT(recipient, '$.postnumber') IS NOT NULL
        OR JSON_EXTRACT(recipient, '$.postNumber') IS NOT NULL
    );

Example Results:

id  | type     | dhl_shipment_no           | postnumber   | street
----|----------|---------------------------|--------------|------------------
24  | outbound | 0034043333301020021029524 | 1234567890   | "Packstation 145"

User Experience

Vorher (Fehler)

1. User klickt "Retourenlabel erstellen"
2. API Call wird gesendet
3. ❌ DHL API error (400): 0 of 1 shipment successfully printed
4. ❌ Kryptische Fehlermeldung
5. ❌ User weiß nicht warum es fehlschlägt

Nachher (Proaktive Verhinderung)

1. User öffnet Packstation-Sendung
2. ✅ Button ist disabled mit Tooltip
3. ✅ Info-Text erklärt warum nicht möglich
4. ✅ Kein fehlgeschlagener API Call
5. ✅ Klarheit für den User

Alternative Lösungen (nicht implementiert)

Option 1: Shop-Adresse als Return-Empfänger

Idee: Bei Packstation-Sendungen automatisch Shop-Adresse als Rücksendeadresse verwenden.

Problem:

  • Kunde muss Paket zur Post/DHL-Filiale bringen
  • Nicht transparent für Kunden
  • Komplexere Logik

Entscheidung: Nicht umgesetzt

Option 2: QR-Code Return ohne Label

Idee: DHL QR-Code Return für Packstation-Kunden.

Problem:

  • Erfordert separates DHL API Produkt
  • Nicht in allen Accounts verfügbar
  • Separate Implementierung nötig

Entscheidung: Nicht umgesetzt (könnte zukünftig ergänzt werden)

Geänderte Dateien

  1. app/Http/Controllers/DhlShipmentController.php

    • Zeile 427-437: Backend-Validation für Packstation-Check
    • Zeile 224-236: DataTable Button-Logik mit Packstation-Check
  2. resources/views/admin/sales/_detail_dhl_shipments.blade.php

    • Zeile 108-127: Blade-Logic für Return-Button (disabled bei Packstation)
  3. resources/views/admin/dhl/show.blade.php

    • Zeile 161-182: Detail-View mit disabled Button + Info-Text

Status

Backend-Validation: Implementiert
UI-Anpassungen (Cockpit): Implementiert
UI-Anpassungen (Order Detail): Implementiert
UI-Anpassungen (Shipment Detail): Implementiert
Dokumentation: Erstellt

Nächste Schritte

  1. Testen: Packstation-Sendung im Cockpit prüfen
  2. Testen: Return-Button sollte disabled sein
  3. Testen: Normale Sendung kann weiterhin Return-Label erstellen
  4. Optional: Kunde über alternative Return-Optionen informieren (Email)

Lessons Learned

  1. Proaktive UI-Validierung: Buttons disablen ist besser als Fehler zeigen
  2. Klare Fehlermeldungen: User sollten verstehen WARUM etwas nicht geht
  3. DHL Limitierungen: Packstationen sind Einweg-Adressen (nur AN, nicht VON)
  4. Multiple Keys prüfen: postnumber vs postNumber - beide abfangen