Medusa Plugin | OCI und cXML PunchOut-Kataloge erstellen | PunchCommerce                            ![](//analytics.punchcommerce.de/matomo.php?idsite=1&rec=1)

Medusa Plugin
=============

Ein Medusa v2 Plugin, das mit [PunchCommerce](https://www.punchcommerce.de/) integriert wird, um cXML/PunchOut Procurement Gateway Funktionalität zu ermöglichen. Procurement-Systeme leiten Einkäufer zu deinem Medusa-Storefront weiter, wo sie Artikel durchstöbern und in einen Warenkorb legen können, um diesen anschließend an das Procurement-System zurück zu übertragen.

Wie es funktioniert
-------------------

1. **Einkäufer klickt auf einen PunchOut-Link** in seinem ERP → PunchCommerce leitet zur **Entry-Route deines Storefronts** mit den Query-Parametern `sID` und `uID` weiter.
2. **Storefront authentifiziert den Einkäufer** über das Medusa SDK unter Verwendung des `punchcommerce` Auth-Providers (`sdk.auth.login("customer", "punchcommerce", { sID, uID })`).
3. **Storefront erstellt einen neuen Warenkorb** für die Sitzung und speichert die `sID` in `cart.metadata.punchcommerce_session_id`. Der Einkäufer kauft normal ein — Artikel werden über die Standard Store API hinzugefügt.
4. **Beim Checkout** ruft das Storefront `GET /store/punchout/basket?cart_id=...` auf, um den PunchOut-Warenkorb-Payload + eine `punchoutUrl` zu erhalten, und sendet den Warenkorb als `multipart/form-data` Formular an diese URL.

Installation
------------

```shell
npm install @punchcommerce/punchcommerce-medusa-plugin
```

Konfiguration
-------------

Das Plugin erfordert **zwei** Einträge in deiner `medusa-config.ts`: das Plugin selbst und einen Auth-Provider innerhalb des Auth-Moduls.

Füge das Plugin zu `medusa-config.ts` hinzu:

```ts
 plugins: [
     // ... andere Plugins
    {
      resolve: "@punchcommerce/punchcommerce-medusa-plugin",
      options: {
        punchcommerceUrl: process.env.PUNCHCOMMERCE_URL,
      },
    },
 ]
```

Registriere den punchcommerce-auth-Provider:

```ts
// medusa-config.ts
module.exports = defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      options: {
        providers: [
           // ... andere Provider
          {
            resolve: "@punchcommerce/punchcommerce-medusa-plugin/providers/punchcommerce-auth",
            id: "punchcommerce",
            options: {
              punchcommerceUrl: process.env.PUNCHCOMMERCE_URL,
              disableSessionValidation: false, // niemals in Produktion deaktivieren
            },
          },
        ],
      },
    },
  ],
})
```

### Optionen

OptionErforderlichStandardBeschreibung`punchcommerceUrl`Nein`https://www.punchcommerce.de`Basis-URL des PunchCommerce Gateways. Überschreiben für Staging- oder selbst gehostete Instanzen. Muss an **beide** Einträge (Plugin und Auth-Provider) übergeben werden.`disableSessionValidation`Nein`false`Auth-Provider Option. Wenn `true`, wird der Aufruf von `GET /gateway/v3/session/validate` übersprungen und jede formal korrekte `sID` akzeptiert. Gedacht für die lokale Entwicklung ohne Live-PunchCommerce-Instanz — **niemals in Produktion aktivieren**.> Hinweis: Die Gateway-Version ist derzeit auf `v3` festgelegt (siehe `src/modules/punchcommerce-client/service.ts`).

Kunden-Einrichtung
------------------

Kunden werden über eine Identität im Auth-Modul von Medusa mit PunchCommerce verknüpft.

1. **In PunchCommerce**: Erstelle einen Kunden und kopiere die *Kundenidentifikation* — dies ist die `uID`.
2. **In Medusa Admin**: Öffne die Detailseite des Kunden. In der rechten Seitenleiste zeigt das **PunchCommerce** Widget die aktuelle Verknüpfung an.
    - Klicke auf **Hinzufügen** (oder das Stift-Symbol) und füge die `uID` ein.
    - Wenn dieselbe `uID` bereits mit einem anderen Kunden verknüpft ist, gibt die API einen Fehler zurück und das Widget zeigt diesen an.
    - Verwende das Papierkorb-Symbol zum Aufheben der Verknüpfung. Die Verknüpfung wird auch automatisch entfernt, wenn der Kunde gelöscht wird.

PunchCommerce Konfiguration
---------------------------

Konfiguriere im PunchCommerce-Dashboard jeden Kunden mit:

- **Einstiegsadresse (Entry address)**: Die PunchOut-Landing-Route deines Storefronts, z.B. `https://my-store.com//punchcommerce/authenticate`
- **Kundenidentifikation (Customer identification)**: Dieselbe `uID`, die du im Medusa Admin eingegeben hast.

PunchCommerce leitet Einkäufer an die Einstiegsadresse weiter, wobei `?sID={UUID}&uID={identifier}` angehängt wird (plus etwaige [Aktionsparameter](#punchout-aktionen-optional)).

Storefront-Anforderungen
------------------------

Das Plugin ist nur für das Backend. Das Storefront muss den PunchOut-Flow orchestrieren.

### 1. Authentifizierungs-Route

Erstelle eine Route, auf die PunchCommerce weiterleitet. Sie muss das Medusa SDK mit dem `punchcommerce` Provider aufrufen und das Auth-Token + `sID` persistieren.

> Alle Beispiele verwenden das Next.js Starter Template:

```ts
// app/[region]/punchcommerce/authenticate/route.ts (Next.js)
import { sdk } from "@lib/config"
import { setAuthToken } from "@lib/data/cookies"
import { NextRequest, NextResponse } from "next/server"

export async function GET(request: NextRequest) {
  const sID = request.nextUrl.searchParams.get("sID")
  const uID = request.nextUrl.searchParams.get("uID")
  if (!sID || !uID) {
    // hier kann auch eine Fehlerseite gerendert werden
    return NextResponse.json({ error: "Fehlende sID oder uID" }, { status: 400 })
  }

  const token = await sdk.auth.login("customer", "punchcommerce", { sID, uID })
  if (typeof token !== "string") {
    // hier kann auch eine benutzerdefinierte Fehlerseite gerendert werden
    return NextResponse.json({ error: "Authentifizierung fehlgeschlagen" }, { status: 401 })
  }

  await setAuthToken(token)
  const res = NextResponse.redirect(new URL("/store", process.env.NEXT_PUBLIC_BASE_URL))

  return res
}
```

Was dies im Backend auslöst (siehe `src/providers/punchcommerce-auth/service.ts`):

1. Die `sID` wird gegen `GET /gateway/v3/session/validate` validiert (außer `disableSessionValidation` ist gesetzt).
2. Die Provider-Identität wird über `entity_id = uID` gesucht. Wenn kein Kunde mit dieser `uID` verknüpft ist, schlägt die Anfrage mit `"No PunchCommerce Identity found."` fehl.
3. Bei Erfolg gibt Medusa ein Auth-Token zurück, das auf den verknüpften Kunden beschränkt ist.

### 2. Sitzungsbezogener Warenkorb

Erstelle nach der Authentifizierung einen **neuen** Warenkorb für die PunchOut-Sitzung und hänge die `sID` an dessen Metadaten an. Alle Store-API-Operationen, die sich auf diesen Warenkorb beziehen, erben die Verknüpfung.

```ts
const { cart } = await sdk.store.cart.create({ region_id, currency_code: "eur" })
await sdk.store.cart.update(cart.id, {
  metadata: { punchcommerce_session_id: sID },
})
```

Bestehende Warenkörbe, die der Kunde außerhalb von PunchOut besitzt, bleiben unberührt. `getPunchOutCartStep` stellt sicher, dass der für jeden Transfer/Aktion verwendete Warenkorb die `punchcommerce_session_id` gesetzt hat.

### 3. PunchOut-Seite (ersetzt Checkout)

Anstatt des normalen Checkouts rendere eine dedizierte `/punchout`-Seite, die den vorbereiteten Warenkorb vom Backend lädt, dem Einkäufer zur Überprüfung anzeigt und ihn bei Klick über ein Formular an PunchCommerce sendet. Der Einkäufer sieht den JSON-Payload nie — nur die Warenkorb-Zusammenfassung und einen Button "An Beschaffungssystem senden".

**Data Loader** (Server Action, die die Store API anspricht):

```ts
// lib/data/punchcommerce.ts
"use server"
import { sdk } from "@lib/config"
import { getAuthHeaders, getCartId } from "./cookies"

export async function getPunchOutBasket() {
  const cartId = await getCartId()
  if (!cartId) return null

  return sdk.client.fetch(
    `/store/punchout/basket`,
    {
      method: "GET",
      cache: "no-store",
      query: { cart_id: cartId },
      headers: { ...(await getAuthHeaders()) },
    }
  )
}
```

**PunchOut-Seite**:

```tsx
// app/[countryCode]/(main)/punchout/page.tsx
export default async function PunchOutPage() {
  const data = await getPunchOutBasket()
  if (!data) return notFound()

  const { basket, punchoutUrl } = data

  return (

      PunchOut abschließen

        {basket.map((item, i) => (

            {item.quantity} × {item.product_name} ({item.product_ordernumber})

        ))}

        {/* Das versteckte Feld MUSS das Array in `{ basket }` einwickeln — das ist das
            Format, das der /gateway/v3/return Endpunkt von PunchCommerce erwartet. */}

        An Beschaffungssystem senden

  )
}
```

### 4. PunchOut-Aktionen (optional)

PunchCommerce kann `actions[]` an die Einstiegs-URL anhängen, um das Storefront aufzufordern, direkt nach der Authentifizierung zusätzliche Schritte durchzuführen. Das Backend stellt `GET /store/punchout/actions` zur Verfügung, um diese zu verarbeiten, und das Storefront entscheidet, was mit der Antwort zu tun ist.

AktionErforderliche ParameterEffekt`restore-basket``items=SKU:QTY,SKU:QTY`Fügt die aufgelisteten Artikel dem aktuellen Warenkorb hinzu. Fehlende SKUs werden als Warnhinweise zurückgegeben.`detail``ordernumber=SKU`Sucht den Product-Handle für die SKU. Storefront leitet zur Produktdetailseite weiter.`search``keyword=…`Storefront leitet zur eigenen Suchergebnisseite weiter.`background-search``keyword=…`Backend erstellt einen Warenkorb aus Suchergebnissen und gibt diesen zusammen mit einer `punchoutUrl` zurück (für Inline-PunchOut-Sitzungen, die Suchergebnisse zurücksenden).Einige Dinge, die vor der Implementierung zu beachten sind:

- **`restore-basket` wird immer ausgeführt**, wenn vorhanden, unabhängig von anderen Aktionen. Es verändert den Warenkorb und kann `warning` Benachrichtigungen für fehlende SKUs hinzufügen. Es setzt niemals eine Navigationsantwort.
- **Nur die erste ergebnisproduzierende Aktion gewinnt.** Wenn `actions[]` sowohl `detail` als auch `search` enthält, verarbeitet das Backend die erste und überspringt den Rest.
- Der Name der Eingabeaktion ist `background-search` (Bindestrich), aber der Diskriminator der Antwort ist `background_search` (**Unterstrich**) — verzweige immer nach `response.type`, nicht nach dem rohen Eingabestring.
- `notifications` (z.B. "SKU X nicht gefunden") überdauern den Aktionsaufruf, auch wenn eine Weiterleitung folgt. Speichere sie in einem Cookie oder einer Flash-Sitzung, um sie dem Einkäufer nach der Weiterleitung anzuzeigen.

**Data Loader** (zusammen mit `getPunchOutBasket` in `lib/data/punchcommerce.ts` hinzufügen):

> **Hinweis:** In der Authentifizierungs-Route wurden das Auth-Token und der Warenkorb gerade erst erstellt, daher können `getCartId()`/`getAuthHeaders()` die frisch gesetzten Cookies möglicherweise noch nicht lesen. Übergib beide Werte explizit von der Route aus; die Standardwerte funktionieren weiterhin für andere Aufrufer (z.B. beim Laden des Loaders von der `/punchout`-Seite nach Aufbau der Sitzung).

```ts
// lib/data/punchcommerce.ts
"use server"
import { sdk } from "@lib/config"
import { getAuthHeaders, getCartId } from "./cookies"

type PunchOutActionNotification = { type: "info" | "warning"; message: string }
type PunchOutActionResponse =
  | { type: "default" }
  | { type: "detail"; product_handle: string }
  | { type: "search"; keyword: string }
  | { type: "background_search"; basket: PunchOutPosition[]; punchoutUrl: string }

export async function processPunchOutActions(
  params: URLSearchParams,
  opts: { cartId?: string; authHeaders?: Record } = {}
): Promise {
  const cartId = opts.cartId ?? (await getCartId())
  if (!cartId) return null
  const headers = opts.authHeaders ?? { ...(await getAuthHeaders()) }

  // Alle Aktionsparameter (actions[], items, ordernumber, keyword) plus den Warenkorb weiterleiten.
  const query = new URLSearchParams(params)
  query.set("cart_id", cartId)

  return sdk.client.fetch(`/store/punchout/actions?${query.toString()}`, {
    method: "GET",
    cache: "no-store",
    headers,
  })
}
```

**Erweiterte Authentifizierungs-Route** — nach `setAuthToken` und Warenkorberstellung (Schritte 1–2), prüfe auf Aktionen und verzweige nach dem Ergebnis:

```ts
// app/[countryCode]/punchcommerce/authenticate/route.ts (erweitert von Schritt 1)
import { sdk } from "@lib/config"
import { getCacheTag, setAuthToken, setCartId } from "@lib/data/cookies"
import { processPunchOutActions, PunchOutPosition } from "@lib/data/punchcommerce"
import { NextRequest, NextResponse } from "next/server"

// Rendert eine Seite, die beim Laden automatisch ein POST-Formular an PunchCommerce sendet.
// Wird für background_search verwendet, wo der Einkäufer den Warenkorb nicht manuell überprüft.
function renderAutoSubmitForm(punchoutUrl: string, basket: PunchOutPosition[]) {
  // Entkomme Anführungszeichen, damit das JSON sicher innerhalb eines HTML-Attributwerts ist.
  const payload = JSON.stringify({ basket }).replace(/"/g, "&quot;")
  return `

      An Beschaffungssystem senden

  `
}

export async function GET(request: NextRequest, { params }) {
  const { countryCode } = await params
  const url = request.nextUrl
  const sID = url.searchParams.get("sID")
  const uID = url.searchParams.get("uID")
  if (!sID || !uID) {
    return NextResponse.json({ error: "Fehlende sID oder uID" }, { status: 400 })
  }

  const token = await sdk.auth.login("customer", "punchcommerce", { sID, uID })
  if (typeof token !== "string") {
    return NextResponse.json({ error: "Authentifizierung fehlgeschlagen" }, { status: 401 })
  }

  await setAuthToken(token)

  // Schritt 2: Erstelle einen neuen sitzungsbezogenen Warenkorb mit punchcommerce_session_id in den Metadaten.
  const authHeaders = { authorization: `Bearer ${token}` }
  const { cart } = await sdk.store.cart.create(
    { region_id, metadata: { punchcommerce_session_id: sID } },
    {},
    authHeaders
  )
  await setCartId(cart.id)

  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL!
  const hasActions = url.searchParams.has("actions[]") || url.searchParams.has("actions")

  if (!hasActions) {
    return NextResponse.redirect(new URL(`/${countryCode}/store`, baseUrl))
  }

  // Übergebe cart.id und das Token explizit — Cookies sind in dieser Anfrage noch nicht lesbar.
  const dispatch = await processPunchOutActions(url.searchParams, {
    cartId: cart.id,
    authHeaders,
  })
  const response = dispatch?.response ?? { type: "default" as const }

  switch (response.type) {
    case "detail":
      return NextResponse.redirect(
        new URL(`/${countryCode}/products/${response.product_handle}`, baseUrl)
      )

    case "search":
      // Weiterleitung zur Shop-Seite mit einem Suchbegriff.
      return NextResponse.redirect(
        new URL(`/${countryCode}/store?q=${encodeURIComponent(response.keyword)}`, baseUrl)
      )

    case "background_search":
      // Das Backend hat bereits einen Warenkorb aus den Ergebnissen der Keyword-Suche erstellt.
      // Gib eine sich selbst sendende Seite zurück, damit der Browser den Warenkorb direkt per POST an
      // PunchCommerce sendet — der Warenkorb MUSS in { basket } eingewickelt sein (wie in Schritt 3).
      return new NextResponse(
        renderAutoSubmitForm(response.punchoutUrl, response.basket),
        { headers: { "content-type": "text/html" } }
      )

    default:
      // restore-basket wurde ausgeführt (falls angefordert), setzte aber keine Navigationsantwort — gehe zum Shop.
      return NextResponse.redirect(new URL(`/${countryCode}/store`, baseUrl))
  }
}
```

> Siehe auch [https://www.punchcommerce.de/swagger#/E-Commerce-Integration/post\_punchcommerce\_authenticate](https://www.punchcommerce.de/swagger#/E-Commerce-Integration/post_punchcommerce_authenticate)

Warenkorb-Mapping (Cart Mapping)
--------------------------------

Das Plugin mappt jede Medusa-Warenkorbposition auf eine `PunchOutPosition` (`src/modules/punchcommerce-client/transform.ts`):

- `price_net` = die Zeile `subtotal` (netto), `price` = `total` (brutto), `item_price` = `subtotal / quantity` (Netto-Stückpreis)
- `tax_rate` wird aus der Warenkorbposition übernommen (dezimal, z.B. `0.19`)
- `product_name` wird auf **39 Zeichen** gekürzt (OCI/cXML Beschränkung)
- `packaging_unit` ist fest auf `"Piece"` eingestellt; ein Varianten-bezogenes Unit-Mapping (`PCE`, `KG`, `LTR`, …) ist **noch nicht implementiert**.
- Der Warenkorb wird als `multipart/form-data` durch das Storefront an `${punchcommerceUrl}/gateway/v3/return` gesendet.

Warenkorb-Lebenszyklus (Cart Lifecycle)
---------------------------------------

Nach einer erfolgreichen Übertragung wird der Medusa-Warenkorb **nicht** automatisch als abgeschlossen markiert, archiviert oder gelöscht — er bleibt in seinem aktuellen Zustand. Empfohlenes Storefront-Verhalten:

- Starte die nächste PunchOut-Sitzung, indem du einen brandneuen Warenkorb mit der neuen `sID` in seinen Metadaten erstellst.

(Die tatsächliche Bestellung wird später über PunchCommerce / das ERP erstellt — Medusa dient nur als Katalog-Oberfläche zum Browsen.)

Parallele Sitzungen
-------------------

Warenkörbe sind pro `cart_id` abgegrenzt, nicht pro Kunde. Daher kann ein einzelner, mit PunchCommerce verknüpfter Kunde mehrere unabhängige PunchOut-Sitzungen gleichzeitig aktiv haben.

REST API Referenz
-----------------

### `GET /store/punchout/basket`

Kunden-authentifiziert (Bearer oder Session). Erstellt einen PunchOut-Warenkorb aus einem sitzungsbezogenen Medusa-Warenkorb.

QueryErforderlichBeschreibung`cart_id`JaWarenkorb, dessen Metadaten `punchcommerce_session_id` enthalten.**Antwort:** `{ basket: PunchOutPosition[], punchoutUrl: string }`

### `GET /store/punchout/actions`

Kunden-authentifiziert. Verarbeitet eine oder mehrere PunchOut-Einstiegsaktionen.

QueryErforderlichBeschreibung`cart_id`JaWarenkorb, auf dem operiert werden soll.`actions[]`JaEine oder mehrere von `restore-basket`, `detail`, `search`, `background-search`.`items`Für `restore-basket`Kommagetrennte `SKU:QTY` Paare.`ordernumber`Für `detail`SKU, die nachgeschlagen werden soll.`keyword`Für `search` / `background-search`Freitext-Suchbegriff.**Antwort:** `{ notifications: PunchOutActionNotification[], response: PunchOutActionResponse }` — siehe `src/modules/punchcommerce-client/types.ts`.

### `GET | POST | DELETE /admin/customers/:id/punchcommerce-customer`

Admin-authentifiziert. Unterstützt das Kunden-Detail-Widget.

- **GET** → `{ punchcommerce_customer: { uid: string } | null }`
- **POST** Body `{ uid: string }` — Erstellt oder aktualisiert die Verknüpfung.
- **DELETE** — Entfernt die Verknüpfung.

Typen-Referenz
--------------

Alle Typen werden von `punchcommerce/modules/punchcommerce-client/types` exportiert.

### `PunchOutPosition`

Eine einzelne Position im PunchOut-Warenkorb. Das Plugin erstellt eine Position pro Medusa-Warenkorbposition.

```ts
type PunchOutPosition = {
  product_ordernumber: string   // SKU der Variante; Primärschlüssel in PunchCommerce
  product_name: string          // Anzeigename, gekürzt auf 39 Zeichen (OCI/cXML Limit)
  quantity: number              // Ganzzahlige Menge für diese Position
  item_price: number            // Netto-Stückpreis (= price_net / quantity)
  price: number                 // Brutto-Positionssumme (mit Steuer) — Medusas `line.total`
  price_net: number             // Netto-Positionssumme (ohne Steuer) — Medusas `line.subtotal`
  tax_rate: number              // Dezimaler Steuersatz, z.B. 0.19 für 19%
  type: "product" | "shipping-costs"  // "shipping-costs" reserviert; derzeit sind alle Zeilen Produkte
  product: PunchOutProduct      // Eingebettete Produkt-Stammdaten (siehe unten)
}
```

### `PunchOutProduct`

Produkt-Daten, die in jeder `PunchOutPosition` eingebettet sind. Werden an PunchCommerce gesendet, damit das Beschaffungssystem das Produkt speichern/anzeigen kann, auch wenn der Katalog des Einkäufers es nicht enthält.

```ts
type PunchOutProduct = {
  id: string                    // Interne Produkt-ID (Medusa product_id) — informativ
  ordernumber: string           // SKU — dupliziert PunchOutPosition.product_ordernumber
  brand_ordernumber: string     // Hersteller-Bestellreferenz; derzeit identisch mit `ordernumber`
  title: string                 // Voller, ungekürzter Produkttitel
  description: string           // Produktbeschreibung als Klartext
  image_url?: string | null     // URL zum Vorschaubild der Variante oder des Produkts
  price: number                 // Netto-Stückpreis (spiegelt PunchOutPosition.item_price wider)
  currency: string              // ISO 4217 Code, kleingeschrieben (z.B. "eur") — wird vom Warenkorb übernommen
  tax_rate: number              // Derselbe dezimale Wert wie PunchOutPosition.tax_rate
  packaging_unit: string        // Heute fest auf "Piece"; Zukunft: pro-Variante Mapping
  shipping_time: number         // Heute fest auf 0
  active: "true" | "false"      // String (kein Boolean) — PunchCommerce Konvention

  // Optionale Felder — werden von diesem Plugin noch nicht befüllt, aber von PunchCommerce akzeptiert:
  brand?: string
  customer_ordernumber?: string
  category?: string
  description_long?: string
  purchase_unit?: number
  reference_unit?: number
  unit?: string                 // OCI Unit Code, z.B. "PCE", "KG", "LTR"
  unit_name?: string            // Lesbarer Einheitenname
  weight?: number
  classification_type?: string
  classification?: string
}
```

### `PunchOutBasket`

Warenkorb-Wrapper auf oberster Ebene. **Dies ist das Format, das der PunchCommerce `/gateway/v3/return` Endpunkt erwartet** — beim Senden des Formulars muss das Positions-Array in `{ basket: [...] }` eingewickelt werden.

```ts
type PunchOutBasket = {
  basket: PunchOutPosition[]
}
```

### `PunchOutActionItem`

Artikel, der an die `restore-basket` Aktion übergeben wird. Die Route parst den `items=SKU:QTY,SKU:QTY` Query-String in ein Array dieser Objekte.

```ts
type PunchOutActionItem = {
  sku: string
  quantity: number
}
```

### `PunchOutActionNotification`

Warnung / Info-Meldung, die zusammen mit einer Aktionsantwort zurückgegeben wird (z.B. wenn eine SKU in `restore-basket` nicht gefunden wurde).

```ts
type PunchOutActionNotification = {
  type: "info" | "warning"
  message: string
}
```

### `PunchOutActionResponse`

Diskriminierte Union, die von `GET /store/punchout/actions` zurückgegeben wird. Das Storefront verzweigt nach `type`, um zu entscheiden, was als Nächstes zu tun ist.

```ts
type PunchOutActionResponse =
  | { type: "default" }                                   // Keine Aktion hat ein Ergebnis geliefert — normal fortfahren
  | { type: "detail"; product_handle: string }            // Leite den Einkäufer zur PDP unter diesem Handle weiter
  | { type: "search"; keyword: string }                   // Leite zur Suchseite deines Storefronts weiter
  | {                                                     // Inline-Search PunchOut: sende den zurückgegebenen Warenkorb
      type: "background_search"
      basket: PunchOutPosition[]
      punchoutUrl: string
    }
```

 [ PunchCommerce® ist ein Produkt der ![Netzdirektion GmbH](https://www.punchcommerce.de/static/netzdirektion-logo.png "PunchCommerce® ist ein Produkt der netzdirektion | Gesellschaft für digitale Wertarbeit mbH") ](https://netzdirektion.de)

 [Feedback erwünscht - Ihre Meinung hilft uns, noch besser zu werden!](https://easy-feedback.de/umfrage/1883200/5FuM95 "Ihre Meinung hilft uns, noch besser zu werden!")
