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