feat: Packstation ueber DHL-Standortsuche als Lieferziel waehlbar
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m8s

Im Checkout laesst sich unter "Versand" jetzt zwischen der eigenen
Anschrift und einer DHL-Packstation waehlen. Postleitzahl eingeben,
suchen, Packstation aus der Trefferliste anklicken, DHL-Postnummer
eintragen — die Auswahl wird an der Bestellung gespeichert.

Aufbau:

- lib/dhl-locations.ts: Client fuer die DHL Location Finder Unified API
  (GET /find-by-address). Bewusst getrennt von lib/dhl.ts — das ist ein
  anderer DHL-Dienst mit anderer Authentifizierung: nur der API-Key im
  Header, kein OAuth und kein Geschaeftskundenvertrag. Die
  Packstation-Auswahl funktioniert damit auch, solange die
  Versandmarken-Erstellung noch nicht eingerichtet ist.
- api/shop/packstationen: serverseitiger Zwischenschritt, damit der
  DHL-API-Key nicht ins Browser-Bundle geraet. Streng rate-limitiert
  (15/Minute statt 60 wie die Produktsuche) und mit 15-Minuten-Cache:
  dahinter steht ein FREMDES Kontingent von 500 Aufrufen pro Tag, das ein
  ungebremstes Suchfeld an einem Nachmittag aufbrauchen koennte.
- Order/Address: packstationNumber + postNumber (Migration
  20260915222938_add_packstation_delivery, beide nullable).
- checkoutSchema: Postnummer als 6-10 Ziffern, plus zwei refine-Regeln —
  ohne Postnummer keine Packstation (DHL verlangt sie zwingend), und eine
  Packstation nur bei Versand, nicht bei Abholung.

Zwei Entscheidungen, die Folgearbeit ersparen:

- Bei Packstation-Auswahl tragen street/zip/city zusaetzlich die lesbare
  Anschrift ("Packstation 145", PLZ, Ort). Dadurch zeigen Rechnung,
  Bestaetigungsmail, Sendungsverfolgung und Admin-Ansicht ohne jede
  Aenderung etwas Sinnvolles; packstationNumber/postNumber tragen daneben
  das, was DHL technisch braucht.
- lib/dhl.ts baut daraus einen Locker-Empfaenger statt einer
  Hausanschrift. DHL kennt fuer den Empfaenger zwei sich ausschliessende
  Formen; eine Packstation als Strassentext zu schicken ergaebe eine Marke
  auf eine Anschrift, die es nicht gibt.

Die Option erscheint nur, wenn DHL_API_KEY gesetzt ist — sonst wuerde
Kundschaft in eine Suche laufen, die immer fehlschlaegt.

Geprueft: sechs Tests fuer das Umsetzen der DHL-Antwort (die
Packstation-Nummer steht in location.keywordId, NICHT in
location.ids[0].locationId — letzteres ist ein Systemcode, mit dem weder
Kundschaft noch DHL etwas anfangen kann). Dazu am laufenden
Produktions-Build: ohne Key liefert der Endpunkt 503 und der Checkout
zeigt die Option gar nicht; mit Key 400 bei ungueltiger PLZ und ein echter
Aufruf an api.dhl.com, der mit 401 quittiert wird — URL, Header und
Anfrageformat stimmen also. Im Browser durchgeklickt: Umschalten blendet
das Strassenfeld aus und Postnummer samt Suche ein, die Suche ruft die
Route und zeigt die Fehlermeldung an.

Feldnamen beider DHL-APIs vorab gegen developer.dhl.com geprueft. Ein
Lauf gegen einen echten Schluessel steht noch aus.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-16 00:37:00 +02:00
1 parent ff365c3cb1
commit 9dd71ffb9a
16 files changed
+757 -8

No files matched your search

+37 -1
View File
@@ -223,6 +223,14 @@ export interface DhlAddressInput {
country: string;
phone?: string | null;
email?: string | null;
/**
* Nur bei Lieferung an eine DHL-Packstation gesetzt (Order.packstationNumber).
* Ist sie vorhanden, verlangt DHL eine voellig andere Empfaengerstruktur —
* siehe buildDhlConsignee() unten.
*/
packstationNumber?: string | null;
/** DHL-Postnummer der empfangenden Person, 6-10 Ziffern (Order.postNumber). */
postNumber?: string | null;
}
export interface DhlShipperInput extends DhlAddressInput {
@@ -265,6 +273,31 @@ function buildDhlContact(address: DhlAddressInput, name1: string) {
};
}
/**
* Empfaengerblock fuer die Shipping-API v2.
*
* DHL kennt fuer den Empfaenger zwei sich gegenseitig ausschliessende
* Formen: eine normale Hausanschrift mit Strasse und Hausnummer, oder eine
* Packstation. Letztere wird NICHT als Strasse uebermittelt — DHL erwartet
* stattdessen "lockerID" (die Packstation-Nummer) zusammen mit der
* "postNumber" der empfangenden Person; Strassen- und Hausnummernfelder
* entfallen dabei ersatzlos. Eine Packstation als Strassentext zu schicken
* ergaebe eine Marke auf eine Anschrift, die es so nicht gibt.
*/
function buildDhlConsignee(address: DhlAddressInput, name: string) {
if (address.packstationNumber && address.postNumber) {
return {
name,
lockerID: address.packstationNumber,
postNumber: address.postNumber,
postalCode: address.zip,
city: address.city,
country: toIso3Country(address.country),
};
}
return buildDhlContact(address, name);
}
export async function createDhlShipmentLabel(
input: DhlCreateShipmentInput
): Promise<DhlCreateShipmentResult> {
@@ -279,7 +312,10 @@ export async function createDhlShipmentLabel(
// DHL erlaubt max. 35 Zeichen fuer refNo.
refNo: input.reference.slice(0, 35),
shipper: buildDhlContact(input.shipper, input.shipper.companyName),
consignee: buildDhlContact(input.receiver, `${input.receiver.firstName} ${input.receiver.lastName}`.trim()),
consignee: buildDhlConsignee(
input.receiver,
`${input.receiver.firstName} ${input.receiver.lastName}`.trim()
),
details: {
weight: { uom: "g", value: Math.max(Math.round(input.weightGrams), 1) },
},