# 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 (`***`), 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`](https://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 (`***`) 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 `***`. - **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](https://developer.dhl.com/api-reference/shipment-tracking) 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.