1462 lines
88 KiB
Markdown
1462 lines
88 KiB
Markdown
# 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`](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 (`***<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](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.
|