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

# Discount Functions

> Apply custom discount logic to orders

# Discount Functions

Discount functions apply custom discounts during checkout — VIP tiering,
loyalty rewards, dynamic promotions, free-shipping thresholds, and any rule
that goes beyond fixed-code discounts. Each discount your function returns is
persisted as its own line so the cart, checkout, order summary, and receipts
all render the **app-supplied label** next to its amount.

## How It Works

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

    Checkout->>Backend: Verify cart / Place order
    Backend->>YourFunction: cart + customer + discountCodes
    YourFunction->>Backend: { discounts: [...] }
    Backend->>Checkout: appDiscount + per-entry array
    Note over Checkout: One line per discount, with your message
```

The function runs at **both** cart verification (cart/checkout preview)
and order placement so the totals you see in the cart match
what gets persisted.

## Function Manifest

```json theme={null}
{
  "handle": "my-discounts-app",
  "name": "Smart Discounts",
  "version": "1.0.0",
  "extensions": {
    "functions": [
      {
        "type": "discount",
        "handle": "dynamic-discounts",
        "title": "Dynamic Discount Rules",
        "entrypoint": "dist/discount.wasm"
      }
    ]
  }
}
```

## Input Schema

```typescript theme={null}
interface DiscountInput {
  cart: {
    items: CartItem[];
    totalPrice: number;       // Subtotal in display currency (e.g. 199.99)
    itemCount: number;        // Sum of quantities
    currency: string;         // e.g. "INR", "USD"
  };
  customer?: {
    id: string;
    email: string;
  };
  shippingAddress: {
    address1: string;
    address2: string;
    city: string;
    province: string;          // State / province from the address form
    country: string;
    zip: string;               // Postal / ZIP code
  };
  discountCodes: string[];    // Customer-entered codes (couponId/code)
}

interface CartItem {
  id: string;                 // lineItemId — pass back in `lineIds` to scope a discount
  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). A
  `price: 19.99` means ₹19.99 / \$19.99 — no division required.
</Note>

<Note>
  `shippingAddress` fields are **empty strings** until the customer completes
  the address step at checkout — always handle the empty case. Once the address
  is entered, the function re-runs with the full destination (including `zip`),
  which enables location-aware logic such as ZIP/district-level sales-tax
  adjustments for US stores.
</Note>

## Output Schema

Your function returns an array of discount entries. Each entry has the same
shape — no separate `percentage` / `fixed_amount` types. The kind of discount
is set by `valueType`, and the scope is set by `target` + `targetSelection`.

```typescript theme={null}
interface DiscountOutput {
  discounts: DiscountEntry[];
}

interface DiscountEntry {
  valueType: 'fixed' | 'percentage';
  value: number;
  //  - fixed:      amount to subtract (display currency, e.g. 10 = ₹10 off)
  //  - percentage: 0–100 (e.g. 15 = 15% off)

  target: 'order' | 'line_item' | 'shipping';
  //  - order:     apply to the line-item subtotal
  //  - line_item: apply only to specific lines (requires lineIds)
  //  - shipping:  reduce shipping + zone shipping

  targetSelection?: 'all' | 'specific';
  //  Only meaningful when target='line_item'. Defaults to 'all'.

  lineIds?: string[];
  //  CartItem.id values when targetSelection='specific'.

  title: string;
  //  REQUIRED. The discount label shown to the customer in cart, checkout,
  //  order summary, and emails. Trimmed to 120 chars. An entry without a
  //  string `title` fails output validation and is silently dropped.

  message?: string;
  //  Optional. When present, overrides `title` as the displayed label.
  //  `title` is still required for the entry to validate.
}
```

<Warning>
  Every entry **must** include a string `title` — it's part of the output
  contract (`title`, `value`, `valueType`, `target`). An entry missing
  `title` fails validation and the whole function result is dropped (the
  cart still ships, just with no discount from your function). `message` is
  an optional label override, not a substitute for `title`.
</Warning>

### `value` semantics

| `valueType`  | `value` interpretation                       | Cap                          |
| ------------ | -------------------------------------------- | ---------------------------- |
| `fixed`      | Amount in display currency (e.g. `10` = ₹10) | Capped at the remaining base |
| `percentage` | Whole percent (e.g. `15` = 15%)              | Clamped to `[0, 100]`        |

### `target` semantics

| `target`    | Base reduced                                                                  |
| ----------- | ----------------------------------------------------------------------------- |
| `order`     | Full line-item subtotal                                                       |
| `line_item` | Subtotal of lines in `lineIds` (or full subtotal if `targetSelection: 'all'`) |
| `shipping`  | `shippingPrice + shippingZonePrice`                                           |

`target: 'order'` and `target: 'line_item'` both surface as `appDiscount`
(and on the persisted order, as an `appDiscounts[]` entry).
`target: 'shipping'` surfaces as `shippingDiscount` (and `shippingDiscounts[]`).

## Examples

### Percentage off the whole order

```js theme={null}
function calculateDiscounts(input) {
  return {
    discounts: [{
      valueType: 'percentage',
      value: 15,
      target: 'order',
      title: 'VIP: 15% off'
    }]
  };
}
```

### Fixed amount off

```js theme={null}
function calculateDiscounts(input) {
  return {
    discounts: [{
      valueType: 'fixed',
      value: 10,
      target: 'order',
      title: 'Loyalty reward: $10 off'
    }]
  };
}
```

### Discount on specific lines

