mivita/dev/2026-05-13-dhl-modul/ENTWICKLUNGSKONZEPT-DHL-MODUL.md

88 KiB
Raw Permalink Blame History

Entwicklungskonzept DHL Modul

Stand: 27.05.2026

Ziel

Diese Datei ist die aktuelle Arbeitsgrundlage fuer die Weiterentwicklung des DHL Moduls. Die bisherigen Markdown-Dateien in diesem Ordner dokumentieren abgeschlossene oder ueberholte Zwischenstaende, insbesondere den frueheren SDK-Ansatz, Paketinstallation, SSL/cURL-Fixes und einzelne abgeschlossene Entwicklungsschritte.

Aktuelle Anforderungen kommen aus docs/dhl/Anpassung DHL Modul.md:

  • Internationaler Versand ausserhalb Deutschlands, insbesondere Oesterreich und Spanien.
  • Freies Feld fuer Sendungsreferenz oder interne Hinweise wie "Nachlieferung".
  • Adressvalidierung vor Labelerstellung, damit fehlerhafte Labels und kostenpflichtige Stornos vermieden werden.
  • Storno im DHL Cockpit pruefen und stabilisieren.
  • Gewicht von Kompensationsprodukten in das DHL-Paketgewicht einrechnen.
  • Tracking-Mails auf Rhythmus, Ausloeser und Mehrfachversand pruefen.
  • Tracking-Codes in Admin, User-Portal und User-N-Portal sichtbar machen.
  • Tracking-Mail-Versand nur bei Statusaenderung.
  • DHL-Umstellung von V62WP Warenpost auf V62KP DHL Kleinpaket bis spaetestens 31.05.2026.

Aktueller technischer Stand

Das produktive Modul basiert auf dem Paket packages/acme-laravel-dhl und verwendet die DHL REST API ueber Acme\Dhl\Support\DhlClient. Die zentrale Tabelle ist dhl_package_shipments.

Wichtige produktive Einstiegspunkte:

  • app/Http/Controllers/DhlShipmentController.php
  • app/Services/DhlShipmentService.php
  • app/Services/DhlModalService.php
  • app/Services/DhlDataHelper.php
  • app/Services/DhlTrackingService.php
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
  • packages/acme-laravel-dhl/src/Services/ReturnsService.php
  • packages/acme-laravel-dhl/src/Models/DhlShipment.php
  • config/dhl.php

Historische Dokumente erwaehnen teilweise alte Strukturen wie App\Models\DhlShipment, dhl_shipments oder einen konsolidierten DhlApiService. Diese Angaben sind nicht mehr massgeblich, ausser sie werden explizit in aktuellem Code noch referenziert. Aktuell gibt es genau dort noch Altlasten, die bereinigt werden muessen.

Offensichtliche Befunde

1. Produktkuerzel V62WP ist veraltet

V62WP ist weiterhin in Konfiguration, Admin-Settings, Validierung, Produktauswahl und Sprachdateien vorhanden. DHL verlangt die Umstellung auf V62KP.

