> ## 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

> Gebruik een Strategy om dynamisch de producten te kiezen die worden getoond in de Upsells-module van Upcart.

<div id="overview">
  ## Overzicht
</div>

Upcart is een aparte app naast Aftersell, dus Strategies zijn niet in de Upsells-module ingebouwd zoals in de post-purchase en checkout flows van Aftersell. In plaats daarvan verbind je de twee apps met een klein script dat de Strategies API rechtstreeks aanroept en het resultaat via de publieke API van Upcart doorgeeft aan de bestaande Upsells-module van Upcart.

Het script is **drop-in** - plak het één keer in de custom HTML van Upcart, vervang twee waarden (je Strategy API-sleutel en de Strategy-ID), en de Upsells-module begint de producten te tonen die de Strategy retourneert.

***

<div id="what-youll-need">
  ## Wat je nodig hebt
</div>

1. **Je Strategy API-sleutel.** Ga in Aftersell naar **Settings → Product Strategy** en kopieer in de kaart **Security Token** je token (dit is je Strategy API-sleutel).
2. **De Strategy-ID.** Open de Strategy die je wilt uitvoeren in de Aftersell Strategy-editor en kopieer de ID.
3. **De Upsells-module ingeschakeld in Upcart.** Het script overschrijft de lijst met producten die in het bestaande upsellblok worden getoond, dus de module moet zijn ingeschakeld om iets weer te geven.

***

<div id="adding-the-script">
  ## Het script toevoegen
</div>

Ga in Upcart naar **Settings → Custom HTML → Scripts (before load)** en plak het onderstaande script. Vervang `STRATEGY_ID` en `STRATEGY_API_KEY` door de waarden uit Aftersell en sla vervolgens op.

```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>
  Je Strategy API-sleutel autoriseert aanroepen naar de Strategies van je winkel. Het bovenstaande script plaatst de sleutel in client-side code, wat de enige praktische manier is om de API vanuit de cart drawer aan te roepen. Behandel de sleutel zoals je elke andere publieke storefront-credential zou behandelen en roteer deze via Aftersell **Settings → Product Strategy** als de sleutel ooit op een onbedoelde manier wordt blootgesteld.
</Warning>

***

<div id="what-the-strategy-sees">
  ## Wat de Strategy ziet
</div>

Omdat dit vanuit de storefront-winkelwagen wordt uitgevoerd, is de context een beperkte subset van wat er beschikbaar is op de eigen surfaces van Aftersell:

<div id="product-context">
  #### Productcontext
</div>

De regelitems die momenteel in de Upcart-winkelwagen zitten, worden als inputproducten meegestuurd. Triggers zoals **producttype**, **leverancier**, **producthandle**, **producttitel** en alle **product-ID / variant-ID** triggers worden allemaal geëvalueerd op basis van deze items.

<div id="cart-context">
  #### Cartcontext
</div>

* **Subtotaal** - het subtotaal van de winkelwagen in de hoofdvaluta-eenheden van de winkel (bijv. dollars). Upcarts `total_price` is in kleinere eenheden (centen), dus het script deelt door 100 om overeen te komen met de eenheden die de rest van de Strategies API gebruikt - en de eenheden waarin je `cart_subtotal`-regels zijn opgesteld.
* **Aantal items** - totale hoeveelheid over alle regels.
* **Aantal regels** - aantal verschillende regelitems.

<div id="session-context">
  #### Sessiecontext
</div>

* **Valutacode** - overgenomen uit `window.Shopify.currency.active`.

<Warning>
  **Klanttriggers en UTM-triggers matchen niet.** Het standaardscript stuurt geen klanttags, aantal bestellingen, locatie of UTM-parameters mee - dus elke regel die deze triggers gebruikt, wordt nooit geactiveerd. Gebruik product-, cart- en valutatriggers, of een Catch all, om ervoor te zorgen dat er altijd iets wordt geretourneerd.
</Warning>

***

<div id="what-happens-when-the-strategy-returns">
  ## Wat er gebeurt wanneer de Strategy een resultaat geeft
</div>

De producten die de Strategy retourneert, **vervangen** volledig de lijst die Upcart anders in de Upsells-module zou tonen. De door de merchant gedefinieerde upselllijst wordt voor de duur van die winkelwagen overschreven - er wordt niet mee samengevoegd.

Elk Strategy-product wordt gemapt naar de productvorm die Upcart verwacht (varianten, afbeeldingen, opties, metavelden, enz.), zodat het in het upsellblok precies zoals elk ander product wordt weergegeven.

***

<div id="when-no-product-is-returned">
  ## Wanneer er geen product wordt geretourneerd
</div>

Als de Strategy geen producten retourneert, toont de Upsells-module **niets** - er worden geen upsells weergegeven.

Om dit te voorkomen, configureer je een **Catch all** in de Strategy, zodat er altijd een terugvalproduct is om te retourneren. Zie de pagina [Strategies bouwen](/nl/aftersell/strategies_building_in_app) voor het instellen van een Catch all.

***

<div id="re-evaluation-on-cart-changes">
  ## Herevaluatie bij wijzigingen in de winkelwagen
</div>

In tegenstelling tot checkout-upsells **evalueert de Upcart-implementatie de Strategy opnieuw telkens wanneer de winkelwagen verandert** - items toegevoegd, verwijderd of in hoeveelheid gewijzigd. Het script abonneert zich op het `cartUpdated`-event van Upcart, stuurt de nieuwe winkelwagen naar de Strategies API en vernieuwt de cart drawer met de nieuwe upselllijst.

Een controle op de cart-signature slaat overbodige aanroepen over als de regelitems en hoeveelheden niet daadwerkelijk zijn veranderd, zodat opeenvolgende cart-events die de winkelwagen niet wezenlijk wijzigen de API niet opnieuw aanroepen.

***

<div id="tips-for-upcart-strategies">
  ## Tips voor Upcart-Strategies
</div>

* **Ontwerp rond de winkelwagen.** Triggers op basis van de vorm van de winkelwagen en producten zijn hier de sterkste signalen. Klantgeschiedenis en op UTM gebaseerde targeting worden niet door het standaardscript meegestuurd.
* **Gebruik Catch all als vangnet.** Zonder Catch all toont de Upsells-module niets wanneer geen enkele regel matcht.
* **Standaard cachevriendelijk.** De cart-signature-beveiliging voorkomt dat de API opnieuw wordt aangeroepen als de winkelwagen niet wezenlijk is veranderd - handig voor shoppers die de winkelwagen openen en sluiten zonder deze te bewerken.
