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

# Selling Plans

> Recurring purchase / subscription data on products in Aqua

# Selling Plans

Selling plans expose subscription / recurring-purchase data on a product
in a standard, theme-friendly shape. Themes that render a
"Subscribe & save" selector or display per-delivery pricing pick up the
data automatically on LaunchMyStore — no theme changes required versus a
Liquid-compatible theme.

The data is hydrated from the backend at render time onto the
`product.selling_plan_groups` and `variant.selling_plan_allocations`
drops. The selected plan id flows through the cart, checkout, and order
placement to be persisted on the resulting order line.

<Warning>
  **MVP status — renewals are dry-run.** The full read-path is live:
  themes see the groups, the PDP selector renders, customers can pick a
  plan at checkout, and the order is recorded with the selected
  `selling_plan_id`. **What is not yet live** is automatic charge of
  subsequent renewals — the renewal-discovery cron runs nightly but
  **only logs** what *would* be charged. Payment-capture wiring is the
  next planned milestone — see [Renewals](#renewals-mvp-behaviour) below.
</Warning>

## How it flows end-to-end

```mermaid theme={null}
sequenceDiagram
    participant PDP as Theme PDP
    participant Cart
    participant Checkout
    participant Platform

    PDP->>PDP: Read product.selling_plan_groups
    PDP->>Cart: Add line w/ selling_plan_id
    Cart->>Checkout: Pass selected_selling_plan_id per line
    Checkout->>Platform: Place order
    Platform->>Platform: Persist the selling plan on each order line
    Note over Platform: A scheduled job tracks due renewals<br/>for each subscription line.
```

## Schema

`product.selling_plan_groups` is an array of groups. Each group has one
or more selling plans. Each plan describes a recurring schedule and
checkout charge.

```json theme={null}
[
  {
    "id": "spg-prod_8a7f...",
    "name": "Subscribe and save",
    "app_id": null,
    "options": [
      {
        "name": "Delivery every",
        "position": 1,
        "values": ["1 Month"]
      }
    ],
    "selling_plans": [
      {
        "id": "sp-prod_8a7f...",
        "name": "Delivery every 1 Month",
        "description": "",
        "recurring_deliveries": true,
        "selected": false,
        "billing_policy": { "interval": "month", "interval_count": 1 },
        "delivery_policy": { "interval": "month", "interval_count": 1 },
        "deliveries_per_cycle": 1,
        "options": [
          {
            "name": "Delivery every",
            "position": 1,
            "value": "1 Month"
          }
        ],
        "price_adjustments": [],
        "checkout_charge": {
          "value": 100,
          "value_type": "percentage"
        }
      }
    ],
    "selling_plan_selected": false
  }
]
```

On each variant, `variant.selling_plan_allocations` carries the
per-variant resolved pricing for each plan in the groups above:

```json theme={null}
[
  {
    "selling_plan": { "id": "sp-prod_8a7f...", "name": "Delivery every 1 Month" },
    "selling_plan_group_id": "spg-prod_8a7f...",
    "price": "24.00",
    "compare_at_price": "24.00",
    "per_delivery_price": "24.00",
    "checkout_charge_amount": "24.00",
    "remaining_balance_charge_amount": 0,
    "price_adjustments": []
  }
]
```

The split between `selling_plan_groups` (definition) and
`selling_plan_allocations` (per-variant pricing) follows the canonical
Liquid selling-plan model — group-level definitions plus per-variant
resolved pricing — so theme code that reads either works as expected.

## Delivery policies (deliver daily, bill weekly)

A plan can deliver **more often than it bills** — the classic prepaid
pattern: a bakery box delivered every morning, charged once a week.
This is modelled with two policies on the plan:

* `billing_policy` — how often the customer is **charged**
  (e.g. every 7 days).
* `delivery_policy` — how often goods are **delivered**
  (e.g. every 1 day).

The platform enforces two rules when the merchant saves the plan: both
policies must use the **same interval unit**, and the billing interval
count must be a **multiple** of the delivery interval count, so one
charge always covers a whole number of deliveries. The quotient is
exposed as `deliveries_per_cycle`.

```json theme={null}
{
  "id": "sp-...",
  "name": "Daily loaf — billed weekly",
  "billing_policy": { "interval": "day", "interval_count": 7 },
  "delivery_policy": { "interval": "day", "interval_count": 1 },
  "deliveries_per_cycle": 7
}
```

Pricing follows automatically — the allocation's `price` is the amount
charged **per billing cycle** (per-delivery price × deliveries per
cycle, after any plan discount), while `per_delivery_price` stays the
single-delivery amount:

```json theme={null}
{
  "selling_plan": { "id": "sp-...", "name": "Daily loaf — billed weekly" },
  "price": "1190.70",
  "compare_at_price": "1323.00",
  "per_delivery_price": "170.10"
}
```

The canonical PDP rendering for such a plan:

```aqua theme={null}
{% for allocation in current_variant.selling_plan_allocations %}
  {% if allocation.selling_plan.id == plan.id %}
    {% if plan.deliveries_per_cycle > 1 %}
      <strong>{{ allocation.per_delivery_price | money }}</strong> per delivery
      &middot; billed {{ allocation.price | money }}
      every {{ plan.billing_policy.interval_count }} {{ plan.billing_policy.interval }}s
    {% else %}
      <strong>{{ allocation.per_delivery_price | money }}</strong> / delivery
    {% endif %}
  {% endif %}
{% endfor %}
```

Cart lines and checkout totals charge the per-cycle amount; the line's
`selling_plan_allocation.per_delivery_price` divides it back down for
display. Recurring billing (Stripe) charges the same per-cycle amount on
the billing cadence.

## Theme usage

The canonical pattern: gate the selector on
`product.selling_plan_groups.size > 0`, then offer the merchant's plans
alongside the one-time purchase option.

### Subscribe & save selector

```aqua theme={null}
<div class="purchase-options">
  {% if product.selling_plan_groups.size > 0 %}
    <label class="purchase-option">
      <input type="radio" name="purchase_type" value="one_time" checked />
      <span>One-time purchase</span>
      <strong>{{ product.selected_or_first_available_variant.price | money }}</strong>
    </label>

    {% for group in product.selling_plan_groups %}
      <fieldset class="selling-plan-group">
        <legend>{{ group.name }}</legend>

        {% for plan in group.selling_plans %}
          <label class="purchase-option">
            <input
              type="radio"
              name="selling_plan"
              value="{{ plan.id }}"
              data-plan-id="{{ plan.id }}"
            />
            <span>{{ plan.name }}</span>

            {% comment %}
              Look up the allocation for the currently-selected variant
              so the customer sees the actual price they'll pay per
              delivery.
            {% endcomment %}
            {% assign current_variant = product.selected_or_first_available_variant %}
            {% for allocation in current_variant.selling_plan_allocations %}
              {% if allocation.selling_plan.id == plan.id %}
                <strong>{{ allocation.per_delivery_price | money }} / delivery</strong>
                {% if allocation.compare_at_price != allocation.price %}
                  <s>{{ allocation.compare_at_price | money }}</s>
                {% endif %}
              {% endif %}
            {% endfor %}

            {% if plan.description != blank %}
              <small class="plan-description">{{ plan.description }}</small>
            {% endif %}
          </label>
        {% endfor %}
      </fieldset>
    {% endfor %}
  {% endif %}
</div>

<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}" />
```

The `<input name="selling_plan">` radio value flows into the cart line
as `selected_selling_plan_id` — the Liquid form serializer reads it
automatically when the form is submitted.

### Per-delivery price for the selected variant

`variant.selling_plan_allocations` is the structured way to read the
price applied when a customer subscribes to a particular plan on that
variant.

```aqua theme={null}
{% assign current_variant = product.selected_or_first_available_variant %}

{% for allocation in current_variant.selling_plan_allocations %}
  <div class="plan-price" data-plan-id="{{ allocation.selling_plan.id }}">
    {{ allocation.selling_plan.name }}:
    <strong>{{ allocation.per_delivery_price | money }}</strong> per delivery
  </div>
{% endfor %}
```

### Showing total checkout charge

Some plans charge less than the full price at checkout (deferred
billing). Read `allocation.checkout_charge_amount` for what gets
charged today and `allocation.remaining_balance_charge_amount` for what
will be billed on subsequent renewals.

```aqua theme={null}
{% for allocation in variant.selling_plan_allocations %}
  <p class="plan-charge-breakdown" data-plan-id="{{ allocation.selling_plan.id }}">
    Charged today: <strong>{{ allocation.checkout_charge_amount | money }}</strong>
    {% if allocation.remaining_balance_charge_amount > 0 %}
      — Remaining {{ allocation.remaining_balance_charge_amount | money }} per renewal.
    {% endif %}
  </p>
{% endfor %}
```

### Detecting subscription-only products

A product that **requires** a selling plan (no one-time purchase
option) exposes `requires_selling_plan: true`. The MVP does not yet flip
this flag — every subscription-enabled product currently allows both
one-time and recurring purchase.

```aqua theme={null}
{% if product.requires_selling_plan %}
  <p class="notice">This product is sold by subscription only.</p>
{% endif %}
```

### Cart-line context

In cart and order templates, a line that has a selected selling plan
carries the plan reference at `line.selling_plan_allocation`:

```aqua theme={null}
{% for line in cart.items %}
  <div class="cart-line">
    {{ line.title }}
    {% if line.selling_plan_allocation %}
      <span class="plan-badge">
        {{ line.selling_plan_allocation.selling_plan.name }}
      </span>
      <small>{{ line.selling_plan_allocation.per_delivery_price | money }} / delivery</small>
    {% endif %}
  </div>
{% endfor %}
```

## Dynamic price calculation

There are two ways a plan can affect price, and themes should be
prepared for both:

### 1. Allocation override

The simplest case: the merchant sets a fixed per-delivery price for the
plan that's lower than the variant price. The platform pre-computes
this and surfaces it as `allocation.per_delivery_price`. **No theme
math** is required — render the allocation price directly.

### 2. Per-billing-cycle adjustments

Some plans charge a different amount on the first delivery vs.
subsequent renewals (e.g. "first month free", "\$10 off your second
delivery"). These are surfaced as `allocation.price_adjustments[]`:

```json theme={null}
{
  "price_adjustments": [
    { "order_count": 1, "adjustment_value": { "adjustment_percentage": 100 } },
    { "order_count": 2, "adjustment_value": { "adjustment_amount": 10 } }
  ]
}
```

Themes generally do **not** render the full adjustment ladder on the
PDP — show just the headline price and let the customer learn the
details on checkout. If you do want to surface them:

```aqua theme={null}
{% for allocation in variant.selling_plan_allocations %}
  {% if allocation.price_adjustments.size > 0 %}
    <ul class="plan-adjustments" data-plan-id="{{ allocation.selling_plan.id }}">
      {% for adj in allocation.price_adjustments %}
        <li>
          Delivery {{ adj.order_count }}:
          {% if adj.adjustment_value.adjustment_percentage %}
            {{ adj.adjustment_value.adjustment_percentage }}% off
          {% elsif adj.adjustment_value.adjustment_amount %}
            {{ adj.adjustment_value.adjustment_amount | money }} off
          {% endif %}
        </li>
      {% endfor %}
    </ul>
  {% endif %}
{% endfor %}
```

## Checkout integration

When the customer adds the product to cart with a selling plan
selected, the cart API persists the `selected_selling_plan_id` on the
line. From there the checkout receives the plan id alongside the
variant and quantity, and order placement writes the plan id onto the
resulting order line.

The checkout UI surfaces the plan name beside the line item the same as
any Liquid-rendered checkout — no theme-side wiring needed.

## Order-placement persistence

Once the order is placed, the platform records:

* The **selling plan group** definition (name, position, the products it
  is offered on).
* Each **selling plan** (name, billing and delivery cadence, recurring
  flag, trial days, checkout charge, category — `subscription`,
  `prepaid`, or `try_before_you_buy`).
* Any **per-cycle price adjustments**.
* The **selected plan on each order line** — the authoritative record of
  which plan was chosen, used to find renewals due.

## Field reference

### Selling plan group (`product.selling_plan_groups[]`)

| Field                   | Type           | Description                                                                      |
| ----------------------- | -------------- | -------------------------------------------------------------------------------- |
| `id`                    | string         | Unique group id. Format: `spg-{productId}` for MVP.                              |
| `name`                  | string         | Group label (e.g. `"Subscribe and save"`).                                       |
| `app_id`                | string \| null | App that registered the group, or `null` for merchant-set. Always `null` in MVP. |
| `options`               | array          | Array of option definitions — `name`, `position`, and possible `values`.         |
| `selling_plans`         | array          | Array of plans in this group.                                                    |
| `selling_plan_selected` | bool           | `true` if a plan in this group is the active selection.                          |

### Selling plan (`group.selling_plans[]`)

| Field                  | Type   | Description                                                                                                                            |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                   | string | Unique plan id.                                                                                                                        |
| `name`                 | string | Plan label.                                                                                                                            |
| `description`          | string | Optional plan description (used for trial info, etc.).                                                                                 |
| `recurring_deliveries` | bool   | `true` if this plan delivers on a schedule.                                                                                            |
| `selected`             | bool   | `true` if this plan is the active selection.                                                                                           |
| `billing_policy`       | object | How often the customer is charged (`{ interval, interval_count }`).                                                                    |
| `delivery_policy`      | object | How often goods are delivered (`{ interval, interval_count }`). Same as `billing_policy` unless the merchant set a delivery frequency. |
| `deliveries_per_cycle` | number | Deliveries covered by one charge (`billing ÷ delivery`). `1` for ordinary subscriptions.                                               |
| `options`              | array  | Array of option values selected for this plan.                                                                                         |
| `price_adjustments`    | array  | Discount adjustments applied to recurring deliveries.                                                                                  |
| `checkout_charge`      | object | How much is charged at checkout (`{ value, value_type }`).                                                                             |

### Variant allocation (`variant.selling_plan_allocations[]`)

| Field                             | Type   | Description                                                                                                                                                                                    |
| --------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `selling_plan`                    | object | Reference to the plan this allocation belongs to (`{ id, name }`).                                                                                                                             |
| `selling_plan_group_id`           | string | Parent group id.                                                                                                                                                                               |
| `price`                           | string | Amount charged per **billing cycle** for this variant (presentment currency). Equals one delivery for ordinary plans; per-delivery × `deliveries_per_cycle` for prepaid delivery-policy plans. |
| `compare_at_price`                | string | One-time price for the same number of deliveries — for the strikethrough.                                                                                                                      |
| `per_delivery_price`              | string | Price of a **single delivery**. Same as `price` unless the plan has a delivery policy.                                                                                                         |
| `checkout_charge_amount`          | string | Amount charged at checkout (subset of `price` if deferred).                                                                                                                                    |
| `remaining_balance_charge_amount` | number | Balance charged on the next renewal. `0` in MVP.                                                                                                                                               |
| `price_adjustments`               | array  | Plan-specific adjustments (`{ order_count, adjustment_value: { adjustment_percentage \| adjustment_amount } }`).                                                                               |

### Cart-line context

| Field                          | Type           | Description                                                                                                           |
| ------------------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------- |
| `line.selling_plan_allocation` | object \| null | Present on lines whose customer picked a plan at PDP. Carries the same shape as `variant.selling_plan_allocations[]`. |

## Renewals (MVP behaviour)

A renewal-discovery job runs daily. For each subscription order line
whose computed next billing date is on/before today and which does not
yet have a child renewal order, the job records the renewal that
*would* happen — it does **not** yet create a child order or capture
payment.

What the job currently does:

1. Finds recent order lines placed with a recurring selling plan whose
   parent order is not cancelled / abandoned / refunded.
2. Computes each line's next billing date from the plan's billing
   cadence.
3. Flags every line due on/before today that has not yet been renewed.

What it does **not** do yet:

* Charge the customer's saved payment method.
* Create a child order for the renewal.
* Decrement inventory.
* Send a renewal email.
* Advance the line's next billing date.

This is intentional — the discovery logic ships first so it can be
validated in production without risking double-charges. Phase 2 wires
payment capture (Stripe off-session) and order creation on top of the
same discovery pass.

### What works today

* Themes render the selector and customers can place orders with a
  `selling_plan_id` attached.
* The recurring schedule is recorded on the order line.
* The plan is visible on the order detail page (admin), the customer
  account order history, and via the storefront `order` drop.
* Subscription-management UI in the admin (cancel, skip, update
  cadence) reads the plan recorded on each order line.

### What doesn't work yet

* Subsequent deliveries are **not** automatically charged or shipped.
* No webhook fires on renewal due-date.
* Failed-payment retry / dunning is not wired.

Until the cron's payment-capture branch ships, merchants who launch
subscriptions need to charge renewals manually (or via a third-party
subscription app that bypasses this cron). This is the foundation Phase
2 will build on — once payment capture is wired into the cron, the same
theme code will start producing real renewal orders without any change
on the theme side.

## See also

* [Aqua Objects](/aqua/objects) — full reference for `product`, `variant`,
  and other globals.
* [Aqua Filters](/aqua/filters) — `money`, `default`, and other filters
  used in the examples above.
* [Metafields in Aqua](/aqua/metafields) — for storing app-specific data
  alongside selling plans (e.g. anchor day, prepaid count).
* [Order drop](/aqua/objects#order) — for reading the selected plan on
  past orders in the customer account.