```js theme={null}
function calculateDiscounts(input) {
  const saleItemIds = input.cart.items
    .filter(i => i.title.toLowerCase().includes('sale'))
    .map(i => i.id);

  if (saleItemIds.length === 0) return { discounts: [] };

  return {
    discounts: [{
      valueType: 'percentage',
      value: 30,
      target: 'line_item',
      targetSelection: 'specific',
      lineIds: saleItemIds,
      title: 'Sale items: 30% off'
    }]
  };
}
```

### Free shipping above a threshold

```js theme={null}
function calculateDiscounts(input, config) {
  const threshold = config.freeShippingThreshold || 100;
  if (input.cart.totalPrice < threshold) return { discounts: [] };

  return {
    discounts: [{
      valueType: 'percentage',
      value: 100,
      target: 'shipping',
      title: `Free shipping on orders over $${threshold}`
    }]
  };
}
```

### Tiered spending discount

```js theme={null}
function calculateDiscounts(input) {
  const tiers = [
    { threshold: 200, value: 20, label: 'Spend $200+, save 20%' },
    { threshold: 150, value: 15, label: 'Spend $150+, save 15%' },
    { threshold: 100, value: 10, label: 'Spend $100+, save 10%' }
  ];

  const tier = tiers.find(t => input.cart.totalPrice >= t.threshold);
  if (!tier) return { discounts: [] };

  return {
    discounts: [{
      valueType: 'percentage',
      value: tier.value,
      target: 'order',
      title: tier.label
    }]
  };
}
```

### Multiple discounts in one call

A function can return as many entries as it likes — each renders as a
separate row.

```js theme={null}
function calculateDiscounts(input) {
  const discounts = [];

  if (input.discountCodes.includes('WELCOME10')) {
    discounts.push({
      valueType: 'percentage',
      value: 10,
      target: 'order',
      title: 'WELCOME10: 10% off'
    });
  }

  if (input.cart.totalPrice >= 100) {
    discounts.push({
      valueType: 'percentage',
      value: 100,
      target: 'shipping',
      title: 'Free shipping over $100'
    });
  }

  return { discounts };
}
```

## How the backend uses your output

For every `DiscountEntry` your function returns, the platform:

1. **Computes `entryAmount`** in display currency, capped by the entry's base
   (subtotal, line-set subtotal, or shipping).

2. **Subtracts** `entryAmount` from `finalPrice` (and proportionally from
   `shippingPrice` / `shippingZonePrice` for shipping discounts).

3. **Appends an entry to `additionalFields.appDiscounts[]`** (or
   `shippingDiscounts[]`):

   ```json theme={null}
   {
     "appId": "discount-app-handle",
     "message": "VIP: 15% off",
     "amount": 22.50,
     "target": "order",
     "valueType": "percentage",
     "value": 15
   }
   ```

4. **Updates the summed scalars** `additionalFields.appDiscount` and
   `additionalFields.shippingDiscount` for backwards compatibility.

Both the per-entry arrays and the summed scalars are persisted on the
order and returned by cart verification so the cart UI, checkout UI, order
detail page, and order confirmation emails can all render the same data.

## How it renders in the UI

The customer-facing summary renders **one row per discount entry**, using the
exact `message` you supplied:

```
Subtotal                          $225.00
Sale items: 30% off               -$13.50
VIP: 15% off                      -$33.75
Free shipping over $100           -$8.00
─────────────────────────────────────────
Total                             $169.75
```

If a legacy order was placed before per-entry storage existed, the platform
falls back to a single summed row labeled "App Discount" / "Shipping
Discount" using the scalar `appDiscount` / `shippingDiscount`.

## Stacking and ordering

* Multiple discount apps run in install order; their outputs are concatenated.
* Each entry's base is **independent** — a percentage entry doesn't see
  earlier reductions. If you need compounding, do it in a single function.
* `target: 'shipping'` entries cap at the remaining shipping. Two free-
  shipping discounts won't refund shipping twice.
* `appDiscount` is surfaced **separately from `couponDiscount`** — coupons
  entered manually by the customer continue to apply in addition to your
  function output.

<Warning>
  Be careful with stacking — a 20% VIP + 30% sale + 10% code can run total
  discount to 60%. Either exclude sale items via `target: 'line_item'` with
  `targetSelection: 'specific'`, or short-circuit when \`discountCodes.length

  > 0\`.
</Warning>

## Best Practices

<AccordionGroup>
  <Accordion title="Use clear, customer-facing `title` text">
    The `title` (or `message`, if you set it) shows up in the cart,
    checkout, order page, and email receipt. Prefer "VIP: 15% off" over
    "Discount applied based on tag vip\_gold".
  </Accordion>

  <Accordion title="Return early on empty input">
    `if (!input.customer) return { discounts: [] }` keeps unauthenticated
    carts fast.
  </Accordion>

  <Accordion title="Keep `value` integer where possible">
    `value: 15` for 15% is easier to debug than `0.15`.
  </Accordion>

  <Accordion title="Test against the real checkout">
    The platform exposes cart-verification + order-placement parity — your
    function output must produce the same totals on both sides. Run the
    cart UI in a browser, not just the function's JSON output, before
    shipping.
  </Accordion>

  <Accordion title="Don't return zero-value entries">
    Entries with `entryAmount` capped to `0` are filtered out — they
    wouldn't render anyway.
  </Accordion>
</AccordionGroup>

## See Also

* [Cart Transform Functions](/functions/cart-transform) — per-line price
  adjustments, bundles, merge/expand. Surfaces as `cartTransformDiscount`.
* [Shipping Rate Functions](/functions/shipping-rate) — add new rates rather
  than discount an existing one.
* [Order Validation Functions](/functions/order-validation) — block order
  placement based on cart contents.
