> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upcart-Upsells

> Verwende eine Strategy, um die im Upsells-Modul von Upcart angezeigten Produkte dynamisch auszuwählen.

<div id="overview">
  ## Überblick
</div>

Upcart ist eine von Aftersell getrennte App, daher sind Strategies nicht so in das Upsells-Modul integriert wie in die Post-Purchase- und Checkout-Flows von Aftersell. Stattdessen verbindest du die beiden Apps mit einem kleinen Skript, das die Strategies-API direkt aufruft und das Ergebnis über die öffentliche API von Upcart in das bestehende Upsells-Modul von Upcart einspeist.

Das Skript ist **direkt einsetzbar** – füge es einmal in das benutzerdefinierte HTML von Upcart ein, ersetze zwei Werte (deinen Strategy-API-Schlüssel und die Strategy-ID), und das Upsells-Modul zeigt fortan die Produkte an, die die Strategy zurückgibt.

***

<div id="what-youll-need">
  ## Was du benötigst
</div>

1. **Deinen Strategy-API-Schlüssel.** Gehe in Aftersell zu **Settings → Product Strategy** und kopiere in der Karte **Security Token** deinen Token (das ist dein Strategy-API-Schlüssel).
2. **Die Strategy-ID.** Öffne die Strategy, die du ausführen möchtest, im Aftersell-Strategy-Editor und kopiere ihre ID.
3. **Das aktivierte Upsells-Modul in Upcart.** Das Skript überschreibt die Liste der im bestehenden Upsell-Block angezeigten Produkte. Das Modul muss also aktiviert sein, damit überhaupt etwas gerendert wird.

***

<div id="adding-the-script">
  ## Das Skript hinzufügen
</div>

Gehe in Upcart zu **Settings → Custom HTML → Scripts (before load)** und füge das folgende Skript ein. Ersetze `STRATEGY_ID` und `STRATEGY_API_KEY` durch die Werte aus Aftersell und speichere dann.

