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]>
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { parseLocations } from "./dhl-locations";
|
|
|
|
/**
|
|
* Ein Treffer im Format der DHL Location Finder API. Die Packstation-Nummer
|
|
* steht in location.keywordId — location.ids[0].locationId ist ein
|
|
* zusammengesetzter Systemcode und ausdruecklich NICHT die Nummer, die
|
|
* Kundschaft am Geraet eingibt.
|
|
*/
|
|
function dhlLocation(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
url: "https://api.dhl.com/location-finder/v1/locations/8003-453113145",
|
|
location: {
|
|
ids: [{ locationId: "8003-453113145", provider: "parcel-de" }],
|
|
keyword: "DHL Packstation",
|
|
keywordId: "145",
|
|
type: "locker",
|
|
},
|
|
name: "Packstation 145",
|
|
distance: 820,
|
|
place: {
|
|
address: {
|
|
countryCode: "DE",
|
|
postalCode: "86450",
|
|
addressLocality: "Altenmünster",
|
|
streetAddress: "Bahnhofstr. 1",
|
|
},
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("parseLocations", () => {
|
|
it("nimmt die Packstation-Nummer aus keywordId, nicht aus locationId", () => {
|
|
const [packstation] = parseLocations([dhlLocation()]);
|
|
|
|
// locationId waere "8003-453113145" — damit koennte niemand etwas anfangen,
|
|
// und DHL wuerde die Versandmarke ablehnen.
|
|
expect(packstation.number).toBe("145");
|
|
expect(packstation.zip).toBe("86450");
|
|
expect(packstation.city).toBe("Altenmünster");
|
|
expect(packstation.street).toBe("Bahnhofstr. 1");
|
|
expect(packstation.distanceMeters).toBe(820);
|
|
});
|
|
|
|
it("sortiert nach Entfernung aufsteigend", () => {
|
|
const parsed = parseLocations([
|
|
dhlLocation({ distance: 3000, location: { keywordId: "300", keyword: "DHL Packstation" } }),
|
|
dhlLocation({ distance: 120, location: { keywordId: "120", keyword: "DHL Packstation" } }),
|
|
]);
|
|
|
|
expect(parsed.map((p) => p.number)).toEqual(["120", "300"]);
|
|
});
|
|
|
|
it("ueberspringt einen Eintrag ohne Packstation-Nummer, statt die ganze Suche scheitern zu lassen", () => {
|
|
const parsed = parseLocations([
|
|
{ location: { keyword: "DHL Packstation" }, place: { address: {} } },
|
|
dhlLocation(),
|
|
]);
|
|
|
|
// Ein Standort ohne Nummer ist unbrauchbar — die uebrigen bleiben gueltig.
|
|
expect(parsed).toHaveLength(1);
|
|
expect(parsed[0].number).toBe("145");
|
|
});
|
|
|
|
it("faellt auf einen zusammengesetzten Namen zurueck, wenn DHL keinen liefert", () => {
|
|
const [packstation] = parseLocations([dhlLocation({ name: undefined })]);
|
|
|
|
expect(packstation.name).toBe("Packstation 145");
|
|
});
|
|
|
|
it("kommt ohne Strasse und ohne Entfernung zurecht", () => {
|
|
const [packstation] = parseLocations([
|
|
dhlLocation({
|
|
distance: undefined,
|
|
place: { address: { postalCode: "10115", addressLocality: "Berlin" } },
|
|
}),
|
|
]);
|
|
|
|
expect(packstation.street).toBe("");
|
|
expect(packstation.distanceMeters).toBeNull();
|
|
// Ohne Entfernung darf der Eintrag nicht verschwinden, nur ans Ende rutschen.
|
|
expect(packstation.city).toBe("Berlin");
|
|
});
|
|
|
|
it("liefert fuer eine leere Trefferliste ein leeres Ergebnis", () => {
|
|
expect(parseLocations([])).toEqual([]);
|
|
});
|
|
});
|