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

# Cart Transform Functions

> Modify cart line items, merge products, expand bundles

# Cart Transform Functions

Cart transform functions modify per-line prices and titles inside the cart —
volume pricing, bundle discounts, expand-on-fulfillment bundles, VIP-tier
prices, anything that needs to change what a customer sees on a **specific
line**.

The platform records each negative delta as its own `cartTransformDiscounts[]`
entry on the persisted order, so cart, checkout, order page, and email
receipts all render one line per transform op with the **title you supplied**.

<Note>
  **Bundles live here.** LaunchMyStore does not expose a dedicated
  `productBundleCreate` REST resource — bundles are authored as cart-transform
  `merge` ops (see [Bundle pricing](#bundle-pricing-merge) below). Apps that
  need fixed-price kits, mix-and-match sets, or expand-on-fulfillment bundles
  should ship a cart\_transform function and treat each merchant-configured
  bundle as configuration passed to that function. Each bundle is expressed as
  a `merge` op that collapses child lines into a single bundle total at cart
  time, leaving the underlying lines on the order for refund and reporting.
</Note>

## How It Works

```mermaid theme={null}
sequenceDiagram
    participant Cart
    participant Backend
    participant YourFunction

    Cart->>Backend: Verify cart / Place order
    Backend->>YourFunction: cart items
    YourFunction->>Backend: { operations: [...] }
    Backend->>Cart: Per-line prices + summed bundle discount
    Note over Cart: One row per op, with your title
```

Runs in **both** cart verification and order placement so the preview matches the
persisted order exactly. Don't fold the savings into `price` yourself — the
platform reports raw `price` plus a separate `cartTransformDiscount` line.

## Function Manifest

```json theme={null}
{
  "handle": "my-cart-app",
  "name": "Cart Transformer",
  "version": "1.0.0",
  "extensions": {
    "functions": [
      {
        "type": "cart_transform",
        "handle": "cart-rules",
        "title": "Cart Transformation Rules",
        "entrypoint": "dist/cart-transform.wasm"
      }
    ]
  }
}
```

## Input Schema

```typescript theme={null}
interface CartTransformInput {
  cart: {
    items: CartItem[];
    totalPrice: number;       // Subtotal in display currency
    itemCount: number;        // Sum of quantities
    currency: string;
  };
}

interface CartItem {
  id: string;                 // lineItemId — use as `lineId` in ops
  variantId: string;
  productId: string;
  title: string;
  quantity: number;
  price: number;              // Effective unit price (discountPrice ?? salePrice ?? price)
  originalPrice: number;      // Compare-at unit price
}
```

<Note>
  Prices are in **display currency units**, not minor units (cents).
  `price: 19.99` means ₹19.99 / \$19.99.
</Note>

## Output Schema

A cart transform function returns **operations**. Three op kinds are
supported: `update`, `merge`, and `expand`. Each op may carry a `title` that
becomes the customer-facing label on the resulting discount row.

```typescript theme={null}
interface CartTransformOutput {
  operations: CartOperation[];
}

type CartOperation =
  | {
      update: {
        lineId: string;       // CartItem.id
        price?: number;       // New unit price (display currency)
        title?: string;       // Override line title and the discount-row label
      };
    }
  | {
      merge: {
        childLineIds: string[]; // Lines to collapse
        price: number;          // New BUNDLE TOTAL (display currency, not per-unit)
        title?: string;         // Discount-row label
      };
    }
  | {
      expand: {
        lineId: string;
        expandedItems: Array<{
          price?: number;       // Unit price for expanded item (display currency)
          quantity: number;
        }>;
        title?: string;         // Discount-row label
      };
    };
```

<Warning>
  The platform does **not** support `add`, `remove`, or `split` ops in cart
  transforms. To add a free gift line, return a `merge` op that includes the
  gift as a child line, or use a `discount` function with
  `target: 'line_item'`. To prevent an item from being purchased, use an
  `order_validation` function.
</Warning>

## Operations

### `update` — change unit price / title for a line

The most common op. Use for VIP pricing, volume breaks, member-only prices.

```js theme={null}
{
  operations: [{
    update: {
      lineId: 'line-0',
      price: 19.99,
      title: 'T-Shirt (VIP price)'
    }
  }]
}
```

* `price` is the **new unit price**. Per-line savings = `(oldUnit - newUnit) * quantity`.
* `title` (optional) overrides the line title **and** the discount-row label.
  If omitted, the label falls back to `"Bundle Discount"`.

### `merge` — collapse N lines into one bundle total

Use for "buy these three together for \$X" bundles. The child lines stay in
the order (for refund/reporting), but the bundle savings get a single
discount row.

```js theme={null}
{
  operations: [{
    merge: {
      childLineIds: ['line-0', 'line-1', 'line-2'],
      price: 89.99,           // BUNDLE TOTAL, not per-child
      title: 'Complete Outfit Bundle (25% off)'
    }
  }]
}
```

* `price` is the total for the bundle — savings =
  `sum(oldLineTotals) - price`.
* The savings appear as a single `cartTransformDiscounts[]` entry with your
  `title` as the label.

### `expand` — replace one line with multiple

Use for mystery boxes, multi-pack splits, or any case where one purchased
line needs to render as several line items.

```js theme={null}
{
  operations: [{
    expand: {
      lineId: 'line-mysterybox',
      expandedItems: [
        { price: 0, quantity: 1 },
        { price: 0, quantity: 1 },
        { price: 0, quantity: 1 }
      ],
      title: 'Mystery Box Reveal (-$45.00 off)'
    }
  }]
}
```

* Each `expandedItems[i]` may set its own `price` (defaults to the original
  line's unit price).
* Total savings = `originalLineTotal - sum(expandedItems[i].price * expandedItems[i].quantity)`.

## Example Implementations

### Volume / quantity-break pricing

```js theme={null}
function transformCart(input) {
  const ops = [];
  const breaks = {
    'prod_tshirt': [
      { minQty: 12, priceEach: 17.99 },
      { minQty: 6,  priceEach: 19.99 },
      { minQty: 3,  priceEach: 21.99 }
    ]
  };

  for (const item of input.cart.items) {
    const tiers = breaks[item.productId];
    if (!tiers) continue;
    const tier = tiers.find(t => item.quantity >= t.minQty);
    if (!tier || tier.priceEach >= item.price) continue;

    ops.push({
      update: {
        lineId: item.id,
        price: tier.priceEach,
        title: `${item.title} (Bulk x${item.quantity} — $${tier.priceEach} each)`
      }
    });
  }
  return { operations: ops };
}
```

### Bundle pricing (merge)

```js theme={null}
function transformCart(input) {
  const required = ['prod_cleanser', 'prod_toner', 'prod_moisturizer'];
  const matching = input.cart.items.filter(i => required.includes(i.productId));

  const hasAll = required.every(pid =>
    matching.some(i => i.productId === pid)
  );
  if (!hasAll) return { operations: [] };

  const originalTotal = matching.reduce(
    (sum, i) => sum + (i.price * i.quantity), 0
  );
  const bundlePrice = +(originalTotal * 0.80).toFixed(2);  // 20% off

  return {
    operations: [{
      merge: {
        childLineIds: matching.map(i => i.id),
        price: bundlePrice,
        title: 'Complete Skincare Set (20% off bundle)'
      }
    }]
  };
}
```

### Mystery box (expand)

```js theme={null}
function transformCart(input) {
  const ops = [];
  for (const item of input.cart.items) {
    if (item.productId !== 'prod_mystery_box') continue;
    ops.push({
      expand: {
        lineId: item.id,
        expandedItems: [
          { price: 0, quantity: 1 },
          { price: 0, quantity: 1 },
          { price: 0, quantity: 1 }
        ],
        title: `Mystery Box (3 items revealed)`
      }
    });
  }
  return { operations: ops };
}
```

## How the backend uses your output

For every op that yields a **negative** delta (lower price than original),
the platform:

1. Mutates the per-line price/title on the order so receipts and refunds
   reflect the bundle.

2. Appends an entry to `additionalFields.cartTransformDiscounts[]`:

   ```json theme={null}
   {
     "appId": "my-cart-app",
     "kind": "update",
     "title": "T-Shirt (Bulk x6 — $19.99 each)",
     "amount": 30.00
   }
   ```

3. Sums all op deltas into the scalar `additionalFields.cartTransformDiscount`
   for backwards compatibility.

4. Returns both shapes from cart verification.

A **positive** delta (a price increase) is allowed but is not recorded as a
discount entry; the per-line price is still mutated.

## How it renders in the UI

The customer-facing summary renders one row per op, using the `title` you
supplied:

```
Subtotal                                        $225.00
Complete Outfit Bundle (25% off)                -$30.00
Mystery Box Reveal (-$45.00 off)                -$45.00
─────────────────────────────────────────────────────
Total                                           $150.00
```

Legacy orders (placed before per-entry storage) fall back to a single summed
row labeled "Bundle Discount" using the scalar
`additionalFields.cartTransformDiscount`.

## Best Practices

<AccordionGroup>
  <Accordion title="Always set a `title`">
    The title becomes the discount-row label. Without it, the customer sees
    a generic "Bundle Discount" row — fine for prototypes, vague in
    production.
  </Accordion>

  <Accordion title="Make ops idempotent">
    `update` ops re-run on every cart change. Always derive the new price
    from `originalPrice` (or the static config), not from the current
    `price`, so re-running doesn't compound discounts.
  </Accordion>

  <Accordion title="Don't fold savings into `price`">
    Return the new unit price; the platform handles the bag total +
    separate discount row. If your function discounts and rewrites
    `payload.price` itself, the line total ends up doubly discounted.
  </Accordion>

  <Accordion title="Keep `merge` totals authoritative">
    `merge.price` is the **bundle total**, not per-child. The platform
    subtracts the sum of child line totals from this number.
  </Accordion>

  <Accordion title="Test against the real checkout">
    Cart transforms run at both cart verification and order placement —
    they must produce identical totals on both sides. Compare the cart
    preview against the placed order's totals after every change.
  </Accordion>
</AccordionGroup>

## See Also

* [Discount Functions](/functions/discount) — order-/line-/shipping-level
  discounts. Surfaces as `appDiscount`.
* [Order Validation Functions](/functions/order-validation) — block order
  placement, e.g. when a bundle is incomplete at checkout.
