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

# Carts

> Create carts, manage lines and addresses, calculate totals, and convert a cart into an order.

A cart holds the purchasable items a customer intends to buy, together with the addresses and shipping selection needed to calculate totals and, eventually, create an order.

## Overview

A cart belongs to a channel and a currency, optionally to an authenticated user and a customer, and holds a collection of lines, each pointing at a purchasable item (typically a product variant).

<Info>
  Cart totals (`subTotal`, `total`, `taxTotal`, and so on) are calculated at runtime by a pipeline and are not stored on the `carts` table. They are populated as public properties on the `Lunar\Core\Models\Cart` instance when `calculate()` runs, and are instances of `Lunar\Core\DataObjects\PriceValue`. Once a cart is converted to an order, the resulting totals are persisted on the order.
</Info>

## Cart model

```php theme={null}
Lunar\Core\Models\Cart
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| `id` | `bigIncrements` | Primary key |
| `public_id` | `ulid` | Publicly-exposable identifier |
| `user_id` | `foreignId` `nullable` | The authenticated user the cart belongs to |
| `merged_id` | `foreignId` `nullable` | References the cart this cart was merged into |
| `currency_id` | `foreignId` | |
| `channel_id` | `foreignId` | |
| `order_id` | `foreignId` `nullable` | References the order created from this cart |
| `coupon_code` | `string` `nullable` | |
| `completed_at` | `dateTime` `nullable` | |
| `meta` | `jsonb` `nullable` | |
| `created_at` | `timestamp` | |
| `updated_at` | `timestamp` | |
| `customer_id` | `foreignId` `nullable` | |
| `deleted_at` | `timestamp` `nullable` | |
| `tax_zone_id` | `foreignId` `nullable` | Overrides the resolved tax zone when set |
| `region_id` | `foreignId` `nullable` | |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `lines` | HasMany | `Lunar\Core\Models\CartLine` | Ordered by `id` |
| `currency` | BelongsTo | `Lunar\Core\Models\Currency` | |
| `channel` | BelongsTo | `Lunar\Core\Models\Channel` | |
| `region` | BelongsTo | `Lunar\Core\Models\Region` | |
| `user` | BelongsTo | The authenticatable model from auth config | |
| `customer` | BelongsTo | `Lunar\Core\Models\Customer` | |
| `taxZone` | BelongsTo | `Lunar\Core\Models\TaxZone` | |
| `addresses` | HasMany | `Lunar\Core\Models\CartAddress` | |
| `shippingAddress` | HasOne | `Lunar\Core\Models\CartAddress` | Where `type` is `shipping` |
| `billingAddress` | HasOne | `Lunar\Core\Models\CartAddress` | Where `type` is `billing` |
| `orders` | HasMany | `Lunar\Core\Models\Order` | |
| `draftOrder` | HasOne | `Lunar\Core\Models\Order` | The order tied to this cart that has not yet been placed |
| `completedOrder` | HasOne | `Lunar\Core\Models\Order` | The order tied to this cart that has been placed |
| `completedOrders` | HasMany | `Lunar\Core\Models\Order` | All placed orders tied to this cart |

### Scopes

| Scope | Description |
| :- | :- |
| `unmerged` | Carts that have not been merged into another cart (`merged_id` is null) |
| `active` | Carts with no orders, or whose orders have not been placed |

<Info>
  `Lunar\Core\Models\Cart` uses `SoftDeletes`. This is not a lifecycle state on the cart itself — it lets order-tied carts hang around for replay and audit rather than being permanently removed.
</Info>

## Cart line model

```php theme={null}
Lunar\Core\Models\CartLine
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| `id` | `bigIncrements` | Primary key |
| `public_id` | `ulid` | Publicly-exposable identifier |
| `cart_id` | `foreignId` | |
| `purchasable_type` | `string` | Morph type of the purchasable item |
| `purchasable_id` | `unsignedBigInteger` | Morph ID of the purchasable item |
| `quantity` | `unsignedInteger` | |
| `meta` | `jsonb` `nullable` | |
| `created_at` | `timestamp` | |
| `updated_at` | `timestamp` | |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `cart` | BelongsTo | `Lunar\Core\Models\Cart` | |
| `purchasable` | MorphTo | Various | The item being purchased, typically `Lunar\Core\Models\ProductVariant` |
| `taxClass` | HasOneThrough | `Lunar\Core\Models\TaxClass` | Reached through the purchasable |
| `discounts` | BelongsToMany | `Lunar\Core\Models\Discount` | Discounts applied to this line |

