Add DHL checkout integrations (autocomplete, postnummer, return label)

Wires the new backend DHL endpoints into checkout: an address-autocomplete
dropdown on the street fields, live Postnummer validation for Packstation
delivery, and a return-label download link on the order-detail page.
Proxied through Next.js API routes since DHL credentials are tenant-
specific and CheckoutContent is a Client Component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-31 13:01:05 +00:00
parent 51e4860fe3
commit 8a4170a1e6
9 changed files with 338 additions and 22 deletions
+21
View File
@@ -200,6 +200,10 @@ export type Product = {
spotlightHeadline: string | null;
spotlightText: string | null;
spotlightImage: string | null;
// Per-product opt-in for a wishlist heart on the homepage spotlight —
// independent of (in addition to) the global wishlistEnabled toggle,
// which still gates the feature site-wide regardless of this flag.
spotlightShowWishlist: boolean;
// Plain booleans, not the raw stock/threshold numbers — the public API
// has no reason to leak exact stock counts, callers only ever need
// "can this be bought right now". `outOfStock` on the product itself
@@ -249,6 +253,7 @@ type PayloadProduct = {
spotlightHeadline: string | null;
spotlightText: string | null;
spotlightImage: { url: string } | number | null;
spotlightShowWishlist: boolean;
trackInventory: boolean;
stock: number | null;
allowBackorder: boolean;
@@ -312,6 +317,7 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
spotlightText: product.spotlightText || null,
spotlightImage:
typeof product.spotlightImage === "object" && product.spotlightImage ? product.spotlightImage.url : null,
spotlightShowWishlist: product.spotlightShowWishlist,
outOfStock: isOutOfStock(product.trackInventory, product.stock, product.allowBackorder),
lowStock: isLowStock(product.trackInventory, product.stock, product.lowStockThreshold),
maxQty: maxPurchasableQty(product.trackInventory, product.stock, product.allowBackorder),
@@ -392,6 +398,21 @@ export async function getProductImagesByIds(ids: number[]): Promise<Map<number,
return map;
}
// Resolves a bare media id to its download URL — used by
// /konto/bestellungen/[orderNumber] for order.dhlReturnLabelMedia, which
// stays a raw id on the order fetch itself (that fetch is deliberately
// depth=0, see getCustomerOrderDetail's own comment) rather than bumping
// that fetch's depth just for this one occasional field. `no-store`, not
// ISR-cached like getProductImagesByIds — a return label is a one-off,
// account-specific document, not shared/reusable content worth caching.
export async function getMediaUrlById(id: number): Promise<{ url: string; filename: string } | null> {
const res = await fetch(`${PAYLOAD_URL}/api/media/${id}`, { cache: "no-store" });
if (!res.ok) return null;
const data: { url?: string; filename?: string } = await res.json();
if (!data.url) return null;
return { url: data.url, filename: data.filename ?? "download.pdf" };
}
// Derived from getProducts() (same 60s-ISR-cached fetch every other
// discovery surface already uses) instead of its own separate Payload
// query — also what lets the auto-spotlight rule below just be a plain