Betroffene Stellen:

  • config/dhl.php
  • app/Http/Controllers/SettingController.php
  • app/Services/DhlModalService.php
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
  • resources/lang/*/dhl.php

Risiko: Kleinpaket/Warenpost-Labels koennen nach der DHL-Frist abgelehnt werden.

2. Async Tracking verwendet veraltetes Model

app/Jobs/TrackShipmentJob.php importiert App\Models\DhlShipment, dieses Model existiert im aktuellen System nicht mehr. Die produktive DHL-Integration verwendet Acme\Dhl\Models\DhlShipment.

Risiko: Asynchrones Tracking bricht zur Laufzeit, sobald Queue-Tracking genutzt wird.

3. Statuswerte fuer Storno sind inkonsistent

Im Paket wird bei erfolgreichem Storno canceled gesetzt. Andere Stellen, Uebersetzungen und historische Dokumente verwenden cancelled.

Risiko: Filter, Badges, Terminal-Status, Uebersetzungen und Storno-Buttons verhalten sich uneinheitlich.

4. Storno ist technisch vorhanden, aber nicht robust genug

Storno laeuft ueber DELETE /parcel/de/shipping/v2/orders/{shipmentNumber}. Der Code prueft canCancel(), speichert aber Fehler nur begrenzt fachlich verwertbar. Produktspezifische Einschraenkungen wie Warenpost/Kleinpaket sind nicht sauber modelliert.

Risiko: Anwender sehen generische Fehler und koennen nicht erkennen, ob ein Storno produktbedingt, statusbedingt, API-bedingt oder wegen Sandbox/Production-Mismatch scheitert.

5. Internationaler Versand ist nur teilweise vorbereitet

V53PAK ist als internationales Produkt vorhanden, und einige Laender werden in 3-stellige ISO-Codes konvertiert. Dennoch gibt es keinen zentralen Produktentscheid je Zielland, keine harte Validierung nicht unterstuetzter Laender und einen gefaehrlichen Fallback auf DEU.

Risiko: Sendungen nach Oesterreich, Spanien oder weitere Laender koennen falsche Produktcodes, falsche Abrechnungsnummern oder falsche Laendercodes erhalten.

6. Adressvalidierung ist nur formal

Aktuell prueft das Modul Pflichtfelder, Hausnummern und einfache PLZ-Regeln. Eine echte Pruefung, ob Strasse, PLZ und Ort postalisch existieren, findet nicht statt.

Empfohlene Loesung: DHL/Post & DHL DATAFACTORY AUTOCOMPLETE 2.0 fuer DE/AT/CH pruefen und integrieren. Alternativen fuer breiteren Laenderumfang: Loqate, Google Address Validation oder HERE. Wichtig ist eine harte Sperre bei nicht versandfaehigen Adressen vor Labelerstellung.

7. Referenzfeld ist API-seitig vorhanden, aber nicht im Cockpit nutzbar

ShippingService kann reference nach refNo mappen. DhlDataHelper setzt aktuell automatisch Order-{id}.

Risiko: Admins koennen Hinweise wie "Nachlieferung" nicht strukturiert am Label/API-Auftrag hinterlegen.

8. Gewicht von Kompensationsprodukten fehlt

Kompensationsprodukte werden im Warenkorb mit Gewicht 0 abgelegt, damit die Kompensationslogik nicht beeinflusst wird. Das DHL-Gewicht kommt aus ShoppingOrder->weight und enthaelt dieses Produktgewicht dadurch nicht.

Risiko: DHL-Label wird mit zu geringem Paketgewicht erstellt.

9. Tracking-Mail-Logik ist grundsaetzlich brauchbar, muss aber abgesichert werden

Der Scheduler ruft stuendlich dhl:update-tracking --days=30 --send-emails auf. Statusabhaengige Intervalle verhindern zu viele API-Calls. Automatische Mails werden nur gesendet, wenn eine Sendung neu auf in_transit wechselt und noch keine Mail markiert wurde.

Risiko: Statusspruenge direkt auf out_for_delivery oder besondere DHL-Statuscodes koennen ohne Mail bleiben. Mehrere Sendungen einer Bestellung werden teils zusammengefasst, aber die fachliche Regel muss final bestaetigt werden.

Entwicklungskonzept

Phase 1: Pflichtkorrekturen vor DHL-Frist

  1. V62WP vollstaendig auf V62KP migrieren.
  2. Neue Konfigurationskeys fuer DHL_ACCOUNT_NUMBER_V62KP, Admin-Setting dhl_account_v62kp, Dimensionen und Uebersetzungen einfuehren.
  3. ShippingService Validierung um V62KP erweitern und V62WP entfernen oder nur noch als Legacy-Mapping fuer Altdaten anzeigen.
  4. Bestehende Sendungen mit V62WP historisch lesbar lassen, aber neue Labelerstellung blockieren.
  5. Tests fuer Produktcode-Auswahl, Validierung und Payload-Erstellung schreiben.

Phase 2: Stabilisierung von Tracking und Storno

  1. TrackShipmentJob auf aktuelles Model und aktuellen DhlTrackingService umstellen.
  2. Statuswerte vereinheitlichen. Empfehlung: intern canceled verwenden, Uebersetzung auf Deutsch "Storniert".
  3. TERMINAL_STATUSES, Badges, Filter und Sprachdateien entsprechend angleichen.
  4. Storno-Fehler strukturiert speichern: HTTP-Status, DHL-Fehlercode, DHL-Detailtext, Zeitpunkt.
  5. Admin-Feedback verbessern: nicht stornierbar wegen Status, Produkt, API-Antwort oder nicht auffindbarer DHL-Sendung.
  6. Tests fuer erfolgreiche Stornierung, bereits stornierte Sendung, nicht stornierbare Sendung und API-Fehler.

Phase 3: Internationalisierung Versand

  1. Zentralen Service fuer Produkt-/Billing-Entscheidung einfuehren, z. B. DhlProductResolver.
  2. Zielland, Produktcode, Abrechnungsnummer und erlaubte Services dort validieren.
  3. Regeln initial:
    • DE: V01PAK oder V62KP
    • AT, ES und weitere aktivierte Laender: V53PAK
  4. Fallback auf DEU entfernen. Unbekannte Laender muessen mit klarer Fehlermeldung abbrechen.
  5. Cockpit-Formular: Produkt anhand Zielland vorschlagen, aber Admin-Korrektur erlauben.

Phase 4: Adressvalidierung vor Labelerstellung

  1. Neuen serverseitigen Validierungsendpunkt fuer DHL-Adressen schaffen.
  2. Basisvalidierung behalten: Pflichtfelder, Land, PLZ-Format, Hausnummer, Packstation/Postnummer.
  3. DHL DATAFACTORY AUTOCOMPLETE 2.0 fuer DE/AT/CH evaluieren.
  4. Falls DHL fuer alle benoetigten Laender nicht ausreicht, externen Provider evaluieren.
  5. UI-Status einfuehren:
    • gueltig: Labelerstellung erlaubt
    • Warnung: Admin kann bewusst fortfahren
    • Fehler: Labelerstellung gesperrt
  6. Validierungsergebnis optional am Shipment/Order protokollieren.

Phase 5: Referenzfeld und Admin-UX

  1. Neues Formularfeld reference oder shipment_reference im DHL Cockpit.
  2. Wert an DhlDataHelper::prepareOrderData() uebergeben.
  3. ShippingService nutzt vorhandenes Mapping nach refNo.
  4. Referenz im Shipment speichern, damit spaeter nachvollziehbar ist, warum eine Sendung erstellt wurde.
  5. Laengenlimit der DHL API beachten, aktuell maximal 35 Zeichen.

Phase 6: DHL-Gewicht korrekt berechnen

  1. Separaten Gewichtsdienst fuer DHL einfuehren, z. B. DhlShipmentWeightCalculator.
  2. Basis: ShoppingOrder->weight.
  3. Fuer shopping_order_items mit comp > 0 das Produktgewicht aus Product->weight nachladen und addieren.
  4. Nur DHL-Gewicht anpassen, nicht die bestehende Warenkorb-/Versandkostenlogik.
  5. Rundung und DHL-Grenzwerte je Produkt testen.

Phase 7: Tracking-Codes und Mails fachlich finalisieren

  1. Bestehende Admin-Anzeige pruefen und bei Bedarf vereinheitlichen.
  2. Tracking-Code-Anzeige in User-Portal und User-N-Portal ergaenzen.
  3. Mail-Regel final definieren:
    • automatisch nur einmal pro Sendung
    • nur bei relevanter Statusaenderung
    • mehrere Sendungen einer Bestellung sinnvoll zusammenfassen
  4. Statusspruenge wie created direkt nach out_for_delivery abdecken.
  5. Command dhl:update-tracking Tests fuer Mailausloeser und Nicht-Ausloeser ergaenzen.

Phase 8: DHL-seitige Adressvalidierung ueber mustEncode / printOnlyIfCodable

Status: umgesetzt fuer deutsche Empfaengeradressen.

Ziel:

  • Die bestehende formale Vorabpruefung bleibt erhalten.
  • Bei der finalen DHL-Labelerstellung soll DHL selbst pruefen, ob die Adresse leitcodierbar bzw. versandfaehig ist.
  • Dafuer soll der DHL-Query-Parameter mustEncode=true genutzt werden.
  • printOnlyIfCodable ist laut DHL-Spezifikation der Legacy-Name dieser Funktion.
  • Wenn DHL die Adresse ablehnt, wird kein Etikett erstellt.
  • Die DHL-Fehlermeldung wird in das bestehende Modal zurueckgespiegelt, damit der Admin die Lieferadresse direkt korrigieren kann.
  • Laut DHL-Beschreibung ist diese Pruefung nur fuer deutsche Empfaengeradressen relevant.

Geplanter Ablauf:

  1. Admin oeffnet das DHL-Erstellungsmodal.
  2. Die bestehende Vorabpruefung prueft weiterhin:
    • Pflichtfelder
    • PLZ-Format
    • Produkt-/Zielland-Kombination
    • Packstation-/Paketbox-Regeln
    • einfache Plausibilitaet
  3. Beim Klick auf Sendung jetzt erstellen wird die Sendung an DHL uebergeben.
  4. Der DHL-Request enthaelt mustEncode=true.
  5. DHL erstellt das Label nur, wenn die Adresse codeable/leitcodierbar ist.
  6. Wenn DHL die Adresse ablehnt:
    • es wird kein Shipment als erfolgreich erstellt markiert
    • die DHL-Fehlermeldung wird normalisiert
    • das Modal bleibt geoeffnet
    • die Fehlermeldung erscheint unten in der Vorabpruefung
    • der Admin kann die Adresse korrigieren und erneut pruefen/erstellen

Geplante technische Integrationspunkte:

  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Create-Shipment-Request um den Query-Parameter mustEncode=true erweitern.
    • Legacy-Begriff printOnlyIfCodable nur als Dokumentations-/Kompatibilitaetshinweis fuehren, nicht als neuen internen Optionsnamen verwenden.
    • DHL-Fehlerantworten fuer nicht codeable Adressen strukturiert auswerten.
  • app/Services/DhlDataHelper.php
    • Option aus Konfiguration/Settings fuer die Request-Erstellung vorbereiten.
  • app/Services/DhlShipmentService.php
    • DHL-Adressfehler als fachliche Validierungsfehler behandeln, nicht als generischen Systemfehler.
  • app/Http/Controllers/DhlShipmentController.php
    • Fehlerantwort im JSON so zurueckgeben, dass das Modal sie anzeigen kann.
  • resources/views/admin/dhl/modal_create_shipment.blade.php
    • DHL-Fehler bei Labelerstellung in der bestehenden Vorabpruefungsbox anzeigen.
    • Keine separate Browser-Alert-Meldung.
  • config/dhl.php und/oder DHL-Settings
    • Einstellung z. B. print_only_if_codeable vorbereiten.
    • Standard fachlich klaeren: fuer Produktion bevorzugt aktiv, fuer Tests/Sandbox ggf. konfigurierbar.

Fachliche Entscheidung:

  • Die formale Pruefung bleibt vor DHL bestehen, damit offensichtliche Fehler frueh im Modal sichtbar sind.
  • mustEncode=true ist die finale DHL-seitige Absicherung direkt bei der Labelerstellung.
  • Eine abgelehnte DHL-Adresse blockiert die Labelerstellung.
  • Provider-Ausfall oder API-Fehler muss von einer fachlichen DHL-Adressablehnung unterscheidbar sein.
  • Die Fehlermeldung soll fuer Admins handlungsorientiert sein, z. B. DHL kann diese Adresse nicht leitcodieren. Bitte Straße, Hausnummer, PLZ und Ort pruefen.
  • Fuer nicht-deutsche Empfaengeradressen darf mustEncode nicht als vollwertige externe Adressvalidierung dargestellt werden, solange DHL diese Einschraenkung in der API-Dokumentation nennt.

Offene Klaerung nach Implementierung:

  • Verhalten in Sandbox und Produktion.
  • Welche DHL-Status-/Fehlercodes fuer nicht codeable Adressen zurueckkommen.
  • Ob mustEncode fuer alle genutzten Produkte gilt:
    • V01PAK
    • V62KP
    • V53PAK
  • Ob internationale Sendungen ignoriert werden, Warnungen liefern oder anders behandelt werden.
  • Ob bestehende manuelle Admin-Uebersteuerung fachlich erlaubt sein soll oder DHL-Ablehnung immer hart blockiert.

Empfohlene Reihenfolge

  1. V62WP -> V62KP, Statuswerte und TrackShipmentJob korrigieren.
  2. Storno stabilisieren und bessere Fehlermeldungen im Cockpit anzeigen.
  3. Internationalen Produktresolver einbauen.
  4. Referenzfeld und Gewichtskorrektur umsetzen.
  5. Adressvalidierung integrieren.
  6. Tracking-Anzeigen und Mailregeln final testen.
  7. DHL-seitige Adressvalidierung ueber mustEncode=true integrieren, sobald API-Verhalten und Fehlerrueckgaben geklaert sind.

Teststrategie

  • Feature-Tests fuer Controller-Endpunkte: Label erstellen, Storno, Tracking-Mail, Tracking-Update.
  • Unit-Tests fuer Produktresolver, Gewichtskalkulation und Adressvalidierung.
  • HTTP-Fakes fuer DHL API Responses inklusive Fehlerfaelle.
  • Regression-Test fuer V62KP Payload.
  • Command-Test fuer dhl:update-tracking --send-emails.

Entwicklungsprotokoll

Dieser Abschnitt dokumentiert die tatsaechlich umgesetzten Entwicklungsschritte. Jede Phase wird hier nach Abschluss mit Ziel, betroffenen Dateien, fachlicher Entscheidung und Verifikation ergaenzt.

27.05.2026 - Phase 1: Pflichtkorrekturen vor DHL-Frist

Status: abgeschlossen.

Ziel:

  • Neue DHL-Labels duerfen nicht mehr mit V62WP Warenpost erstellt werden.
  • V62KP DHL Kleinpaket wird als neues Produkt fuer nationale Kleinpaket-Sendungen eingefuehrt.
  • Historische Sendungen mit V62WP bleiben im System lesbar.

Umsetzung:

  • config/dhl.php
    • DHL_ACCOUNT_NUMBER_V62KP als neuer Konfigurationskey eingefuehrt.
    • V62KP in account_numbers und dimensions aufgenommen.
    • V62WP aus der produktiven Konfiguration fuer neue Label entfernt.
  • app/Http/Controllers/SettingController.php
    • DHL-Konfiguration liefert nun account_numbers.V62KP und dimensions.V62KP.
    • Altes Datenbank-Setting dhl_account_v62wp wird nur noch als Fallback genutzt, falls dhl_account_v62kp noch nicht gepflegt ist.
    • Altes Default-Produkt V62WP wird beim Lesen auf V62KP normalisiert.
  • resources/views/admin/settings/index.blade.php
    • Admin-Auswahl fuer Standard-Produkt von V62WP - Warenpost National auf V62KP - DHL Kleinpaket umgestellt.
    • Admin-Feld dhl_account_v62kp eingefuehrt.
    • Bestehender Wert aus dhl_account_v62wp wird im Formular als Fallback angezeigt, damit Bestandskonfigurationen nicht leer starten.
  • app/Services/DhlModalService.php
    • Produktauswahl im DHL-Cockpit bietet V62KP - DHL Kleinpaket statt V62WP - DHL Warenpost National an.
    • Fallback-Produktsatz ebenfalls auf V62KP umgestellt.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Validierung fuer neue Label erlaubt V62KP.
    • V62WP wird fuer neue Label abgelehnt.
    • Billing-Nummer-Aufloesung nutzt automatisch den neuen Setting-Key dhl_account_v62kp.
  • resources/lang/de/dhl.php, resources/lang/en/dhl.php, resources/lang/es/dhl.php, resources/lang/fr/dhl.php
    • V62KP als DHL Kleinpaket ergaenzt.
    • V62WP als Legacy-Anzeige belassen, damit historische Sendungen weiterhin verstaendlich dargestellt werden.
  • tests/Pest.php
    • TestCase-Bootstrapping fuer tests/Unit/Dhl registriert.
  • tests/Unit/Dhl/ShippingServiceProductCodeTest.php
    • Neuer Regression-Test fuer Phase 1 ergaenzt.

Fachliche Entscheidung:

  • V62WP wird nicht hart aus allen Anzeigen entfernt, weil bestehende DHL-Sendungen mit diesem Produktcode historisch nachvollziehbar bleiben muessen.
  • Neue Labelerstellung blockiert V62WP bewusst ueber die ShippingService-Validierung.
  • Eine Datenmigration fuer bestehende Settings wurde noch nicht angelegt. Stattdessen wird dhl_account_v62wp temporaer als Fallback verwendet. Eine spaetere Migration kann den Wert dauerhaft nach dhl_account_v62kp ueberfuehren.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl/ShippingServiceProductCodeTest.php
    • Ergebnis: 3 Tests bestanden, 5 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

Offene Folgepunkte:

  • Phase 2: TrackShipmentJob auf aktuelles DHL-Model und aktuellen DhlTrackingService umstellen. Erledigt am 27.05.2026.
  • Phase 2: Storno-Statuswerte zwischen canceled und cancelled vereinheitlichen. Erledigt am 27.05.2026.
  • Optional: Migration oder Admin-Hinweis fuer das alte Setting dhl_account_v62wp, sobald die Fallback-Phase beendet werden soll.

27.05.2026 - Phase 2: Tracking-Job und Storno-Status stabilisiert

Status: abgeschlossen fuer Tracking-Job, Statusvereinheitlichung und strukturierte Storno-Fehlerbasis.

Ziel:

  • Asynchrones Tracking darf nicht mehr auf das alte, nicht mehr produktive DHL-Model App\Models\DhlShipment zugreifen.
  • Storno-Statuswerte werden intern auf canceled vereinheitlicht.
  • Alte Daten mit cancelled bleiben lesbar und filterbar.
  • Storno-Fehler werden fachlich verwertbarer im Shipment protokolliert.

Umsetzung:

  • app/Jobs/TrackShipmentJob.php
    • Import von App\Models\DhlShipment auf Acme\Dhl\Models\DhlShipment umgestellt.
    • Abhaengigkeit auf den alten DhlApiService entfernt.
    • Job nutzt nun DhlTrackingService::updateTrackingNow() ueber Laravel-Dependency-Injection im handle()-Methodenparameter.
    • Queue-Tracking fuehrt dadurch synchron innerhalb des Jobs aus und dispatcht nicht erneut rekursiv in dieselbe Queue.
    • Logging verwendet jetzt dhl_shipment_no statt des alten/inkonsistenten tracking_number.
  • app/Services/DhlTrackingService.php
    • updateTrackingNow() ergaenzt, um Tracking bewusst ohne Queue-Dispatch auszufuehren.
    • Bestehender Controller-/Service-Pfad updateTracking() bleibt unveraendert und entscheidet weiterhin anhand der DHL-Konfiguration zwischen sync/async.
  • packages/acme-laravel-dhl/src/Models/DhlShipment.php
    • cancelled als Legacy-Alias fuer canceled eingefuehrt.
    • Statusuebersetzung und Badge-Klasse normalisieren alte cancelled-Werte auf canceled.
    • Terminal-Statusliste enthaelt canceled und cancelled, damit Alt-Datensaetze nicht weiter getrackt werden.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Erfolgreiche Stornierung setzt weiterhin intern status = canceled.
    • Erfolgreiche Storno-Antwort wird unter api_response_data.cancellation gespeichert.
    • Fehlgeschlagene Stornos werden unter api_response_data.cancellation_error strukturiert gespeichert:
      • status
      • http_status
      • dhl_code
      • detail
      • exception_class
      • occurred_at
  • app/Services/DhlShipmentService.php
    • Lokale Storno-Validierungsfehler wie fehlende DHL-Sendungsnummer oder nicht stornierbarer Status werden ebenfalls in api_response_data.cancellation_error protokolliert.
    • Admin-Feedback fuer nicht stornierbaren Status nutzt die uebersetzte Statusbezeichnung.
  • app/Http/Controllers/DhlShipmentController.php
    • Statusfilter normalisiert cancelled auf canceled.
    • Bei Filter canceled werden neue canceled- und alte cancelled-Sendungen gemeinsam gefunden.
    • DataTable-Badges verwenden intern canceled.
  • resources/views/admin/dhl/cockpit.blade.php
    • Statusfilter zeigt canceled als neuen Wert fuer "Storniert".
    • Alte URL-/Request-Werte mit cancelled bleiben im Select kompatibel.
  • resources/views/admin/dhl/show.blade.php
    • Detailansicht behandelt canceled und cancelled gleich.
    • Aktionsbereich wird fuer beide Storno-Statuswerte ausgeblendet.
  • resources/views/public/tracking.blade.php
    • Public-Tracking behandelt canceled und cancelled gleich.
  • resources/lang/de/dhl.php, resources/lang/en/dhl.php, resources/lang/es/dhl.php, resources/lang/fr/dhl.php
    • Neuer Statuskey canceled ergaenzt.
    • Legacy-Key cancelled bleibt erhalten.
  • tests/Unit/Dhl/DhlShipmentStatusTest.php
    • Neue Tests fuer Statusnormalisierung, Uebersetzung und Badge-Klasse.
  • tests/Unit/Dhl/TrackShipmentJobTest.php
    • Neuer Test, dass der Queue-Job den aktuellen DhlTrackingService nutzt.

Fachliche Entscheidung:

  • Intern gilt ab jetzt canceled als kanonischer DHL-Storno-Status.
  • cancelled wird nicht migriert oder entfernt, weil es in bestehenden Daten/URLs/Views vorkommen kann und weiterhin verstaendlich angezeigt werden soll.
  • Storno-Fehler werden zunaechst in api_response_data gespeichert, um keine neue Datenbankmigration fuer den ersten Stabilisierungsschritt zu erzwingen. Falls spaeter gezielte Admin-Auswertungen gebraucht werden, koennen dedizierte Spalten oder eine eigene Fehler-Tabelle folgen.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 6 Tests bestanden, 12 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

Offene Folgepunkte:

  • Phase 2 Rest: Tests fuer echte Storno-API-Fehler mit HTTP-Fakes/Mocking weiter ausbauen.
  • Phase 2 Rest: Admin-Detailanzeige fuer gespeicherte api_response_data.cancellation_error bei Bedarf sichtbar machen.
  • Phase 3: Internationalen Produkt-/Billing-Resolver einfuehren. Erledigt am 27.05.2026.

27.05.2026 - Phase 3: Internationaler Produkt- und Laenderresolver

Status: abgeschlossen fuer zentrale Produkt-/Laenderentscheidung, initiale Laenderfreigabe und harten Zielland-Fallback-Stopp.

Ziel:

  • Produktcode, Zielland und DHL-Laendercode werden zentral entschieden.
  • Deutschland nutzt fuer neue Label nur erlaubte nationale Produkte V01PAK oder V62KP.
  • Oesterreich und Spanien nutzen initial V53PAK.
  • Unbekannte oder nicht freigegebene Ziellaender duerfen nicht mehr still auf DEU fallen.
  • Das DHL-Cockpit schlaegt den Produktcode anhand des Ziellands vor, laesst aber erlaubte Admin-Korrekturen zu.

Umsetzung:

  • app/Services/DhlProductResolver.php
    • Neuer zentraler Resolver fuer DHL-Produkt- und Laenderentscheidungen.
    • Regeln initial:
      • DE: erlaubt V01PAK, V62KP
      • AT: erlaubt V53PAK
      • ES: erlaubt V53PAK
    • DHL-Laendercode-Konvertierung zentralisiert, z. B. DE -> DEU, AT -> AUT, ES -> ESP.
    • Unbekannte Laendercodes loesen eine klare Exception aus statt auf Deutschland zurueckzufallen.
    • Explizit falsch gewaehlte Produkt-/Laender-Kombinationen werden abgelehnt.
    • Fehlende Billing-Nummern werden ueber assertBillingNumber() fachlich klar abgebrochen.
  • app/Services/DhlDataHelper.php
    • Empfaengerland ist jetzt Pflicht fuer die DHL-Datenaufbereitung.
    • Produktcode wird ueber DhlProductResolver::resolveForShipment() bestimmt.
    • Der alte Empfaengerland-Fallback auf DE wurde entfernt.
    • Dimensionen werden anhand des aufgeloesten Produktcodes gelesen.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Payload-Erstellung nutzt den Resolver fuer Zielland und Produktcode.
    • Billing-Nummer wird gegen den aufgeloesten Produktcode validiert.
    • convertCountryCode() nutzt den Resolver und gibt keinen DEU-Fallback mehr zurueck.
    • Normale Empfaengeradressen und Packstation/Paketbox-Payloads verwenden die gleiche harte Laendercode-Validierung.
  • app/Services/DhlModalService.php
    • Modal-Daten enthalten nun productSuggestions und selectedProductCode.
    • Initialer Produktvorschlag wird aus dem Zielland abgeleitet.
    • Servervalidierung prueft Produkt-/Zielland-Kombinationen ueber den Resolver.
  • resources/views/admin/dhl/modal_in_order_shipment.blade.php
    • Produktauswahl markiert den vorgeschlagenen Produktcode.
    • Laenderoptionen tragen den ISO-Code als data-country-code.
    • Hinweistext ergaenzt, dass der Produktcode anhand des Ziellands vorgeschlagen wird.
  • resources/views/admin/dhl/modal_create_shipment.blade.php
    • Bei Ziellandwechsel wird der passende Produktvorschlag automatisch gesetzt.
  • app/Http/Controllers/ModalController.php
    • Fallback-Daten fuer das DHL-Modal um Produktvorschlaege und Default-Produkt ergaenzt.
  • tests/Unit/Dhl/DhlProductResolverTest.php
    • Neue Tests fuer Deutschland, Oesterreich, Spanien, nicht freigegebene Laender, unbekannte Laendercodes und fehlende Billing-Nummern.
  • tests/Unit/Dhl/ShippingServiceProductCodeTest.php
    • Payload-Test fuer internationales Paket nach Oesterreich ergaenzt.
    • Regression-Test ergaenzt, dass nicht freigegebene Laender nicht auf Deutschland fallen.

Fachliche Entscheidung:

  • DE, AT und ES sind die initial freigegebenen DHL-Versandlaender fuer diese Phase.
  • Weitere Laender werden nicht implizit erlaubt, auch wenn eine ISO-Konvertierung technisch bekannt ist. Sie muessen fachlich freigegeben und im Resolver ergaenzt werden.
  • Falls im Backend kein Produktcode explizit uebergeben wird, kann der Resolver fuer AT/ES automatisch V53PAK vorschlagen. Wenn ein Admin explizit ein nicht erlaubtes Produkt waehlt, wird die Labelerstellung serverseitig abgelehnt.
  • Der alte DEU-Fallback wurde bewusst entfernt, weil ein falsches Zielland zu falschen Labels und kostenpflichtigen Stornos fuehren kann.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 15 Tests bestanden, 32 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

Offene Folgepunkte:

  • Phase 3 Erweiterung: Weitere Laender erst nach fachlicher Freigabe in DhlProductResolver aufnehmen.
  • Phase 3 Erweiterung: Optional Admin-Setting fuer freigegebene internationale Ziellaender einfuehren.
  • Phase 4: Adressvalidierung vor Labelerstellung integrieren. Erledigt als formale Basisvalidierung am 27.05.2026.

27.05.2026 - Phase 4: Formale Adressvalidierung vor Labelerstellung

Status: abgeschlossen fuer serverseitige Basisvalidierung, Warn-/Fehlerstatus und Cockpit-Vorpruefung. Externe postalische Validierung ist noch offen.

Ziel:

  • Vor der Labelerstellung wird eine DHL-Adresse serverseitig bewertet.
  • Offensichtlich nicht versandfaehige Adressen blockieren die Labelerstellung.
  • Pruefbeduerftige Adressen erzeugen Warnungen und muessen im Cockpit bewusst bestaetigt werden.
  • Packstation/Paketbox-Faelle werden strenger validiert.
  • Die bestehende Labelerstellung bleibt auch serverseitig abgesichert, falls die UI-Pruefung umgangen wird.

Umsetzung:

  • app/Services/DhlAddressValidator.php
    • Neuer zentraler Validator fuer formale DHL-Adresspruefung.
    • Rueckgabeformat:
      • status: valid, warning oder error
      • can_create_label: true/false
      • errors
      • warnings
      • normalized
    • Blockierende Pruefungen:
      • Pflichtfelder fuer Strasse, PLZ, Ort und Land.
      • Name/Firma muss vorhanden sein.
      • Zielland muss im DhlProductResolver freigegeben sein.
      • PLZ-Format fuer DE, AT, ES.
      • Packstation/Paketbox nur fuer Deutschland.
      • DHL Postnummer muss bei Packstation/Paketbox vorhanden und 6-10-stellig sein.
      • Packstation-/Paketbox-Nummer muss vorhanden und 3-stellig im Bereich 100-999 sein.
    • Warnungen:
      • Telefonnummer fehlt.
      • E-Mail-Adresse fehlt.
      • Hausnummer enthaelt keine Ziffer.
      • Postnummer ist gesetzt, aber Strasse/Nr. sieht nicht nach Packstation/Paketbox aus.
  • app/Services/DhlModalService.php
    • Bestehende Modal-Adresspruefung nutzt nun den neuen DhlAddressValidator.
    • validateShipmentData() fuehrt die Adressvalidierung auch serverseitig vor der Labelerstellung aus.
  • app/Http/Controllers/DhlShipmentController.php
    • Neuer Endpoint validateAddress().
    • Antwortet mit Status, Fehlern, Warnungen und can_create_label.
    • Gibt HTTP 422 zurueck, wenn die Adresse nicht label-faehig ist.
  • routes/domains/crm.php
    • Neue Route POST /admin/dhl/validate-address mit Name admin.dhl.validate-address.
  • resources/views/admin/dhl/modal_create_shipment.blade.php
    • Modal ruft vor der eigentlichen Labelerstellung den neuen Validierungsendpunkt auf.
    • Bei Fehlern wird die Labelerstellung blockiert.
    • Bei Warnungen muss der Admin bewusst bestaetigen, bevor das Label erstellt wird.
  • tests/Unit/Dhl/DhlAddressValidatorTest.php
    • Neue Tests fuer:
      • formal gueltige Adresse
      • nicht freigegebenes Zielland
      • falsches PLZ-Format fuer aktiviertes Land
      • Warnstatus ohne Blockade
      • gueltige Packstation
      • ungueltige Packstation-Postnummer
      • fehlende Packstation-Pflichtdaten

Fachliche Entscheidung:

  • Diese Phase implementiert noch keine echte postalische Existenzpruefung von Strasse/PLZ/Ort.
  • Der Validator verhindert aber bereits die teuersten formalen Fehler vor Labelerstellung.
  • Warnungen blockieren nicht automatisch, weil Admins im Cockpit bewusst korrigierte oder fachlich bekannte Sonderfaelle versenden koennen sollen.
  • Die spaetere Provider-Integration kann hinter dem gleichen DhlAddressValidator ergaenzt werden.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 22 Tests bestanden, 50 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

Offene Folgepunkte:

  • Phase 4 Erweiterung: DHL DATAFACTORY AUTOCOMPLETE 2.0 fuer DE/AT/CH fachlich und technisch evaluieren.
  • Phase 4 Erweiterung: Falls DHL DATAFACTORY nicht fuer alle benoetigten Laender reicht, externen Provider wie Loqate, Google Address Validation oder HERE bewerten.
  • Phase 4 Erweiterung: Validierungsergebnis optional dauerhaft an Shipment/Order protokollieren.
  • Phase 5: Referenzfeld und Admin-UX umsetzen. Erledigt am 27.05.2026.

27.05.2026 - Phase 5: Referenzfeld und Admin-UX

Status: abgeschlossen.

Ziel:

  • Admins koennen im DHL-Cockpit eine eigene Versandreferenz setzen.
  • Die Referenz wird als DHL refNo an die Shipping API uebergeben.
  • Die Referenz wird an der Sendung gespeichert und in der Detailansicht angezeigt.
  • Das DHL-Laengenlimit von 35 Zeichen wird eingehalten.
  • Ohne Admin-Eingabe bleibt der bisherige Fallback Order-{id} erhalten.

Umsetzung:

  • database/migrations/2026_05_27_120253_add_reference_to_dhl_package_shipments_table.php
    • Neue nullable Spalte reference mit Laenge 35 fuer dhl_package_shipments.
    • Migration prueft Schema::hasColumn, damit sie robust gegen bereits vorhandene Spalten bleibt.
  • packages/acme-laravel-dhl/src/Models/DhlShipment.php
    • reference in $fillable aufgenommen.
  • resources/views/admin/dhl/modal_in_order_shipment.blade.php
    • Neues Feld reference in der Sendungskonfiguration.
    • Default-Wert: Order-{id}.
    • maxlength="35" und Hilfetext zum DHL refNo.
  • resources/views/admin/dhl/modal_create_shipment.blade.php
    • Clientseitige Validierung fuer maximal 35 Zeichen ergaenzt.
  • app/Http/Controllers/DhlShipmentController.php
    • Servervalidierung fuer reference mit max:35.
    • Uebergibt reference in die Optionen fuer DhlShipmentService.
  • app/Services/DhlDataHelper.php
    • Uebernimmt reference oder alternativ shipment_reference aus den Optionen.
    • Normalisiert Leerraum.
    • Kuerzt programmgesteuert auf 35 Zeichen.
    • Nutzt Order-{id} als Fallback, wenn keine Referenz gesetzt ist.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Bestehendes Mapping nach DHL refNo bleibt aktiv.
    • Gesendete Referenz wird beim Erstellen des DhlShipment-Datensatzes gespeichert.
  • resources/views/admin/dhl/show.blade.php
    • Referenz wird in den Sendungsinformationen angezeigt.
  • tests/Unit/Dhl/DhlDataHelperReferenceTest.php
    • Neue Tests fuer Admin-Referenz, Fallback Order-{id} und 35-Zeichen-Normalisierung.

Fachliche Entscheidung:

  • Die Referenz bleibt bewusst ein kurzes Freitextfeld und wird nicht an Bestellnotizen oder interne Kommentare gekoppelt.
  • Das Feld wird fuer DHL refNo und spaetere Nachvollziehbarkeit genutzt, nicht als internes Memo.
  • Der Fallback Order-{id} bleibt erhalten, damit bestehende Prozesse ohne manuelle Referenz unveraendert funktionieren.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 25 Tests bestanden, 54 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

Offene Folgepunkte:

  • Datenbankmigration vor Nutzung in der Zielumgebung ausfuehren.
  • Phase 6: DHL-Gewicht korrekt berechnen.

27.05.2026 - Abgleich DHL Geschaeftskundenportal: Aktivierte Dienste

Status: dokumentiert als fachlicher Abgleich vor Phase 6.

Quelle:

  • Screenshot aus dem DHL Geschaeftskundenportal vom 27.05.2026 mit aktivierten Produkten und Abrechnungsnummern.

Aktivierte Dienste laut Portal:

  • 63144073550101 - DHL Paket GKP
  • 63144073550701 - DHL Retoure Online
  • 63144073555301 - DHL Paket International GKP
  • 63144073555302 - DHL Retoure Int. A
  • 63144073556201 - Warenpost National / DHL Kleinpaket
  • 63144073556601 - Warenpost International
  • 63144073550801 - DHL Retoure MAUL

Abgleich mit aktueller Implementierung:

  • DHL Paket GKP
    • Aktueller Produktcode im Modul: V01PAK
    • Konfiguriert in config/dhl.php als DHL_ACCOUNT_NUMBER_V01PAK.
    • Wird fuer nationale Paketsendungen genutzt.
    • Status: abgedeckt.
  • DHL Paket International GKP
    • Aktueller Produktcode im Modul: V53PAK
    • Konfiguriert in config/dhl.php als DHL_ACCOUNT_NUMBER_V53PAK.
    • Wird aktuell nur fuer fachlich freigegebene Ziellaender AT und ES genutzt.
    • Status: technisch abgedeckt, Laenderfreigabe bleibt bewusst begrenzt.
  • Warenpost National / DHL Kleinpaket
    • Aktueller Produktcode im Modul: V62KP.
    • Konfiguriert in config/dhl.php als DHL_ACCOUNT_NUMBER_V62KP.
    • Ersetzt das alte V62WP.
    • Status: abgedeckt.
  • DHL Retoure Online
    • Konfiguriert in config/dhl.php als DHL_ACCOUNT_NUMBER_V07PAK.
    • Status: Konto ist konfiguriert. Der bestehende ReturnsService nutzt in Teilen noch Fallback-Logik und muss vor produktiver Retourennutzung separat gegen das aktive Retoure-Konto geprueft werden.
  • DHL Retoure Int. A
    • Konto im Portal aktiv.
    • Im Modul aktuell nicht als eigenstaendiger internationaler Retourenprozess modelliert.
    • Status: offen, eigener Folgepunkt.
  • Warenpost International
    • Konto im Portal aktiv.
    • Im Modul aktuell nicht freigeschaltet. Laut DHL API ist dafuer V66WPI relevant; dafuer sind Zoll-/CN22-Daten und eigene Gewichts-/Laenderregeln erforderlich.
    • Status: bewusst nicht Teil der bisherigen Phasen.
  • DHL Retoure MAUL
    • Konto im Portal aktiv.
    • Im Modul aktuell nicht modelliert.
    • Status: offen, nur nach fachlichem Bedarf umsetzen.

Technische Entscheidung:

  • Fuer Phase 6 wird nur das Gewicht fuer die bereits freigegebenen Ausgangsprodukte betrachtet: V01PAK, V53PAK, V62KP.
  • Internationale Warenpost (V66WPI) wird nicht stillschweigend aktiviert, obwohl ein Konto im Portal sichtbar ist. Die Produktart benoetigt separate Regeln und ggf. Zollangaben.
  • Internationale Retouren und Retoure MAUL werden nicht mit bestehenden Paket-/Retoure-Fallbacks vermischt.

Neue Folgepunkte aus dem Portal-Abgleich:

  • Retourenlogik separat gegen aktives DHL Retoure Online Konto pruefen und ggf. V07PAK/Returns-API sauber modellieren.
  • Internationales Retourenprodukt DHL Retoure Int. A fachlich klaeren.
  • Warenpost International/V66WPI nur als eigene Phase ergaenzen, wenn Zoll-, Gewichts- und Laenderregeln geklaert sind.
  • DHL Retoure MAUL nur bei konkretem Prozessbedarf aufnehmen.

27.05.2026 - Nachtrag vor Phase 6: Internationale Paketlaender per DHL-Settings steuerbar

Status: abgeschlossen.

Ziel:

  • Kunden/Admins koennen selbst entscheiden, welche Ziellaender fuer DHL Paket International aktiv sind.
  • Die Freigabe erfolgt im bestehenden Settings-Bereich ueber Checkboxen.
  • Der DhlProductResolver nutzt die gespeicherten Laender dynamisch statt einer fest kodierten AT/ES-Liste.
  • Deutschland bleibt separat geregelt und wird nicht als internationales Paketland gespeichert.

Umsetzung:

  • config/dhl.php
    • Neuer Fallback international_countries aus DHL_INTERNATIONAL_COUNTRIES.
    • Default bleibt AT,ES.
  • app/Http/Controllers/SettingController.php
    • getDhlConfig() liefert nun international_countries.
    • Bei DHL_CONFIG_SOURCE=env wird keine Datenbankabfrage fuer diese Liste ausgefuehrt.
    • Bei Datenbankprioritaet wird dhl_international_countries aus den Settings gelesen.
  • resources/views/admin/settings/index.blade.php
    • Neuer Checkbox-Bereich DHL Paket International Ziellaender.
    • Es werden aktive App-Laender angezeigt, deren ISO-Code im DHL-Resolver bekannt ist.
    • Speicherung als Setting dhl_international_countries vom Typ object.
  • app/Services/DhlProductResolver.php
    • Feste internationale Liste durch getSupportedInternationalCountries() ersetzt.
    • Resolver liest je nach DHL_CONFIG_SOURCE entweder Config/ENV oder Datenbank-Setting.
    • normalizeCountryCodeList() normalisiert, dedupliziert und filtert unbekannte Codes sowie DE.
    • Produktvorschlaege fuer das Modal werden dynamisch aus der aktivierten Laenderliste erzeugt.
  • app/Http/Controllers/ModalController.php
    • Fallback-Daten fuer das DHL-Modal nutzen nun ebenfalls dynamische Produktvorschlaege.
  • tests/Unit/Dhl/DhlProductResolverTest.php
    • Tests fuer konfigurierbare internationale Laender ergaenzt.
    • Tests fuer Normalisierung und Filterung der Laenderliste ergaenzt.

Fachliche Entscheidung:

  • Die Checkboxen aktivieren nur V53PAK fuer DHL Paket International.
  • V66WPI Warenpost International bleibt davon unberuehrt und wird nicht versehentlich aktiviert.
  • DE wird nicht als internationales Zielland zugelassen, weil nationale Sendungen ueber V01PAK/V62KP laufen.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 27 Tests bestanden, 60 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

27.05.2026 - Phase 6: DHL-Gewicht korrekt berechnen

Status: abgeschlossen.

Ziel:

  • Das DHL-Labelgewicht beruecksichtigt Kompensationsprodukte.
  • Die bestehende Warenkorb-, Versandkosten- und Kompensationslogik bleibt unveraendert.
  • Admins koennen das Gewicht weiterhin im Modal erhoehen, aber nicht unter das berechnete DHL-Mindestgewicht druecken.
  • Produktbezogene DHL-Gewichtsgrenzen werden geprueft.

Umsetzung:

  • app/Services/DhlShipmentWeightCalculator.php
    • Neuer zentraler Gewichtsdienst fuer DHL-Labelerstellung.
    • Basisgewicht ist ShoppingOrder->weight in Gramm.
    • Fuer shopping_order_items mit comp > 0 wird das verknuepfte Product->weight je qty addiert.
    • Falls kein Gewicht vorhanden ist, wird ein sicherer Fallback von 1.0 kg genutzt.
    • Rundung erfolgt auf drei Nachkommastellen.
    • Produktlimits:
      • V01PAK: 31.5 kg
      • V53PAK: 31.5 kg
      • V62KP: 1.0 kg
  • app/Services/DhlModalService.php
    • Laedt Bestellpositionen jetzt mit Produktrelation.
    • Modal-Gewicht nutzt den neuen Kalkulator.
    • Validierung prueft das Gewicht gegen das gewaehlte DHL-Produkt.
  • app/Http/Controllers/DhlShipmentController.php
    • Vor Labelerstellung wird das tatsaechliche Versandgewicht auf mindestens das berechnete DHL-Gewicht gesetzt.
    • Ein manuell hoeheres Gewicht aus dem Formular bleibt erhalten.
  • app/Services/DhlShipmentService.php
    • Ermittelt serverseitig ebenfalls das Mindestgewicht, damit Queue- und Direktaufrufe abgesichert sind.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Prueft produktbezogene DHL-Gewichtsgrenzen vor Payload-Erstellung.
  • resources/views/admin/dhl/modal_in_order_shipment.blade.php
    • Hinweistext am Gewichtsfeld stellt klar, dass Kompensationsprodukte eingerechnet sind.
  • tests/Unit/Dhl/DhlShipmentWeightCalculatorTest.php
    • Neue Tests fuer:
      • Basisgewicht aus Bestellung
      • Addition von Kompensationsprodukt-Gewichten
      • Fallbackgewicht
      • V62KP-Gewichtslimit
      • erlaubtes Paketgewicht fuer V01PAK

Fachliche Entscheidung:

  • Die Berechnung wirkt nur auf das DHL-Labelgewicht.
  • ShoppingOrder->weight und die bestehende Checkout-/Versandkostenlogik werden nicht veraendert.
  • Kompensationsartikel werden nur dann addiert, wenn comp > 0 und ein Produktgewicht vorhanden ist.
  • V62KP wird mit 1.0 kg begrenzt, damit Kleinpaket nicht versehentlich fuer schwerere Sendungen genutzt wird.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 32 Tests bestanden, 66 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

27.05.2026 - Phase 7: Tracking-Codes und Tracking-Mails finalisiert

Status: abgeschlossen fuer Admin/User-Anzeige und automatische Mail-Ausloesung.

Ziel:

  • Tracking-Codes sind in Admin- und User-Order-Details sichtbar.
  • Automatische Tracking-Mails werden nur einmal pro Sendung versendet.
  • Mails werden nur bei relevanter Statusaenderung ausgeloest.
  • Statusspruenge wie created direkt nach out_for_delivery werden abgedeckt.
  • Mehrere passende Sendungen einer Bestellung werden weiterhin in einer Mail zusammengefasst.

Umsetzung:

  • packages/acme-laravel-dhl/src/Models/DhlShipment.php
    • Neue Konstante TRACKING_EMAIL_TRIGGER_STATUSES.
    • Relevante automatische Mail-Statuswerte:
      • in_transit
      • out_for_delivery
    • Neue Methode shouldTriggerTrackingEmail().
    • Bedingung:
      • aktueller Status ist relevant
      • Status hat sich gegenueber dem vorherigen Status geaendert
      • Tracking-Mail wurde noch nicht markiert
      • Sendung hat DHL-Sendungsnummer und Empfaengeradresse
    • Scope needsTrackingEmail() nutzt die zentrale Statusliste.
  • app/Console/Commands/DhlUpdateTracking.php
    • Automatische Mailentscheidung nutzt nun DhlShipment::shouldTriggerTrackingEmail().
    • Beim Zusammenfassen mehrerer Sendungen werden alle unbenachrichtigten Sendungen der Bestellung mit relevantem Status aufgenommen.
    • Dadurch werden direkte Spruenge nach out_for_delivery nicht mehr uebersehen.
  • app/Services/DhlTrackingService.php
    • DHL-Statusmapping erweitert:
      • transit, in-transit, in_transit -> in_transit
      • out-for-delivery, out_for_delivery -> out_for_delivery
      • weitere Varianten fuer pre_transit und failed
    • Mapping ist nun statisch nutzbar und separat testbar.
  • resources/views/admin/dhl/show.blade.php
    • Oeffentlicher Tracking-Link nutzt nun den korrekten Query-Parameter tracking_number.
  • resources/views/admin/sales/_detail_dhl_shipments.blade.php
    • Bereits vorhandener DHL-Block wird auch in den User-Order-Modalen mit eingebunden, Aktionen bleiben ueber isAdmin abgesichert.
  • tests/Unit/Dhl/DhlShipmentStatusTest.php
    • Tests fuer Tracking-Mail-Ausloesung bei out_for_delivery.
    • Tests fuer einmalige Mail-Ausloesung.
    • Tests, dass delivered keine neue automatische Tracking-Mail mehr ausloest.
    • Tests fuer DHL-Statusmapping-Varianten.

Fachliche Entscheidung:

  • Automatische Tracking-Mails werden bei in_transit oder out_for_delivery versendet.
  • delivered loest keine neue automatische Tracking-Mail aus, weil die Benachrichtigung dann fachlich zu spaet waere und Mehrfachmails vermieden werden sollen.
  • Manuelles erneutes Senden im Admin bleibt weiterhin moeglich.
  • Mehrere Sendungen einer Bestellung werden zusammengefasst, sofern sie noch keine Tracking-Mail erhalten haben und einen relevanten Status besitzen.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 38 Tests bestanden, 74 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

27.05.2026 - Nachtrag Phase 7: Tracking-E-Mail-Historie in der Detailansicht

Status: abgeschlossen.

Ziel:

  • In der DHL-Detailansicht soll nicht nur der letzte Tracking-Mail-Status sichtbar sein.
  • Es soll nachvollziehbar sein, welche Tracking-E-Mails mit welchem Sendungsstatus versendet wurden.
  • Automatische und manuelle Versendungen sollen unterscheidbar bleiben.
  • Zusammengefasste Tracking-Mails sollen zeigen, welche Sendungen enthalten waren.

Umsetzung:

  • packages/acme-laravel-dhl/src/Models/DhlShipment.php
    • markTrackingEmailSent() erweitert.
    • Bei jedem Versand wird ein Eintrag in api_response_data.tracking_email_history geschrieben.
    • Gespeicherte Felder je Eintrag:
      • sent_at
      • type (auto oder manual)
      • recipient_email
      • status
      • tracking_status
      • dhl_shipment_no
      • included_shipment_ids
    • Neue Methode getTrackingEmailHistory() liefert die Historie mit neuestem Eintrag zuerst.
    • Neue statische Methode getStatusBadgeClassFor() fuer Status-Badges in historischen Eintraegen.
  • app/Http/Controllers/DhlShipmentController.php
    • Manueller Tracking-Mail-Versand uebergibt Empfaenger und enthaltene Sendungen an markTrackingEmailSent().
  • app/Console/Commands/DhlUpdateTracking.php
    • Automatischer Tracking-Mail-Versand uebergibt Empfaenger und enthaltene Sendungen an markTrackingEmailSent().
  • resources/views/admin/dhl/show.blade.php
    • Bereich Tracking-E-Mail Status zeigt weiterhin den letzten Versand prominent an.
    • Darunter wird eine Historientabelle angezeigt mit:
      • Zeitpunkt
      • Typ
      • Status zum Versandzeitpunkt
      • Empfaenger
      • enthaltene Sendungs-IDs
  • tests/Unit/Dhl/DhlShipmentStatusTest.php
    • Tests fuer Tracking-Mail-Historie mit neuestem Eintrag zuerst.
    • Test fuer Legacy-Sendungen ohne Historie.

Fachliche Entscheidung:

  • Die Historie wird im bestehenden api_response_data JSON der Sendung gespeichert, damit keine neue Tabelle notwendig ist.
  • Die bestehenden Felder tracking_email_sent_at und tracking_email_type bleiben fuer schnelle Anzeige und bestehende Logik erhalten.
  • Historische Sendungen, die nur die alten Felder besitzen, bleiben kompatibel und zeigen weiterhin den letzten Versandstatus.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 40 Tests bestanden, 79 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

27.05.2026 - Nachtrag: Modale Vorabpruefung vor Labelerstellung

Status: abgeschlossen.

Ziel:

  • Vor der finalen DHL-Labelerstellung soll im bestehenden Erstellungsmodal eine sichtbare Vorabpruefung erscheinen.
  • Der Admin soll Produktcode, nationale/internationale Sendungsart, Zielland und Lieferadressstatus sehen.
  • Fehler blockieren die Labelerstellung; Warnungen bleiben sichtbar und muessen bewusst bestaetigt werden.

Umsetzung:

  • app/Http/Controllers/DhlShipmentController.php
    • Der bestehende Endpoint validateAddress() liefert nun zusaetzlich strukturierte preflight-Daten.
    • Die Produktpruefung nutzt DhlProductResolver.
    • Fehler aus Produkt-/Zielland-Kombinationen werden gemeinsam mit Adressfehlern zurueckgegeben.
  • app/Services/DhlProductResolver.php
    • Neue Methoden getProductScope() und getProductScopeLabel().
    • Dadurch kann die UI Produktcodes fachlich als national oder international anzeigen.
  • resources/views/admin/dhl/modal_in_order_shipment.blade.php
    • Neuer Statusbereich Vorabpruefung vor Labelerstellung.
    • Der Statusbereich sitzt am Ende des Formulars direkt vor den Aktionsbuttons.
    • Initialer Buttontext: Vorabpruefung durchfuehren.
  • app/Services/DhlAddressValidator.php
    • Die Lieferadresse wird zusaetzlich auf plausible Feldinhalte geprueft.
    • Offensichtlich ungueltige Werte bei Straße, PLZ oder Ort fuehren jetzt zu Fehlern statt nur zu einer positiven Formalpruefung.
    • Fuer DE, AT und CH ist eine formale DACH-Pruefung hinterlegt.
    • Diese umfasst Pflichtfelder, PLZ-Format, Plausibilitaet, Platzhalter-/Testdaten und Packstation-Regeln.
    • DACH-Hausnummern ohne Ziffer werden als Fehler blockiert.
    • Die UI weist nun ausdruecklich darauf hin, dass keine echte DHL-/Adressdatenbank- oder Leitcodepruefung angebunden ist.
    • Fuer unterstuetzte Ziellaender ohne landesspezifische Pruefung wird ein Hinweis auf die reine Basis-Adresspruefung ausgegeben.
  • resources/views/admin/dhl/modal_create_shipment.blade.php
    • Der erste Klick fuehrt nur die Vorabpruefung aus.
    • Die separate Browser-Alert-Fehlermeldung bei Vorpruefungsfehlern wurde entfernt.
    • Der Validierungsstatus zeigt nun Formale DACH-Pruefung statt irrefuehrend Aktiv.
    • Das Ergebnis wird direkt im Modal angezeigt:
      • Produktcode
      • Sendungsart
      • Zielland
      • normalisierte Lieferadresse
      • Status der Adressvalidierung
      • Fehler und Hinweise
    • Erst nach erfolgreicher Vorabpruefung wechselt der Button auf Sendung jetzt erstellen.
    • Aenderungen an Formularfeldern setzen die Freigabe automatisch zurueck.
  • tests/Unit/Dhl/DhlProductResolverTest.php
    • Neuer Test fuer nationale und internationale Produktklassifizierung.
  • tests/Unit/Dhl/DhlAddressValidatorTest.php
    • Neuer Test fuer unplausible Lieferadressfelder.
    • Neue Tests fuer CH-Adressvalidierung und Basispruefungs-Hinweise.
    • Neue Tests fuer Platzhalteradressen und DACH-Hausnummern ohne Ziffer.

Fachliche Entscheidung:

  • Die bestehende Servervalidierung bleibt die Quelle der Wahrheit.
  • Die Vorabpruefung ersetzt keine Validierung bei der finalen Erstellung; vor dem Erstellen wird nochmals gegen denselben Endpoint geprueft.
  • Dadurch werden nachtraegliche Formularaenderungen oder veraltete Modalzustande nicht ungeprueft an DHL uebergeben.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 46 Tests bestanden, 109 Assertions.
  • IDE/Linter-Pruefung der geaenderten Dateien:
    • Ergebnis: keine Fehler.

27.05.2026 - Phase 8: DHL-seitige Adressvalidierung ueber mustEncode

Status: umgesetzt fuer deutsche Empfaengeradressen.

Ziel:

  • DHL soll selbst die finale Adress-/Leitcodefaehigkeit pruefen.
  • Grundlage ist der DHL-Query-Parameter mustEncode=true.
  • printOnlyIfCodable ist laut DHL-Spezifikation der Legacy-Name.
  • Wenn diese Option gesetzt ist, soll DHL das Etikett nur dann erstellen, wenn die Adresse codeable/leitcodierbar ist.
  • Wird die Adresse von DHL abgelehnt, soll der Fehler im bestehenden Modal erscheinen und dort korrigierbar sein.
  • Laut DHL-Dokumentation ist die Funktion nur fuer deutsche Empfaengeradressen relevant.

Umsetzung:

  • Die aktuelle formale Vorabpruefung bleibt erhalten.
  • config/dhl.php
    • Neue Option print_only_if_codeable, steuerbar ueber DHL_PRINT_ONLY_IF_CODEABLE.
    • Standard: aktiv.
  • app/Http/Controllers/SettingController.php
    • DHL-Konfiguration liefert print_only_if_codeable.
  • resources/views/admin/settings/index.blade.php
    • Neue Checkbox DHL-Leitcodierung erzwingen (mustEncode).
  • app/Services/DhlDataHelper.php
    • Option wird in die Orderdaten fuer den DHL-Request uebernommen.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Fuer deutsche Empfaengeradressen wird bei aktiver Option der Query-Parameter mustEncode=true an die DHL Create-Shipment-Operation uebergeben.
    • DHL-Responses ohne Label/Shipment oder mit Item-Fehlerstatus werden vor dem Speichern der Sendung abgefangen.
    • Nicht leitcodierbare Adressen werden als DhlAddressValidationException normalisiert.
  • packages/acme-laravel-dhl/src/Exceptions/DhlAddressValidationException.php
    • Neue fachliche Exception fuer DHL-Adressablehnungen.
  • app/Services/DhlShipmentService.php
    • DHL-Adressablehnungen werden als Validierungsfehler zurueckgegeben.
    • Wenn Queue aktiv ist, aber mustEncode fuer eine deutsche Empfaengeradresse greift, wird synchron erstellt, damit der DHL-Fehler direkt im Modal sichtbar bleibt.
  • app/Http/Controllers/DhlShipmentController.php
    • Gibt DHL-Adressvalidierungsfehler mit HTTP 422 zurueck.
  • resources/views/admin/dhl/modal_create_shipment.blade.php
    • Fehler aus der finalen DHL-Erstellung werden in der bestehenden Vorabpruefungsbox angezeigt.
    • Keine separate Browser-Alert-Meldung.

27.05.2026 - Nachtrag: DHL-Laenderauswahl in Settings speicherbar

Status: abgeschlossen.

Problem:

  • Die Checkboxen fuer DHL Paket International Ziellaender liessen sich im Admin sichtbar anklicken.
  • Nach dem Speichern wurden die ausgewaehlten Laender aber nicht wieder angezeigt.
  • Ursache war DHL_CONFIG_SOURCE=env: Dadurch wurden die gespeicherten Datenbankwerte fuer diese Laenderliste beim Lesen ueberdeckt.
  • Zusaetzlich wurde eine leere Checkbox-Auswahl als null gespeichert und fiel dadurch wieder auf die .env-Konfiguration zurueck.

Umsetzung:

  • app/Http/Controllers/SettingController.php
    • dhl_international_countries wird beim Speichern normalisiert.
    • Ein leerer Wert wird als leere Liste gespeichert.
    • Beim Lesen wird ein vorhandener gespeicherter Array-Wert fuer diese Laenderliste verwendet, auch wenn DHL_CONFIG_SOURCE=env aktiv ist.
  • app/Services/DhlProductResolver.php
    • Nutzt gespeicherte DHL-Laender, sobald sie als Array vorhanden sind.
    • Wenn keine settings-Tabelle verfuegbar ist, bleibt der Config-Fallback erhalten.
  • app/Models/Setting.php
    • Object-Settings speichern leere Arrays jetzt als Array statt als null.
  • tests/Unit/Dhl/DhlProductResolverTest.php
    • Tests fuer gespeicherte Laender bei env-Prioritaet.
    • Test fuer bewusst leer gespeicherte Laenderliste.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 51 Tests bestanden, 117 Assertions.

Offene Punkte:

  • DHL-Sandbox-Verhalten testen.
  • Fehlercodes fuer nicht codeable Adressen sammeln.
  • Produktabdeckung fuer V01PAK, V62KP und V53PAK klaeren.
  • Klaeren, wie internationale Sendungen behandelt werden, da DHL mustEncode als nur fuer deutsche Empfaengeradressen relevant beschreibt.

Verifikation:

  • ./vendor/bin/pint --dirty --format agent
  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 49 Tests bestanden, 115 Assertions.

Phase 9 - Security & Code-Hygiene Hardening (Audit 2026-05-27)

Status: umgesetzt am 2026-05-27.

Im Rahmen eines Audits der bestehenden DHL-Umsetzung wurden mehrere sicherheits- und konsistenzrelevante Punkte mit Prio 1 identifiziert und behoben.

Prio 1.1 Credentials und PII aus Logs entfernen

Problem:

  • DhlShipmentService::createShipment() schrieb die komplette DHL-Config inklusive api_key, username, password, api_secret und Abrechnungsnummern via \Log::info('dhlConfig', $dhlConfig) in das Application-Log.
  • DhlDataHelper::prepareOrderData() schrieb das Roh-Options-Array (inkl. Versandadresse, Name, Telefon) in das Log.
  • ShippingService::createLabel() schrieb die fertige Order-Daten und in [DHL API] Sending payload to DHL zusaetzlich payload_json mit dem kompletten Payload (Adressen, Abrechnungsnummer) ins Log. Die Fehlerbranch loggte den vollen Payload ebenfalls. Die Response loggte zudem das base64-kodierte Label.

Umsetzung:

  • app/Services/DhlShipmentService.php
    • Neue statische Helper sanitizeDhlConfigForLog() und sanitizeOrderDataForLog().
    • sanitizeDhlConfigForLog() ersetzt Geheimnisse durch boolesche has_*-Flags und gibt nur nicht-vertrauliche Konfigurationsmetadaten zurueck.
    • sanitizeOrderDataForLog() reduziert Bestelldaten auf Routing-Felder (Order-ID, Produktcode, Empfaenger-Land, ersten beiden Stellen der PLZ, Reference-Flag).
    • Bestehende Logs nutzen jetzt diese Helper.
  • app/Services/DhlDataHelper.php
    • Kein Roh-Dump des $options-Arrays mehr. Statt dessen strukturiertes Log mit order_id, Produktcode und Flags.
  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • Neue private Helper buildPayloadLogContext() und buildResponseLogContext().
    • createLabel()-Log nutzt DhlShipmentService::sanitizeOrderDataForLog().
    • Payload-Log enthaelt nur Produktcode, letzte vier Stellen der Abrechnungsnummer, Gewicht, Empfaenger-Land, must_encode-Flag.
    • Response-Log enthaelt nur Sendungsnummer, Routing-Code, Status. Das base64-Label wird nicht mehr geloggt.
    • Error-Branch verwendet ebenfalls den redaktierten Payload-Log.

Tests:

  • tests/Unit/Dhl/DhlSanitizeLoggingTest.php (neu)
    • Stellt sicher, dass api_key, password, api_secret, username und Abrechnungsnummern niemals im Log-Kontext landen.
    • Stellt sicher, dass Name, Strasse, Telefon, E-Mail und PLZ aus den Bestelldaten heraus normalisiert werden.

Prio 1.2 CreateShipmentJob darf dhlConfig nicht serialisieren

Problem:

  • App\Jobs\CreateShipmentJob hat die DHL-Konfiguration im Konstruktor in eine public $dhlConfig-Property geschrieben. Dadurch wurden API-Key, Basic-Auth-Passwort und Abrechnungsnummern beim Dispatch (per serialize()) in den Queue-Speicher (Redis/Datenbank) abgelegt.

Umsetzung:

  • app/Jobs/CreateShipmentJob.php
    • public $dhlConfig entfernt.
    • Der Konstruktor akzeptiert den Parameter weiterhin (Backwards-Compat), persistiert ihn aber nicht mehr.
    • handle() laedt die DHL-Konfiguration jetzt ueber SettingController::getDhlConfig() direkt aus der kanonischen Quelle. Der Worker hat ohnehin Zugriff darauf.

Tests:

  • tests/Unit/Dhl/CreateShipmentJobSerializationTest.php (neu)
    • Serialisiert eine Job-Instanz und stellt sicher, dass keine der DHL-Secrets im Payload landen.
    • Bestaetigt, dass es keine dhlConfig-Property mehr gibt.

Prio 1.3 IDOR im DHL-Versand-Modal verhindern

Problem:

  • ModalController::handleDhlShipmentModal() haengt am auth-Middleware, aber nicht am admin-Guard. Jeder eingeloggte CRM-Nutzer konnte POST /modal/load mit action=create-dhl-shipment und einer beliebigen Order-ID aufrufen und damit Empfaenger-Name, Adresse, E-Mail sowie bestehende Sendungen einer fremden Bestellung auslesen.

Umsetzung:

  • app/Http/Controllers/ModalController.php
    • Neue private Methode authorizeDhlShipmentModal() prueft Auth::user()->isAdmin() und antwortet sonst mit 403.
    • Wird vor handleDhlShipmentModal() aufgerufen.

Tests:

  • tests/Unit/Dhl/DhlModalAuthorizationTest.php (neu)
    • Gaeste, VIP-User (admin == 1) und regulaere Berater (admin == 0) erhalten 403.
    • Echte Admins (admin >= 2) werden durchgelassen.

Prio 1.4 XSS-Schutz im Tracking-Frontend und in DataTables

Problem:

  • Im Backend-DataTable DhlShipmentController::index()/list() wurde customer = firstname + ' ' + lastname per addColumn mit anschliessendem rawColumns(['customer', ...]) ausgegeben. Manipulierte Adressdaten haetten HTML/JS injizieren koennen.
  • Im oeffentlichen Tracking-Frontend resources/views/public/tracking.blade.php wurden data.tracking_status, data.tracking_number, data.last_tracked_at und data.status ungefiltert in jQuery-.html()-Templates eingebaut.

Umsetzung:

  • app/Http/Controllers/DhlShipmentController.php
    • customer-Spalte ist jetzt e(trim($firstname.' '.$lastname)).
  • resources/views/public/tracking.blade.php
    • Neue JS-Funktion escapeTrackingHtml() (HTML-Entity-Encoding fuer &, <, >, ", ').
    • Alle Felder aus der Tracking-Response werden vor dem Einsetzen damit escaped.
    • Trackingnummer fuer den DHL-Link wird zusaetzlich mit encodeURIComponent() geschuetzt.
    • showError() nutzt .text() statt .html().
    • getStatusBadge() escaped Status-Default und CSS-Klassen, sodass unbekannte DHL-Statuscodes nicht aus dem class-Attribut ausbrechen koennen.

Prio 1.5 ReturnsService an DhlProductResolver anbinden, 'DEU'-Fallback entfernen

Problem:

  • ReturnsService::convertCountryCode() und convertAddressFor2LetterCountry() hielten eine eigene Hardcoded-Liste an Laendercodes und fielen bei unbekannten Codes still auf 'DEU' zurueck. Damit konnten Retouren fuer auslaendische Empfaenger versehentlich nach Deutschland geroutet werden und der DhlProductResolver (Single Source of Truth) wurde umgangen.

Umsetzung:

  • packages/acme-laravel-dhl/src/Services/ReturnsService.php
    • convertCountryCode() delegiert an DhlProductResolver::toDhlCountryCode().
    • convertAddressFor2LetterCountry() delegiert an DhlProductResolver::normalizeCountryCode().
    • Unbekannte Laender werfen jetzt eine InvalidArgumentException, statt unbemerkt eine deutsche Retoure zu erzeugen.
  • app/Http/Controllers/DhlShipmentController.php
    • Alle 'DEU'-Defaults in den Retouren-Pfaden (getBillingAddressForReturn(), createReturnLabelSync()) durch DhlProductResolver::DOMESTIC_COUNTRY ('DE') ersetzt, damit konsistente ISO-2-Codes an den Resolver gehen.

Tests:

  • tests/Unit/Dhl/ReturnsServiceCountryCodeTest.php (neu)
    • Mappt DE/AT/CH/ES korrekt auf ISO-3.
    • Akzeptiert bereits korrekte ISO-3-Codes.
    • Wirft fuer unbekannte Codes statt still DEU zurueckzugeben.
    • Normalisiert Adressen zurueck auf ISO-2 und respektiert fehlende country-Felder.

Prio 1.6 houseNumber = '1'-Default in ShippingService::parseAddressFields entfernen

Problem:

  • Wenn der Adressparser keine Hausnummer aus der Strasse extrahieren konnte, hat er die Hausnummer still auf '1' gesetzt. Das fuehrte dazu, dass Pakete an die falsche Adresse zugestellt werden konnten, ohne dass der Operator etwas davon mitbekam.

Umsetzung:

  • packages/acme-laravel-dhl/src/Services/ShippingService.php
    • parseAddressFields() wirft jetzt eine InvalidArgumentException mit klarer deutscher Fehlermeldung, sobald keine Hausnummer ermittelt werden konnte.
    • Das interne Erfolgs-Log enthaelt zusaetzlich nur noch Laengen / Praefixe, nicht mehr die volle Adresse.

Tests:

  • tests/Unit/Dhl/ShippingServiceParseAddressTest.php (neu)
    • Explizite Hausnummer wird unveraendert uebernommen.
    • Kombiniertes Strassenfeld wird sauber zerlegt.
    • Adresse ohne erkennbare Hausnummer fuehrt zu einer Validierungs-Exception.
    • Leere Eingabe fuehrt nicht zur Exception (kein neues Verhalten).

Verifikation Phase 9

  • php artisan test --compact tests/Unit/Dhl tests/Feature/DhlApiCurlLoggingTest.php
    • Ergebnis: 75 Tests bestanden, 195 Assertions.
  • ./vendor/bin/pint --dirty --format agent

Offene Punkte / weiterhin im Backlog

Die in Phase 9 noch offenen Backlog-Items wurden inzwischen in Phase 10 abgearbeitet (siehe dort).

Phase 10 - Backlog-Aufraeumen (2026-05-27)

Status: umgesetzt am 2026-05-27 direkt im Anschluss an Phase 9.

Prio 2.1 - Legacy DhlApiService entfernen

Problem:

  • app/Services/DhlApiService.php (1311 Zeilen) war ein veralteter SOAP-/SDK-basierter Service. Er referenzierte ein nicht existierendes Model App\Models\DhlShipment und das nicht installierte SDK christophschaeffer/dhl-business-shipping. Die Klasse wurde vom Autoloader geladen, war aber komplett tot (keine Aufrufer in app/, routes/, resources/, tests/, packages/). Jeder versehentliche Aufruf haette zur Laufzeit zu Class not found gefuehrt.

Umsetzung:

  • app/Services/DhlApiService.php ersatzlos geloescht.

Verifikation:

  • grep -r DhlApiService app/ routes/ resources/ packages/ tests/ config/ → keine Treffer mehr.
  • Verbleibende Vorkommen ausschliesslich in dev/2026-05-13-dhl-modul/legacy/... (historische Markdowns) und dev/app-bak/ (Backup-Verzeichnis).

Prio 2.2 - Request-/Prozess-Caching fuer getDhlConfig()

Problem:

  • SettingController::getDhlConfig() baute die DHL-Konfiguration bei jedem Aufruf von Grund auf neu auf und holte dabei rund 25 Werte einzeln per Setting::getContentBySlug() aus der Datenbank. Pro DHL-Vorgang gibt es mehrere Aufrufer (DhlShipmentService, CreateShipmentJob, DhlShipmentController fuer Cancel/Return, …), so dass schnell 50-100 redundante SELECT ... FROM settings-Queries pro Request entstanden.

Umsetzung:

  • app/Http/Controllers/SettingController.php
    • Neue statische Property private static ?array $cachedDhlConfig = null als prozessweiter In-Memory-Cache.
    • getDhlConfig() liefert beim zweiten Aufruf direkt aus dem Cache und triggert keine DB-Queries mehr.
    • Neue oeffentliche Methode flushDhlConfigCache() setzt den Cache zurueck. Sie wird automatisch in store() und updateDhlConfigCache() aufgerufen, sodass nach einer Settings-Aenderung der naechste Aufruf wieder frische Werte liefert.

Tests:

  • tests/Unit/Dhl/DhlConfigCachingTest.php (neu)
    • Verifiziert, dass getDhlConfig() einen vorbefuellten Cache zurueckgibt, ohne die Datenbank zu beruehren.
    • Verifiziert, dass flushDhlConfigCache() den Cache zuverlaessig leert.

Prio 2.3 - Doppelte public.tracking-Route bereinigen

Problem:

  • routes/domains/crm.php registrierte GET /admin/dhl/public/track mit dem Namen public.tracking innerhalb der admin-Middleware-Gruppe. Dadurch war die Route in Wahrheit nicht oeffentlich, sondern admin-only. Gleichzeitig existiert in routes/domains/main.php die korrekte oeffentliche Route GET /tracking ebenfalls unter dem Namen public.tracking. Da pro Request immer nur eine Domain-Routedatei geladen wird, gab es keinen direkten Konflikt, aber auf der CRM-Subdomain zeigte der Routenname auf den falschen Endpunkt.

Umsetzung:

  • routes/domains/crm.php
    • Die public.tracking-Definition wurde entfernt und durch einen erklaerenden Kommentar ersetzt.
  • resources/views/admin/dhl/show.blade.php
    • Der bisher einzige Verbraucher (route('public.tracking') im aktuell deaktivierten @if(false)-Block) zeigt jetzt absolut auf die Main-Domain via \App\Domain\EarlyDomainParser::getMainUrl().'/tracking'. Die Trackingnummer wird zusaetzlich mit urlencode() geschuetzt.

Tests:

  • tests/Unit/Dhl/DhlRouteRegistrationTest.php (neu)
    • Stellt sicher, dass im CRM-Routing kein Route::...->name('public.tracking')-Eintrag mehr existiert.
    • Stellt sicher, dass die Main-Domain weiterhin GET /tracking als public.tracking registriert.

Verifikation Phase 10

  • php artisan test --compact tests/Unit/Dhl tests/Feature/DhlApiCurlLoggingTest.php
    • Ergebnis: 79 Tests bestanden, 201 Assertions.
  • ./vendor/bin/pint --dirty --format agent

Verbleibendes Backlog

  • updateDhlConfigCache() ruft weiterhin \Artisan::call('config:clear') auf. Das ist global und kann bei gecachter Konfiguration zu kurzfristiger Latenz fuehren - akzeptabel, weil das Setting-Update ein seltener Admin-Vorgang ist.
  • dev/app-bak/Services/DhlApiService.php (Backup-Kopie ausserhalb des Autoloaders) bleibt als historische Referenz erhalten.

Phase 11 - Tracking-Hardening (Bugfix Live, 2026-05-27)

Status: umgesetzt am 2026-05-27.

Ausloeser: Im Live-System lieferte das Cockpit beim Klick auf "Tracking aktualisieren" die irrefuehrende Meldung Sendung nicht gefunden oder noch nicht im System erfasst. HTTP Status: 401, obwohl die Sendung auf der DHL-Website laengst zugestellt war. Gleichzeitig zeigte das Cockpit einen frischen last_tracked_at-Zeitstempel mit altem tracking_status-Text - typisches Symptom eines schon laenger fehlschlagenden Trackings.

Wurzelursachen

  1. Phantom-Fallback-Endpoint: DhlTrackingService::trackShipmentDE() rief https://api-eu.dhl.com/parcel/de/tracking/v0/shipments auf. Dieser Endpoint existiert in der offiziellen DHL-Doku nicht (siehe https://developer.dhl.com/api-reference/shipment-tracking). Es gibt nur die "Shipment Tracking - Unified API" unter https://api-eu.dhl.com/track/shipments. Der Fallback produzierte daher zwangslaeufig 401/404.
  2. Falsche User-Message bei 401: Eine 401 Unauthorized-Antwort wurde dem Operator als "Sendung nicht gefunden" gezeigt. Der eigentliche Fehler (DHL-API-Key ohne Tracking-Subscription im DHL Developer Portal) blieb verborgen.
  3. Stale-Daten erscheinen frisch: Sowohl updateTrackingSync() als auch updateTrackingBatch() setzten last_tracked_at = now() selbst dann, wenn die Tracking-Antwort fehlschlug. Damit wirkte ein veralteter Status im Cockpit "gerade eben aktualisiert".
  4. Per-Shipment-Fallback verbrennt Quota: Bei einem Batch-Auth-Fehler ist der Code in eine Schleife mit updateTracking() pro Sendung gegangen, was den 401 mit N Folgeaufrufen multipliziert.
  5. Tote api_secret-Property: Wurde im Konstruktor geladen aber nirgends im Request verwendet (Tracking-Unified-API kennt kein OAuth/Basic).

Umsetzung

  • app/Services/DhlTrackingService.php
    • Neue Konstante TRACKING_ENDPOINT = 'https://api-eu.dhl.com/track/shipments'.
    • trackShipmentDE() ersatzlos entfernt; trackShipment() hat keinen Fallback mehr.
    • Neue private Helper buildHttpOptions(), processSingleShipmentResponse(), isAuthErrorStatus(), buildAuthErrorMessage(), redactApiKey().
    • 401/403 werden explizit als auth_error => true plus http_status zurueckgegeben, mit klarer User-Message: "DHL Tracking API: Authentifizierung fehlgeschlagen (HTTP 401). Bitte pruefen, ob der hinterlegte DHL-API-Key gueltig ist und im DHL Developer Portal fuer 'Shipment Tracking - Unified' freigeschaltet wurde.".
    • 404 oder leere Sendungsliste werden als not_found => true markiert (eindeutig getrennt von Auth-Fehlern).
    • trackMultipleShipments() nutzt jetzt denselben gemeinsamen Pfad und liefert ebenfalls strukturierte auth_error-Antworten.
    • updateTrackingSync() setzt last_tracked_at nur noch bei Erfolg oder bei explizitem not_found. Auth- und Transport-Fehler lassen den Zeitstempel unangetastet, damit der naechste Cron-Lauf erneut versucht und das Cockpit den fehlschlagenden Zustand sichtbar macht.
    • updateTrackingBatch() bricht beim ersten Auth-Fehler ab (kein Per-Shipment-Fallback, kein erneutes Tracking) und markiert alle Sendungen der laufenden Charge mit auth_error. Transport-Exceptions fuehren ebenfalls nicht mehr zu einem last_tracked_at-Update.
    • Properties apiSecret und isSandbox entfernt.
    • Diagnose-Logs enthalten jetzt Endpoint, Status-Code und ein redaktiertes API-Key-Suffix (***<letzte 4>), aber niemals den vollen Key.

Tests

  • tests/Unit/Dhl/DhlTrackingAuthErrorTest.php (neu, 6 Tests)
    • HTTP 401 / 403 fuehrt zu auth_error => true mit klarer Message.
    • Es wird kein Fallback-Request mehr an /parcel/de/tracking/... gesendet.
    • 404 / leere Sendungsliste werden eindeutig als not_found zurueckgegeben.
    • Multi-Tracking liefert ebenfalls strukturierte auth_error-Antworten.
    • Das Auth-Fehler-Log enthaelt den vollen API-Key nicht (nur Suffix).
  • tests/Unit/Dhl/DhlTrackingStaleProtectionTest.php (neu, 3 Tests)
    • updateTracking() ruft bei 401 kein $shipment->update(...) auf - last_tracked_at bleibt also fuer den naechsten Cron-Versuch alt.
    • updateTracking() setzt bei "not found" gezielt nur last_tracked_at, damit nicht sofort erneut probiert wird.
    • updateTrackingBatch() bricht bei einem Auth-Fehler nach dem ersten Versuch ab (Http::assertSentCount(1)) und markiert alle drei Sendungen der Charge als auth_error => true.

Verifikation Phase 11

  • php artisan test --compact tests/Unit/Dhl tests/Feature/DhlApiCurlLoggingTest.php
    • Ergebnis: 88 Tests bestanden, 263 Assertions (vorher Phase 10: 79 Tests, 201 Assertions).
  • ./vendor/bin/pint --dirty --format agent

Operatives Vorgehen bei einem 401 im Live

Der Code kann das Problem nur sichtbar machen und nicht beheben - die eigentliche Loesung erfolgt im DHL Developer Portal:

  1. DHL Developer Portal → My Apps → die produktive App oeffnen.
  2. Unter "APIs" pruefen, ob Shipment Tracking - Unified API aktiv und genehmigt ist. Falls nicht: hinzufuegen und Production-Freischaltung beantragen (manuelle DHL-Approval).
  3. Den DHL_API_KEY (Consumer Key) aus dem Portal mit dem Wert in den Einstellungen / .env abgleichen.
  4. Nach Korrektur einmal Tracking aktualisieren im Cockpit anstossen; der last_tracked_at-Zeitstempel wird jetzt nur noch bei tatsaechlich erfolgreicher Antwort frisch gesetzt.

Phase 12 - 429-Handling und lokale Quota-Pause (Bugfix Live, 2026-05-27)

Status: umgesetzt am 2026-05-27 (Folgebefund zu Phase 11).

Ausloeser: Trotz Phase 11 zeigte das Live-System weiterhin veraltete Tracking-Stati. Konkretes Beispiel: Sendung 5082 (Trackingnummer 00340435065133094790) blieb seit 2026-05-27 05:00:33 auf "in der Region des Empfaengers angekommen", obwohl DHL bereits um 13:41 Uhr "Zustellung erfolgreich" meldete. Quer ueber alle 381 aktiven Sendungen war MAX(last_tracked_at) = 06:00:35 - der stuendliche Cron dhl:update-tracking lieferte ab 07:00 Uhr ueber 11 Stunden keine Updates mehr.

Wurzelursachen (revidiert)

  1. DHL-Tageslimit von 250 Aufrufen pro Tag erschoepft. Laut developer.dhl.com/api-reference/shipment-tracking bekommt jede Standard-App initial 250 calls per day, with a maximum of 1 call every 5 seconds. Direkter Test gegen den Live-Endpunkt belegte das: curl https://api-eu.dhl.com/track/shipments?trackingNumber=... lieferte HTTP 429 mit Body {"status":429,"title":"Too Many Requests","detail":"Too many requests within defined time period, please try again later."}. Der Antrag auf eine hoehere Quota muss im DHL Developer Portal gestellt werden.

    Hinweis zur ersten Diagnose: Wir hatten zunaechst vermutet, der Live-DHL_API_KEY sei der oeffentliche Sandbox-Demo-Key aus der DHL-Doku, weil der Wert mit den im Repo eingecheckten Sandbox-Beispielen uebereinstimmte (tests/Feature/DhlApiCurlLoggingTest.php, tests/DHL/curl-trace.txt). Diese Annahme war falsch - der Wert in der Live-.env ist ein echter produktiver Key, er hat nur das Standard-Tageslimit. Die fruehere Operator-Message mit dem Hinweis "Sandbox-Demo-Key" wurde entfernt.

  2. 429 wurde wie ein generischer Fehler behandelt: Der Code aus Phase 11 erkannte 401/403 als auth_error und 404 als not_found, aber 429 fiel in den Pfad "Fehler beim Abrufen der Tracking-Informationen. HTTP 429". Im Batch fuehrte das zum Per-Shipment-Fallback, der pro Sendung einen weiteren 429-Aufruf abgesetzt hat - die schon erschoepfte Quota wurde so noch schneller weiter belastet und der Cron-Lauf dauerte unnoetig lange.

  3. Folgelaeufe verbrennen Quota nur um 429 erneut zu sehen: Solange DHL noch im selben Quota-Fenster mit 429 antwortet, kostet jeder hourly-Cron mindestens einen HTTP-Roundtrip. Bei knapper Quota ist das ein Selbstlaeufer, der den Recovery-Moment nach dem Tages-Reset spuerbar verzoegert.

Umsetzung

  • app/Services/DhlTrackingService.php

    • processSingleShipmentResponse() erkennt HTTP 429 explizit und liefert rate_limited => true plus optional retry_after (Sekunden). Die User-Message lautet jetzt sachlich: "DHL Tracking API: Tageslimit erreicht (HTTP 429). Die DHL-API liefert vorruebergehend keine Daten mehr. Standard-Apps haben laut DHL-Doku 250 Aufrufe pro Tag und max. 1 Aufruf alle 5 Sekunden. Bei Bedarf im DHL Developer Portal eine Quota-Erhoehung beantragen." Der frueher hier eingefuegte "Sandbox-Demo-Key"-Hinweis ist entfernt.
    • trackMultipleShipments() erkennt 429 ebenfalls und liefert dieselbe strukturierte Antwort fuer die Batch-Route.
    • updateTrackingBatch() bekommt einen eigenen rate_limited-Pfad analog zu auth_error: Beim ersten 429 wird die gesamte Charge sofort als rate_limited markiert und der Cron-Lauf abgebrochen, ohne Per-Shipment-Fallback. last_tracked_at bleibt unangetastet, damit der naechste planmaessige Cron-Lauf die gleichen Sendungen erneut versucht (statt sie mit frischem Zeitstempel und altem Status zu konservieren).
    • Auch im "Sonstiger Batch-Fehler"-Fallback wird ein rate_limited-Befund pro Sendung als Abbruchgrund erkannt - die Schleife bricht sofort ab, ebenso wie schon bei auth_error.
    • Neuer Helper extractRetryAfter() interpretiert sowohl ganzzahlige Sekunden als auch RFC-7231-HTTP-date-Werte aus dem Retry-After-Header.
    • Logging in [DHL Tracking Service] Unified API rate-limited / Multi tracking rate-limited / Batch tracking aborted due to rate-limit enthaelt Endpoint, HTTP-Status, ein redaktiertes API-Key-Suffix (***<letzte 4>) und - falls von DHL geliefert - die Retry-After-Sekunden.
  • Lokale Quota-Pause (neu in Phase 12.1)

    • Neuer Cache-Key dhl_tracking:quota_paused_until haelt einen absoluten "do not call DHL before"-Zeitstempel. Default-Pause: 1 Stunde, wenn DHL keinen Retry-After-Header sendet; andernfalls genau die signalisierte Wartezeit.
    • Statische Helper auf DhlTrackingService: isQuotaPaused(), getQuotaPausedUntil(), pauseQuota(?int $retryAfterSeconds = null), clearQuotaPause().
    • trackShipment(), trackMultipleShipments() und updateTrackingBatch() pruefen die Pause vor jedem HTTP-Roundtrip. Ist sie aktiv, wird sofort eine rate_limited => true-Antwort mit paused_until zurueckgegeben - kein API-Aufruf. Damit kostet ein Cron-Lauf in einem ausgeschoepften Quota-Fenster 0 Calls statt 10+ und beschleunigt den Recovery-Moment nach Quota-Reset.
    • Beim ersten echten 429-Response wird pauseQuota($retryAfter) aufgerufen, sodass ein einzelner Cron-Lauf maximal einen "Quota-Probierschuss" pro Stunde absetzt.

Tests

  • tests/Unit/Dhl/DhlTrackingRateLimitTest.php (neu, 9 Tests)
    • 429-Single-Tracking-Response: Antwort ist rate_limited => true, enthaelt http_status = 429, retry_after = 120 und eine deutschsprachige Operator-Message mit DHL-Doku-konformem Hinweis (Tageslimit erreicht, 250 Aufrufe pro Tag, Quota-Erhoehung). Der frueher gepruefte String "Sandbox-Demo-Key" darf explizit nicht mehr vorkommen.
    • Multi-Tracking liefert dieselbe strukturierte 429-Antwort.
    • updateTrackingBatch() bricht nach genau einem HTTP-Aufruf ab, markiert die Charge als rate_limited und ruft $shipment->update(...) nicht auf.
    • HTTP-date als Retry-After wird korrekt in Sekunden umgerechnet.
    • Im Rate-Limit-Log taucht der volle API-Key nicht auf, nur das ***<suffix>.
    • Quota-Pause aktiviert sich aus dem Retry-After-Header und der zweite trackShipment()-Aufruf sendet keinen HTTP-Request mehr (Http::assertSentCount(1)).
    • Default-Pause ohne Retry-After-Header dauert ~1 Stunde (mit Toleranz fuer Test-Drift).
    • updateTrackingBatch() ueberspringt 25 Sendungen vollstaendig, wenn die Pause bereits gecached ist (Http::assertNothingSent()).
    • clearQuotaPause() raeumt den Cache-Eintrag wieder auf - wichtig fuer Tests und fuer Operator-Eingriffe.

Verifikation Phase 12

  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 96 Tests bestanden, 406 Assertions (vorher Phase 11: 88 Tests, 263 Assertions).
  • ./vendor/bin/pint --dirty --format agent

Operatives Vorgehen bei einem 429 im Live

Der Code-Fix verhindert, dass aus einer erschoepften Tracking-Quota ein mehrstuendiger Datenstau wird, das Cockpit veraltete Stati als "gerade aktualisiert" anzeigt oder die hourly-Crons die Recovery selbst weiter verzoegern. Die eigentliche Loesung liegt aber im DHL-Account:

  1. DHL Developer Portal -> My Apps -> produktive App oeffnen.
  2. Pruefen, ob Shipment Tracking - Unified API in der App als Production approved ist (nicht nur Sandbox). Standard-Quota: 250 Aufrufe pro Tag, max. 1 Aufruf alle 5 Sekunden.
  3. Im DHL Developer Portal eine Quota-Erhoehung beantragen (Antrag-Formular im API-Detailbereich). Solange das offen ist: Hourly-Cron mit Status-basierten Intervallen einplanen, dazu siehe "Empfehlung Sendungsvolumen vs. Quota" unten.
  4. Cron-Lauf beobachten: Nach dem ersten 429 bleibt der Cron fuer 1 Stunde lokal in einer Quota-Pause, ohne weitere DHL-Aufrufe. Der naechste dhl:update-tracking versucht es dann erneut. Sobald die Quota frei wird (Production-Erhoehung oder Tages-Reset), nimmt der Cron die Sendungen automatisch wieder mit.
  5. Falls Operator manuell sofort entsperren moechte: php artisan tinker -> App\Services\DhlTrackingService::clearQuotaPause().

Empfehlung Sendungsvolumen vs. Quota

Aktuell sind ~381 Sendungen aktiv (status IN ('created','in_transit','out_for_delivery','exception','unknown')). Die DHL-Standardquota von 250 Calls/Tag reicht dafuer mathematisch nicht aus, sobald jede Sendung mehrmals pro Tag aktualisiert werden soll. Solange keine erhoehte Quota bewilligt ist, sind drei Stellschrauben sinnvoll - Reihenfolge nach Wirkung:

  1. --stale-days enger setzen (z.B. 14 statt 30). Sendungen mit Status created, die seit zwei Wochen keinen Fortschritt zeigen, sind in der Praxis nicht zustellbar oder Karteileichen. markStaleShipmentsCompleted() schliesst sie automatisch ab und reduziert die Cron-Last spuerbar (in der aktuellen DB sind 266 von 381 aktiven Sendungen im Status created).
  2. Status-Intervalle verlaengern (DhlShipment::TRACKING_INTERVALS). Vorschlag fuer 250/Tag: out_for_delivery = 1h, in_transit = 6h, exception = 8h, unknown = 12h, created = 24h. Das passt grob in die Quota und behaelt schnelle Reaktion fuer die "letzte Meile".
  3. Vorhandener "Batch"-Pfad ist faktisch keine Ersparnis: Die DHL Unified Tracking API kennt laut Doku nur den Singular-Parameter trackingNumber. trackMultipleShipments() sendet ?trackingNumber=A,B,C,... - das wird als eine einzige unbekannte Sendungs-ID behandelt, schickt den Lauf in den Per-Shipment-Fallback und kostet einen Call extra pro Chunk. Wenn die Quota verlaesslich erhoeht ist, sollte dieser Pfad entweder gegen einzelne Calls mit 5-Sekunden-Throttling ersetzt oder ganz entfernt werden. Bewusst noch nicht angefasst, weil das Refactoring umfangreich ist - separate Phase 13 wenn gewuenscht.

Verbleibende Beobachtungen

  • Eine zusaetzliche Beobachtung (Monitoring) waere sinnvoll: ein Alarm wenn MAX(last_tracked_at) quer ueber alle aktiven Sendungen aelter als z.B. 3 Stunden ist. Das macht eine erschoepfte Quota oder einen haengenden Cron sofort sichtbar, ohne dass jemand einzelne Sendungen manuell prueft.
  • Default-Pause von 1 Stunde ist eine sinnvolle Heuristik, weil DHL kein verlaessliches Retry-After mitsendet. Falls die echte Quota-Reset-Zeit bekannt wird, koennte man die Default-Pause dynamisch bis zum naechsten Reset stellen.

Phase 13 - Echtes Single-Tracking + Quota-vertraegliche Intervalle (2026-05-27)

Status: umgesetzt am 2026-05-27 (Folge zu Phase 12 - die Quota-Erhoehung auf 10.000 calls/day wurde im DHL Developer Portal beantragt, kann aber nach Erfahrung mehrere Wochen brauchen. Bis dahin muss das System mit den 250/day-Standardlimits sauber funktionieren).

Ausloeser

Bei der Aufarbeitung von Phase 12 sind zwei strukturelle Schwachstellen aufgefallen:

  1. trackMultipleShipments() war kein echter Batch-Aufruf. Die DHL Unified Tracking API kennt laut Doku nur den Singular-Parameter trackingNumber. Der Code sendete ?trackingNumber=A,B,C,...,J als eine kommaseparierte Liste - DHL interpretierte das als eine unbekannte Sendungs-ID, lieferte ein leeres shipments-Array zurueck und der Code fiel in den Per-Shipment-Fallback. Effekt: ein verschwendeter Aufruf pro Chunk plus die zehn echten Aufrufe danach.
  2. Status-Intervalle aus Vor-Audit-Zeit: in_transit = 2h und created = 6h waren so eng, dass selbst mit perfekt funktionierender API ca. 2.500 Aufrufe pro Tag nötig waeren - rund das 10-fache des dokumentierten Standardlimits von 250 calls/day.

Umsetzung

  • app/Services/DhlTrackingService.php

    • trackMultipleShipments() komplett entfernt. Damit existiert nur noch der Single-Tracking-Pfad trackShipment(), der einen einzelnen trackingNumber an DHL sendet. Ein neuer Unit-Test (it does not have a trackMultipleShipments method anymore) friert diese Entfernung als Vertrag ein.
    • Neue statische Property $callIntervalSeconds = 5 plus Setter/Getter setCallIntervalSeconds() / getCallIntervalSeconds(). Voreinstellung folgt der DHL-Doku ("max 1 call every 5 seconds"). In Tests wird der Wert auf 0 gesetzt, damit die Suite nicht real schlaeft.
    • updateTrackingBatch() neu geschrieben: Anstelle der Chunks von 10 mit Pseudo-Batch-API laeuft jetzt eine echte HTTP-Anfrage pro Sendung, dazwischen sleep($callIntervalSeconds). Abbruchbedingungen unveraendert: Quota-Pause vor dem ersten Call, auth_error oder rate_limited brechen die Schleife sofort ab, alle restlichen Sendungen werden ohne weiteren API-Aufruf uebersprungen. Bei einem Transport-Fehler (DNS/TLS) wird die einzelne Sendung als transport_error markiert; der Loop laeuft weiter, weil das ein vorruebergehendes Netzproblem sein kann.
    • Das Cron-Log enthaelt jetzt zusaetzlich die Felder http_calls und call_interval_seconds, sodass Quota- und Throttle-Verhalten direkt nachvollziehbar sind.
  • packages/acme-laravel-dhl/src/Models/DhlShipment.php

    • TRACKING_INTERVALS angehoben:
      • out_for_delivery: 1 h (unveraendert - kundenrelevant, betrifft nur sehr wenige Sendungen gleichzeitig)
      • in_transit: 2 h -> 6 h
      • exception: 4 h -> 8 h
      • unknown: 4 h -> 12 h
      • created: 6 h -> 24 h
    • DEFAULT_TRACKING_INTERVAL: 4 h -> 8 h.
    • Indikative Rechnung mit aktuellem Live-Bestand (266 created + 115 in_transit):
      • created 1x/Tag = 266 calls/Tag
      • in_transit 4x/Tag = 460 calls/Tag
      • Summe ~726 calls/Tag bei vollstaendigem Throughput - reicht fuer 10.000/Tag-Quota dreifach, aktiviert bei 250/Tag-Standardquota nach ca. 5 Stunden die Quota-Pause aus Phase 12. Da bleibt der Tracking-Status fuer alle Sendungen ohne out_for_delivery mindestens 1x/Tag aktuell - das ist mit Standard-Quota verkraftbar.

Tests

  • tests/Unit/Dhl/DhlTrackingBatchThrottleTest.php (neu, 4 Tests)
    • 3 Sendungen erzeugen genau 3 HTTP-Calls, jede mit einzelner trackingNumber (kein Komma in den Query-Params).
    • setCallIntervalSeconds(1) -> 3 Sendungen brauchen >= 2 s Wall-Clock (sleep(1) zwischen Call 1->2 und 2->3).
    • getCallIntervalSeconds() Round-Trip plus -99 wird auf 0 geklemmt.
    • Vertragstest: DhlTrackingService::trackMultipleShipments existiert nicht mehr.
  • tests/Unit/Dhl/DhlTrackingStaleProtectionTest.php (angepasst)
    • Der "Batch bricht beim ersten Auth-Fehler ab"-Test pruef jetzt failed = 1 statt 3: in der neuen Logik wird nur die erste Sendung tatsaechlich angefragt, der Rest gar nicht erst gestartet. Das ist quotaschonender als das alte "Charge komplett als auth_error markieren"-Verhalten.
  • tests/Unit/Dhl/DhlTrackingRateLimitTest.php (angepasst)
    • Der frueher gegen trackMultipleShipments() gerichtete Test "marks the multi-tracking call as rate_limited too" ist ersatzlos entfernt, weil die Methode nicht mehr existiert.
    • Der 429-Batch-Test pruefe jetzt analog failed = 1 mit nur einem HTTP-Call - die anderen 11 Sendungen werden uebersprungen.
  • tests/Unit/Dhl/DhlTrackingAuthErrorTest.php (angepasst)
    • Der frueher direkte trackMultipleShipments()-Test ist entfernt, weil die Methode nicht mehr existiert.

Verifikation Phase 13

  • php artisan test --compact tests/Unit/Dhl
    • Ergebnis: 98 Tests bestanden, 398 Assertions.
  • ./vendor/bin/pint --dirty --format agent

Operatives Vorgehen

  • Solange Standard-Quota (250/day) aktiv ist: Mit den neuen Intervallen erreicht das System ein realistisches Limit, bevor die Quota-Pause aus Phase 12 zuschlaegt. out_for_delivery-Sendungen werden bevorzugt aktualisiert, der Rest folgt nach.
  • Sobald die beantragte 10.000/day-Quota durchgeht: Keine Code-Anpassung noetig. Die Intervalle sind so gewaehlt, dass auch der naechste Bestandszuwachs (mehr Sendungen) noch passt; bei sehr starkem Wachstum koennten in_transit wieder auf 4 h gesetzt werden.
  • Throttle bei eigenen Service Level: Wer einen vertraglich erweiterten Service Level mit > 1 call/sec hat, kann das im Bootstrap setzen, z.B. DhlTrackingService::setCallIntervalSeconds(2);. Default bleibt 5 s, weil das den Standard-Service-Levels und der Doku entspricht.

Verbleibende Empfehlung

  • --stale-days im Cron auf 14 setzen, sobald sicher ist, dass keine legitime Bestellung > 2 Wochen im Status created bleibt. Das reduziert den getrackten Sendungspool spuerbar (266 von 381 aktiven Sendungen sind aktuell created, viele davon sind reine Karteileichen, deren aelteste seit 2026-04-29 03:00 keine Aenderung mehr hatten). Bewusst noch nicht durchgefuehrt, weil das eine Geschaeftsentscheidung ist - der aktuelle Default bleibt bei --stale-days=30.

Legacy-Dokumentation

Die bisherigen Markdown-Dateien wurden nach dev/dhl-modul/legacy verschoben. Sie bleiben als Historie erhalten, sind aber nicht mehr die aktuelle Arbeitsgrundlage.