```html theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
<script>
  const STRATEGY_ID = "YOUR_STRATEGY_ID";
  const STRATEGY_API_KEY = "YOUR_STRATEGY_API_KEY";
  const STRATEGY_BACKEND_URL = "https://start.aftersell.app";

  let cartToken = null;
  const fetchCartToken = async () => {
    const res = await fetch("/cart.js");
    const c = await res.json();
    cartToken = c.token;
  };

  const mapCartItemToContext = (cartItem) => ({
    productId: "gid://shopify/Product/" + cartItem.productId.toString(),
    variantId: "gid://shopify/ProductVariant/" + cartItem.variantId.toString(),
    tags: [],
    title: cartItem.title,
    vendor: cartItem.vendor,
    productType: cartItem.productType,
    handle: cartItem.handle,
    quantity: cartItem.quantity,
    price: cartItem.originalPrice / 100,
  });

  // --- StrategyProduct -> Upcart Product conversion -----------------------

  const gidToNumericId = (gid) => Number(String(gid).split("/").pop());
  const priceStringToCents = (price) =>
    price == null ? null : Math.round(parseFloat(price) * 100);

  const strategyMetafieldsToProductMetafields = (metafields = []) => {
    const grouped = {};
    for (const { namespace, key, value } of metafields) {
      grouped[namespace] = grouped[namespace] || {};
      grouped[namespace][key] = value;
    }
    return { product: grouped };
  };

  const deriveProductOptions = (variants = []) => {
    const byName = new Map();
    for (const variant of variants) {
      (variant.selectedOptions ?? []).forEach((opt, idx) => {
        if (!byName.has(opt.name)) {
          byName.set(opt.name, { name: opt.name, position: idx + 1, values: [] });
        }
        const entry = byName.get(opt.name);
        if (!entry.values.includes(opt.value)) entry.values.push(opt.value);
      });
    }
    return [...byName.values()];
  };

  const mapStrategyVariantToProductVariant = (variant) => {
    const selected = variant.selectedOptions ?? [];
    const optionValues = selected.map((o) => o.value);
    return {
      id: gidToNumericId(variant.variantId),
      title: variant.title,
      option1: optionValues[0] ?? null,
      option2: optionValues[1] ?? null,
      option3: optionValues[2] ?? null,
      sku: variant.sku ?? "",
      requires_shipping: true,
      taxable: true,
      featured_image: null,
      available: variant.availableForSale,
      name: variant.title,
      public_title: variant.title,
      options: optionValues,
      price: priceStringToCents(variant.price) ?? 0,
      weight: 0,
      compare_at_price: priceStringToCents(variant.compareAtPrice),
      inventory_management: "",
      barcode: null,
      requires_selling_plan: false,
      selling_plan_allocations: [],
    };
  };

  const mapStrategyProductToProduct = (product) => {
    const variants = (product.variants ?? []).map(mapStrategyVariantToProductVariant);
    const variantPrices = variants.map((v) => v.price);
    const priceMin = variantPrices.length ? Math.min(...variantPrices) : (priceStringToCents(product.price) ?? 0);
    const priceMax = variantPrices.length ? Math.max(...variantPrices) : (priceStringToCents(product.price) ?? 0);

    const variantCompareAtPrices = variants
      .map((v) => v.compare_at_price)
      .filter((p) => p != null);
    const compareAtMin = variantCompareAtPrices.length ? Math.min(...variantCompareAtPrices) : 0;
    const compareAtMax = variantCompareAtPrices.length ? Math.max(...variantCompareAtPrices) : 0;

    const images = (product.images ?? [])
      .slice()
      .sort((a, b) => a.position - b.position)
      .map((img) => img.src);

    return {
      id: gidToNumericId(product.productId),
      title: product.title,
      handle: product.handle,
      description: product.description ?? "",
      published_at: "",
      created_at: "",
      vendor: product.vendor ?? "",
      type: product.productType ?? "",
      tags: [...(product.tags ?? [])],
      price: priceStringToCents(product.price) ?? 0,
      price_min: priceMin,
      price_max: priceMax,
      available: product.availableForSale,
      price_varies: priceMin !== priceMax,
      compare_at_price: priceStringToCents(product.compareAtPrice),
      compare_at_price_min: compareAtMin,
      compare_at_price_max: compareAtMax,
      compare_at_price_varies: compareAtMin !== compareAtMax,
      variants,
      images,
      featured_image: images[0] ?? "",
      options: deriveProductOptions(product.variants),
      url: product.url ?? "",
      media: [],
      requires_selling_plan: false,
      selling_plan_groups: [],
      metafields: strategyMetafieldsToProductMetafields(product.metafields),
    };
  };

  // -----------------------------------------------------------------------

  let replacedUpsells = null;
  let lastFetchedCartSignature = null;

  const cartSignature = (cart) =>
    JSON.stringify(cart.items.map((i) => [i.variantId, i.quantity]));

  const runStrategyEvaluation = async () => {
    if (!STRATEGY_ID || !STRATEGY_API_KEY) return;

    await fetchCartToken();
    if (!cartToken) return;

    const cart = window.upcartGetCart();
    if (!cart) return;

    const signature = cartSignature(cart);
    if (signature === lastFetchedCartSignature) return;
    lastFetchedCartSignature = signature;

    const cartContext = {
      subtotal: cart.total_price / 100,
      itemCount: cart.items.reduce((acc, item) => acc + item.quantity, 0),
      lineCount: cart.items.length,
    };

    const products = cart.items.map(mapCartItemToContext);

    const res = await fetch(STRATEGY_BACKEND_URL + "/api/public/strategy/evaluate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Strategy-Api-Key": STRATEGY_API_KEY,
      },
      body: JSON.stringify({
        shopDomain: window.Shopify.shop,
        strategyId: STRATEGY_ID,
        context: {
          products,
          cartToken,
          cart: cartContext.itemCount > 0 ? cartContext : undefined,
          session: { currencyCode: window.Shopify.currency.active },
        },
      }),
    });
    replacedUpsells = await res.json();

    if (typeof window.upcartRefreshCart === "function") {
      window.upcartRefreshCart();
    }
  };

  window.upcartSubscribeCartUpdated(runStrategyEvaluation);

  const waitForUpcartCart = (timeoutMs = 10000) =>
    new Promise((resolve) => {
      const start = Date.now();
      const check = () => {
        if (window.upcartGetCart()) return resolve(true);
        if (Date.now() - start > timeoutMs) return resolve(false);
        setTimeout(check, 100);
      };
      check();
    });

  waitForUpcartCart().then((ready) => {
    if (ready) runStrategyEvaluation();
  });

  window.upcartModifyListOfUpsells = () => {
    if (!replacedUpsells || !Array.isArray(replacedUpsells.products)) return;
    try {
      return replacedUpsells.products.map(mapStrategyProductToProduct);
    } catch (err) {
      console.error("upcartModifyListOfUpsells mapping failed", err);
      return;
    }
  };
</script>
```

<Warning>
  Dein Strategy-API-Schlüssel autorisiert Aufrufe der Strategies deines Shops. Das obige Skript platziert ihn in clientseitigem Code, was der einzige praktikable Weg ist, die API aus dem Cart Drawer aufzurufen. Behandle den Schlüssel wie jedes andere öffentliche Storefront-Zugangsdatum und rotiere ihn in Aftersell unter **Settings → Product Strategy**, falls er jemals auf unbeabsichtigte Weise offengelegt wird.