<Info>
  `purchasable_type` / `purchasable_id` on a cart line are always set — every cart line points at a real purchasable. This is unlike an order line, where the same morph is nullable for self-describing lines such as shipping or ad-hoc charges. A cart never produces a purchasable-less line.
</Info>

<Info>
  Don't confuse the `purchasable` morph relation with `Lunar\Core\Enums\SellingPolicy`, the `always` / `in_stock` / `in_stock_or_on_backorder` mode stored on `ProductVariant::$selling_policy` (renamed from `purchasable` in v2). The two are unrelated: the morph is *what* is in the line, the selling policy is *whether* the underlying variant can currently be sold, and it's what `canBeFulfilledAtQuantity()` checks when a line is added or updated (see [Adding and updating lines](#adding-and-updating-lines)).
</Info>

## Cart address model

```php theme={null}
Lunar\Core\Models\CartAddress
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| `id` | `bigIncrements` | Primary key |
| `public_id` | `ulid` | Publicly-exposable identifier |
| `cart_id` | `foreignId` | |
| `country_id` | `foreignId` `nullable` | |
| `title` | `string` `nullable` | |
| `first_name` | `string` `nullable` | |
| `last_name` | `string` `nullable` | |
| `company_name` | `string` `nullable` | |
| `line_one` | `string` `nullable` | |
| `line_two` | `string` `nullable` | |
| `line_three` | `string` `nullable` | |
| `city` | `string` `nullable` | |
| `state` | `string` `nullable` | |
| `postcode` | `string` `nullable` | |
| `delivery_instructions` | `string` `nullable` | |
| `contact_email` | `string` `nullable` | |
| `contact_phone` | `string` `nullable` | |
| `type` | `string` | Either `shipping` or `billing` |
| `shipping_option` | `string` `nullable` | The identifier of the selected shipping option |
| `meta` | `jsonb` `nullable` | |
| `created_at` | `timestamp` | |
| `updated_at` | `timestamp` | |
| `tax_identifier` | `string` `nullable` | |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `cart` | BelongsTo | `Lunar\Core\Models\Cart` | |
| `country` | BelongsTo | `Lunar\Core\Models\Country` | |

A cart holds at most one address per `type` — adding an address of a given type replaces any existing one of that type.

## Retrieving and using carts

`Lunar\Core\Facades\CartSession` manages the cart for the current visitor, backed by `Lunar\Core\Managers\CartSessionManager`. It is bound `scoped` in the container, so under Octane or a queue worker each request or job gets its own instance — see [Session-scoped, not a singleton](#session-scoped-not-a-singleton) below.

```php theme={null}
use Lunar\Core\Facades\CartSession;

// Get the current cart, or null if none is in the session.
$cart = CartSession::current();

// Get the current cart, creating one if none exists.
$cart = CartSession::manager();
```

<Info>
  By default `CartSession::current()` does not create a cart. Set `lunar.cart_session.auto_create` to `true` to have it create one automatically, or call `CartSession::manager()`, which always returns a cart.
</Info>

Configuration lives in `packages/core/config/cart_session.php`:

| Key | Description |
| :- | :- |
| `session_key` | The session key used to store the current cart's ID (default `lunar_cart`) |
| `auto_create` | Whether `current()` creates a cart automatically when none exists (default `false`) |
| `allow_multiple_orders_per_cart` | Whether a cart can be reused after it already has a completed order (default `false`) |
| `delete_on_forget` | Whether the cart is deleted when `CartSession::forget()` is called (default `true`) |

When no cart ID is in the session and the visitor is authenticated, `CartSession` looks up the user's latest unmerged, active cart before creating a new one, so a returning logged-in user picks their existing cart back up.

### Creating a cart directly

Carts can also be created directly, bypassing the session, which is useful for APIs or background jobs:

```php theme={null}
use Lunar\Core\Models\Cart;

$cart = Cart::create([
    'currency_id' => $currency->id,
    'channel_id' => $channel->id,
]);
```

### Setting the channel and currency

```php theme={null}
use Lunar\Core\Facades\CartSession;

CartSession::setChannel($channel);
CartSession::setCurrency($currency);
```

Setting the currency on an existing cart updates `currency_id` on the cart and drops its loaded `currency` and `lines` relations, so the next `calculate()` prices in the new currency.

### Associating a user

```php theme={null}
CartSession::associate($cart, $user, policy: 'merge');
```

`$policy` is either `merge` (the default) or `override`. On `merge`, the user's existing active cart is merged into the given cart via `MergeCart`; matching lines (same purchasable and meta) have their quantities combined, others are copied across. On `override`, the user's existing cart is marked as merged into itself and abandoned in favor of the given cart. This mirrors `lunar.cart.auth_policy`, applied automatically by `CartSessionAuthListener` when a user logs in.

### Forgetting the session cart

```php theme={null}
CartSession::forget();
```

Removes the cart ID from the session. Deletes the cart itself unless `$delete` is passed as `false` or `lunar.cart_session.delete_on_forget` is `false`.

## Adding and updating lines

Cart lines are also managed through verb methods on the `Cart` model itself, which is what `CartSession` delegates to.

```php theme={null}
use Lunar\Core\Models\Cart;

/** @var Cart $cart */
$cart->add($productVariant, quantity: 2, meta: ['gift_wrap' => true]);
```

`add()` runs the configured `lunar.cart.validators.add_to_cart` validators (quantity, stock, and channel/customer-group availability by default) before adding or incrementing the line, then refreshes and recalculates the cart. If a line for the same purchasable and meta already exists, its quantity is incremented rather than a new line being created.

```php theme={null}
// Update a line's quantity (and optionally its meta).
$cart->updateLine(cartLineId: $line->id, quantity: 3);

// Remove a line.
$cart->remove($line->id);

// Add or update several lines in a single transaction.
$cart->addLines([
    ['purchasable' => $variantA, 'quantity' => 1],
    ['purchasable' => $variantB, 'quantity' => 2, 'meta' => ['engraving' => 'Happy Birthday']],
]);

$cart->updateLines(collect([
    ['id' => $lineA->id, 'quantity' => 5],
    ['id' => $lineB->id, 'quantity' => 1, 'meta' => ['note' => 'Fragile']],
]));

// Remove every line.
$cart->clear();
```

Stock validation calls `$purchasable->canBeFulfilledAtQuantity($quantity)`, which for a `Lunar\Core\Models\ProductVariant` reads its `Lunar\Core\Enums\SellingPolicy` (`Always`, `InStock`, `InStockOrOnBackorder`) — a variant with policy `Always` is always fulfillable regardless of stock.

## Addresses and tax zone

```php theme={null}
$cart->setShippingAddress($address);
$cart->setBillingAddress($address);
```

`$address` accepts either an array of address fields or a model implementing `Lunar\Core\Contracts\Addressable`. Setting an address replaces any existing address of the same type on the cart. Setting the shipping address clears any manually-set `tax_zone_id` by default (pass `$clearTaxZone = false` to keep it), so tax resolution falls back to the address-derived zone.

```php theme={null}
// Override the zone tax is calculated against, or clear it.
$cart->setTaxZone($taxZone);
$cart->setTaxZone(null);
```

## Shipping

```php theme={null}
use Lunar\Core\DataTypes\ShippingOption;

$option = $cart->getEstimatedShipping($params);     // cheapest non-collection option, for display
$cart->setShippingOption($option);                  // persist the chosen option on the shipping address
$current = $cart->getShippingOption();               // the option currently applied to the calculated cart
```

Shipping options are resolved through `Lunar\Core\Facades\ShippingManifest` — see [Extending Shipping](/2.x/extending/shipping) for how carriers register options. `CartSession::getShippingOptions()` returns the options available for the session's current cart.

## Discounts

Coupon codes and cart-level discounts are applied during calculation, not through a dedicated verb method — set `coupon_code` on the cart and recalculate:

```php theme={null}
$cart->update(['coupon_code' => 'SAVE10']);
$cart->recalculate();
```

See [Discounts](/2.x/reference/discounts) for how discounts are matched and applied. After calculation, `$cart->discounts`, `$cart->discountTotal`, `$cart->discountBreakdown`, `$cart->promotions`, and `$cart->freeItems` describe what was applied.

## Calculating totals

```php theme={null}
$cart->calculate();      // calculate once; skips if already calculated
$cart->recalculate();    // force recalculation
$cart->isCalculated();   // whether the cart and every line already carry a total
```

`calculate()` runs the pipeline configured at `lunar.cart.pipelines.cart`, in order:

1. `Lunar\Core\Pipelines\Cart\CalculateLines` — runs the `lunar.cart.pipelines.cart_lines` pipeline (`GetUnitPrice`) over each line, and any registered `Lunar\Core\Modifiers\CartLineModifier` classes
2. `Lunar\Core\Pipelines\Cart\ApplyShipping` — resolves and applies the shipping option
3. `Lunar\Core\Pipelines\Cart\CalculateShippingSubTotal`
4. `Lunar\Core\Pipelines\Cart\ApplyDiscounts` — matches and applies eligible discounts
5. `Lunar\Core\Pipelines\Cart\CalculateTax`
6. `Lunar\Core\Pipelines\Cart\Calculate` — sums line totals, discounts, and shipping into the cart-level totals

Each stage populates public properties on the cart (and its lines) as `Lunar\Core\DataObjects\PriceValue` instances:

| Property | Description |
| :- | :- |
| `subTotal` | Sum of cart line subtotals, before tax, shipping, and cart-level discounts |
| `subTotalDiscounted` | Subtotal after line-level discounts |
| `shippingSubTotal` | Shipping cost before tax |
| `shippingTaxTotal` | Tax on the shipping cost |
| `shippingTotal` | Shipping cost including tax |
| `taxTotal` | Sum of all tax across lines and shipping |
| `discountTotal` | Sum of all line-level and cart-level discounts |
| `discountBreakdown` | `Collection<Lunar\Core\ValueObjects\Cart\DiscountBreakdown>` |
| `taxBreakdown` | `Lunar\Core\ValueObjects\Cart\TaxBreakdown` |
| `shippingBreakdown` | `Lunar\Core\ValueObjects\Cart\ShippingBreakdown` |
| `promotions` | `Collection<Lunar\Core\ValueObjects\Cart\Promotion>` |
| `freeItems` | `Collection<Lunar\Core\ValueObjects\Cart\FreeItem>` |
| `total` | Line totals plus shipping, minus cart-level discounts |

A `PriceValue` exposes `->value` (the integer minor-unit amount), `->decimal()`, and `->format()`, resolved against the cart's currency. It also supports `add()`, `subtract()`, `multiply()`, and `clampToZero()` for combining amounts in custom pipeline stages.

<Info>
  Custom calculation logic is added as a `Lunar\Core\Modifiers\CartModifier` (cart-level) or `Lunar\Core\Modifiers\CartLineModifier` (line-level), each exposing `calculating()` / `calculated()` hooks (and, for line modifiers, `subtotalled()`). See [Extending Carts](/2.x/extending/carts).
</Info>

## Converting a cart to an order

```php theme={null}
$order = $cart->createOrder();
```

`createOrder()` recalculates the cart, runs the `lunar.cart.validators.order_create` validators (`ValidateCartForOrderCreation` by default — quantity, stock, and address/shipping completeness), then delegates to the `CreatesOrder` action contract to create or update the draft order tied to the cart, mark any used discounts as consumed, and return the resulting `Lunar\Core\Models\Order`.

```php theme={null}
$cart->canCreateOrder();   // true/false, without throwing
```

`CartSession::createOrder()` is the session-aware equivalent — it creates the order from the session's current cart and, by default, calls `forget()` afterwards.

A cart's `fingerprint()` produces a hash of its lines, user, currency, and coupon code, used to detect whether a cart has changed since a price or order was last calculated for it.

## Pruning carts

```bash theme={null}
php artisan lunar:prune:carts
```

`Lunar\Core\Console\Commands\PruneCarts` deletes carts (and their lines and addresses) matched by the `lunar.cart.prune_tables.pipelines` pipeline. By default this excludes carts with orders and carts merged into another cart, and only targets carts older than `lunar.cart.prune_tables.prune_interval` days (default 90). Pruning is disabled by default (`lunar.cart.prune_tables.enabled`); schedule the command once enabled.

## Session-scoped, not a singleton

`Lunar\Core\Contracts\CartSession` is bound `scoped` in the container, not `singleton` — it memoizes the current cart for the life of one request or job. Under Octane or a queue worker, a `singleton` binding would persist across requests and leak one visitor's cart into the next; `scoped` is discarded when the request or job ends.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.