</Warning>

***

<div id="what-the-strategy-sees">
  ## Was die Strategy sieht
</div>

Da dies aus dem Storefront-Warenkorb heraus läuft, ist der Kontext eine schlanke Teilmenge dessen, was auf den nativen Oberflächen von Aftersell verfügbar ist:

<div id="product-context">
  #### Produkt-Kontext
</div>

Die aktuell im Upcart-Warenkorb befindlichen Positionen werden als Eingabeprodukte gesendet. Trigger wie **product type**, **vendor**, **product handle**, **product title** und alle **product ID / variant ID**-Trigger werden anhand dieser Artikel ausgewertet.

<div id="cart-context">
  #### Warenkorb-Kontext
</div>

* **Subtotal** – Zwischensumme des Warenkorbs in den Haupteinheiten der Store-Währung (z. B. Dollar). Upcarts `total_price` ist in Untereinheiten (Cent) angegeben, daher teilt das Skript durch 100, um den Einheiten zu entsprechen, die der Rest der Strategies-API verwendet – und in denen deine `cart_subtotal`-Regeln verfasst sind.
* **Item count** – Gesamtmenge über alle Positionen.
* **Line count** – Anzahl der unterschiedlichen Positionen.

<div id="session-context">
  #### Session-Kontext
</div>

* **Währungscode** – wird aus `window.Shopify.currency.active` gelesen.

<Warning>
  **Kunden-Trigger und UTM-Trigger greifen nicht.** Das Standardskript sendet keine Kunden-Tags, Bestellanzahl, Standort- oder UTM-Parameter – Regeln, die diese Trigger verwenden, werden also nie ausgelöst. Verwende Produkt-, Warenkorb- und Währungs-Trigger oder einen Catch all, um sicherzustellen, dass immer etwas zurückgegeben wird.
</Warning>

***

<div id="what-happens-when-the-strategy-returns">
  ## Was passiert, wenn die Strategy ein Ergebnis liefert
</div>

Die von der Strategy zurückgegebenen Produkte **ersetzen** die Liste, die Upcart andernfalls im Upsells-Modul anzeigen würde, vollständig. Die vom Händler definierte Upsell-Liste wird für die Dauer dieses Warenkorbs überschrieben – sie wird nicht zusammengeführt.

Jedes Strategy-Produkt wird in das von Upcart erwartete Produktformat abgebildet (Varianten, Bilder, Optionen, Metafelder usw.), sodass es im Upsell-Block genau wie jedes andere Produkt gerendert wird.

***

<div id="when-no-product-is-returned">
  ## Wenn kein Produkt zurückgegeben wird
</div>

Wenn die Strategy keine Produkte zurückgibt, rendert das Upsells-Modul **leer** – es werden keine Upsells angezeigt.

Um das zu vermeiden, konfiguriere in der Strategy einen **Catch all**, sodass immer ein Fallback-Produkt zurückgegeben werden kann. Wie du einen Catch all einrichtest, erfährst du auf der Seite [Strategies erstellen](/de/aftersell/strategies_building_in_app).

***

<div id="re-evaluation-on-cart-changes">
  ## Neuauswertung bei Warenkorbänderungen
</div>

Anders als bei Checkout-Upsells wertet die Upcart-Implementierung die Strategy **bei jeder Warenkorbänderung neu aus** – wenn Artikel hinzugefügt, entfernt oder Mengen aktualisiert werden. Das Skript abonniert das `cartUpdated`-Event von Upcart, sendet den neuen Warenkorb an die Strategies-API und aktualisiert den Cart Drawer mit der neuen Upsell-Liste.

Eine Warenkorbsignatur-Prüfung überspringt redundante Aufrufe, wenn sich Positionen und Mengen nicht tatsächlich geändert haben. Aufeinanderfolgende Warenkorb-Events, die den Warenkorb nicht wesentlich verändern, treffen die API also nicht erneut.

***

<div id="tips-for-upcart-strategies">
  ## Tipps für Upcart-Strategies
</div>

* **Baue auf dem Warenkorb auf.** Warenkorbmerkmale und Produkt-Trigger sind hier die stärksten Signale, die du hast. Kundenhistorie und UTM-basiertes Targeting werden vom Standardskript nicht gesendet.
* **Nutze Catch all als Sicherheitsnetz.** Ohne Catch all zeigt das Upsells-Modul nichts an, wenn keine Regel greift.
* **Standardmäßig cache-freundlich.** Die Warenkorbsignatur-Absicherung verhindert erneute API-Aufrufe, wenn sich der Warenkorb nicht wesentlich geändert hat – gut für Käufer, die den Warenkorb öffnen und schließen, ohne ihn zu bearbeiten.
