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

# Cart

> Manage shopping carts with pipeline-based calculation, discount validation, tax integration, and order conversion.

The `shopper/cart` package handles everything between "Add to cart" and "Place order". It manages cart lines, validates stock, applies discounts, calculates taxes, and converts completed carts into orders. All through a clean, testable API.

Every monetary value is stored in **cents** as an unsigned integer. A product priced at \$25.00 is stored as `2500`.

## How It Works

The cart system has five main pieces:

1. **CartManager** handles cart operations: add, update, remove, apply coupons, calculate totals
2. **CartSessionManager** persists the current cart in the session for storefront use
3. **Pipeline system** calculates totals through a configurable chain of pipes (subtotals → discounts → taxes → total)
4. **DiscountValidator** enforces coupon rules: eligibility, usage limits, zones, minimum amounts
5. **CreateOrderFromCartAction** converts a cart into an order within a database transaction

## Cart Manager

The `CartManager` is the primary API for all cart operations. It is registered as a singleton and injected with the `CartPipelineRunner`.

```php theme={null}
use Shopper\Cart\CartManager;

$manager = resolve(CartManager::class);
```

### Adding Items

Pass any model that implements `Priceable`, typically a `Product` or `ProductVariant`. The manager looks up the unit price from the purchasable's price list using the cart's currency.

```php theme={null}
$line = $manager->add($cart, $product);

$line = $manager->add($cart, $product, quantity: 3);

$line = $manager->add($cart, $variant, metadata: [
    'gift_wrap' => true,
    'message' => 'Happy birthday!',
]);
```

Adding the same purchasable twice increments the existing line's quantity instead of creating a duplicate.

```php theme={null}
$manager->add($cart, $product, quantity: 2); // quantity: 2
$manager->add($cart, $product, quantity: 3); // quantity: 5
```

### Stock Validation

Every `add()` and `update()` call checks available stock. If the purchasable implements `Stockable` and `allow_backorder` is false, an `InsufficientStockException` is thrown when the requested quantity exceeds available inventory.

```php theme={null}
use Shopper\Cart\Exceptions\InsufficientStockException;

try {
    $manager->add($cart, $product, quantity: 100);
} catch (InsufficientStockException $e) {
    $e->purchasable; // The product or variant
    $e->available;   // Current stock level
    $e->requested;   // Quantity that was requested
}
```

Products with `allow_backorder` set to `true` bypass stock checks entirely.

### Updating a Line

```php theme={null}
$updated = $manager->update($cart, $lineId, [
    'quantity' => 5,
]);

$updated = $manager->update($cart, $lineId, [
    'metadata' => ['gift_wrap' => false],
]);
```

### Removing Lines

```php theme={null}
$manager->remove($cart, $lineId);

$manager->clear($cart);
```

### Applying Coupons

The `applyCoupon()` method validates that the discount code exists in the database before setting it on the cart. The actual discount calculation happens during the pipeline.

```php theme={null}
use Shopper\Cart\Exceptions\InvalidDiscountException;

try {
    $manager->applyCoupon($cart, 'SUMMER20');
} catch (InvalidDiscountException $e) {
    // Discount code not found
}

$manager->removeCoupon($cart);
```

Removing a coupon also deletes all line adjustments that were created by the discount pipeline.

### Adding Addresses

```php theme={null}
use Shopper\Core\Enum\AddressType;

$manager->addAddress($cart, AddressType::Shipping, [
    'first_name' => 'John',
    'last_name' => 'Doe',
    'address_1' => '123 Main St',
    'city' => 'New York',
    'postal_code' => '10001',
    'country_id' => $countryId,
]);

$manager->addAddress($cart, AddressType::Billing, $billingData);
```

If an address of the same type already exists, it is updated instead of duplicated.

### Calculating Totals

```php theme={null}
use Shopper\Cart\Pipelines\CartPipelineContext;

$context = $manager->calculate($cart);

$context->subtotal;      // Sum of line subtotals (before discounts/tax)
$context->discountTotal; // Total discount amount
$context->taxTotal;      // Total tax amount
$context->total;         // Final total
$context->taxInclusive;  // Whether tax is included in prices
```

### Completed Cart Guard

All mutating operations (`add`, `update`, `remove`, `clear`, `applyCoupon`, `removeCoupon`) throw `CartCompletedException` if the cart has already been completed. This prevents modifications after an order has been placed.

```php theme={null}
use Shopper\Cart\Exceptions\CartCompletedException;

$cart->update(['completed_at' => now()]);

$manager->add($cart, $product); // throws CartCompletedException
```

## Session Management

The `CartSessionManager` handles cart persistence in the HTTP session. Use it in your storefront to retrieve or create the current customer's cart.

### Using the Facade

```php theme={null}
use Shopper\Cart\Facades\Cart;

$cart = Cart::current();

$cart = Cart::create();

$cart = Cart::create(['channel_id' => $channelId, 'zone_id' => $zoneId]);

Cart::use($existingCart);

Cart::associate($user);

Cart::forget();
```

### Behavior

* `current()` returns `null` if no cart exists in the session and `auto_create` is `false`
* `current()` returns `null` if the session cart has been completed (it won't return stale carts)
* `create()` stores the new cart's ID in the session and sets the `currency_code` from `shopper_currency()`
* `associate()` sets the `customer_id` on the current cart. Call this after login
* `forget()` removes the cart ID from the session without deleting the cart itself

## Models

### Cart

```php theme={null}
Shopper\Cart\Models\Cart
```

| Column          | Type      | Nullable | Description                         |
| --------------- | --------- | -------- | ----------------------------------- |
| `id`            | bigint    |          | Primary key                         |
| `customer_id`   | bigint    | Yes      | FK to `users`                       |
| `channel_id`    | bigint    | Yes      | FK to `shopper_channels`            |
| `zone_id`       | bigint    | Yes      | FK to `shopper_zones`               |
| `currency_code` | string    |          | ISO currency code (e.g. `USD`)      |
| `coupon_code`   | string    | Yes      | Applied discount code               |
| `completed_at`  | timestamp | Yes      | Set when cart is converted to order |
| `metadata`      | json      | Yes      | Custom data                         |
| `created_at`    | timestamp |          |                                     |
| `updated_at`    | timestamp |          |                                     |

#### Relationships

```php theme={null}
$cart->lines;            // HasMany<CartLine>
$cart->addresses;        // HasMany<CartAddress>
$cart->customer;         // BelongsTo<User>
$cart->channel;          // BelongsTo<Channel>
$cart->zone;             // BelongsTo<Zone>
```

#### Methods

```php theme={null}
$cart->isCompleted();       // bool, checks if completed_at is set
$cart->shippingAddress();   // ?CartAddress, address with type Shipping
$cart->billingAddress();    // ?CartAddress, address with type Billing
```

### CartLine

```php theme={null}
Shopper\Cart\Models\CartLine
```

| Column              | Type         | Nullable | Description                                    |
| ------------------- | ------------ | -------- | ---------------------------------------------- |
| `id`                | bigint       |          | Primary key                                    |
| `cart_id`           | bigint       |          | FK to cart                                     |
| `purchasable_type`  | string       |          | Morph type (`Product`, `ProductVariant`, etc.) |
| `purchasable_id`    | bigint       |          | Morph ID                                       |
| `quantity`          | unsigned int |          | Item quantity                                  |
| `unit_price_amount` | unsigned int |          | Price per unit in cents                        |
| `metadata`          | json         | Yes      | Custom data                                    |

#### Relationships

```php theme={null}
$line->cart;           // BelongsTo<Cart>
$line->purchasable;    // MorphTo, Product, ProductVariant, etc.
$line->adjustments;    // HasMany<CartLineAdjustment>
$line->taxLines;       // HasMany<CartLineTaxLine>
```

### CartAddress

```php theme={null}
Shopper\Cart\Models\CartAddress
```

| Column        | Type   | Nullable | Description                                |
| ------------- | ------ | -------- | ------------------------------------------ |
| `id`          | bigint |          | Primary key                                |
| `cart_id`     | bigint |          | FK to cart                                 |
| `type`        | string |          | `shipping` or `billing` (AddressType enum) |
| `country_id`  | bigint | Yes      | FK to `shopper_countries`                  |
| `first_name`  | string | Yes      |                                            |
| `last_name`   | string |          |                                            |
| `company`     | string | Yes      |                                            |
| `address_1`   | string |          | Street address                             |
| `address_2`   | string | Yes      | Apartment, suite, etc.                     |
| `city`        | string |          |                                            |
| `state`       | string | Yes      |                                            |
| `postal_code` | string |          |                                            |
| `phone`       | string | Yes      |                                            |

The `full_name` accessor combines `first_name` and `last_name`.

### CartLineAdjustment

```php theme={null}
Shopper\Cart\Models\CartLineAdjustment
```

| Column         | Type         | Nullable | Description              |
| -------------- | ------------ | -------- | ------------------------ |
| `id`           | bigint       |          | Primary key              |
| `cart_line_id` | bigint       |          | FK to cart line          |
| `discount_id`  | bigint       | Yes      | FK to discount           |
| `amount`       | unsigned int |          | Discount amount in cents |
| `code`         | string       | Yes      | Coupon code              |

### CartLineTaxLine

```php theme={null}
Shopper\Cart\Models\CartLineTaxLine
```

| Column         | Type         | Nullable | Description                       |
| -------------- | ------------ | -------- | --------------------------------- |
| `id`           | bigint       |          | Primary key                       |
| `cart_line_id` | bigint       |          | FK to cart line                   |
| `tax_rate_id`  | bigint       | Yes      | FK to tax rate                    |
| `code`         | string       |          | Tax code                          |
| `name`         | string       |          | Tax name (e.g. "VAT")             |
| `rate`         | decimal(8,4) |          | Tax rate (e.g. `20.0000` for 20%) |
| `amount`       | unsigned int |          | Tax amount in cents               |

## Calculation Pipeline

Cart totals are calculated through a pipeline of configurable steps. Each pipe receives a `CartPipelineContext`, performs its calculation, and passes it to the next pipe.

### Default Pipeline

```php theme={null}
use Shopper\Cart\Pipelines\CalculateLines;
use Shopper\Cart\Pipelines\ApplyDiscounts;
use Shopper\Cart\Pipelines\CalculateTax;
use Shopper\Cart\Pipelines\Calculate;

// config/shopper/cart.php
'pipelines' => [
    'cart' => [
        CalculateLines::class,
        ApplyDiscounts::class,
        CalculateTax::class,
        Calculate::class,
    ],
],
```

### Pipeline Steps

**1. CalculateLines**. Computes the subtotal for each line (`unit_price_amount × quantity`) and sums them into `$context->subtotal`.

**2. ApplyDiscounts**. If the cart has a `coupon_code`, validates the discount through `DiscountValidator` and calculates the discount amount. For percentage discounts, each line gets `(lineSubtotal × rate / 100)`. For fixed amount discounts, the amount is distributed proportionally across lines. Creates `CartLineAdjustment` records.

**3. CalculateTax**. Resolves the shipping address country and calculates tax for each line using the core `TaxCalculator`. The taxable amount per line is `(lineSubtotal - discountAmount)`. Creates `CartLineTaxLine` records.

**4. Calculate**. Computes the final total. If tax is inclusive: `total = subtotal - discountTotal`. If tax is exclusive: `total = subtotal - discountTotal + taxTotal`. The minimum total is always `0`.

### Custom Pipeline

You can replace or extend the pipeline in `config/shopper/cart.php`. Add a custom pipe to handle shipping costs, loyalty points, or any other calculation:

```php theme={null}
'pipelines' => [
    'cart' => [
        CalculateLines::class,
        ApplyDiscounts::class,
        App\Cart\Pipes\ApplyShippingCost::class,
        CalculateTax::class,
        Calculate::class,
    ],
],
```

Your custom pipe must implement `__invoke(CartPipelineContext $context, Closure $next)`:

```php theme={null}
namespace App\Cart\Pipes;

use Closure;
use Shopper\Cart\Pipelines\CartPipelineContext;

final class ApplyShippingCost
{
    public function __invoke(CartPipelineContext $context, Closure $next): mixed
    {
        // Your calculation logic here

        return $next($context);
    }
}
```

## Discount Validation

When a coupon is applied during the pipeline, the `DiscountValidator` runs a series of checks before the discount is calculated. Each check produces a clear, translatable error message.

### Validation Rules

| Rule             | Condition                                                                                       | Error Key                           |
| ---------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------- |
| Active           | `discount->is_active` must be true                                                              | `discount.not_active`               |
| Started          | `discount->start_at` must be in the past                                                        | `discount.not_started`              |
| Not expired      | `discount->end_at` must be null or in the future                                                | `discount.expired`                  |
| Usage limit      | Total uses must be under the global limit                                                       | `discount.usage_limit_reached`      |
| Per-user limit   | Customer must not have exceeded their per-user limit                                            | `discount.already_used`             |
| Eligibility      | If restricted to specific customers, the cart customer must be in the list                      | `discount.customer_not_eligible`    |
| Requires login   | If eligibility is set, or the discount has a per-user limit, the cart must have a `customer_id` | `discount.requires_login`           |
| Zone             | If the discount has a `zone_id`, it must match `cart->zone_id`                                  | `discount.not_available_in_zone`    |
| Minimum amount   | Cart subtotal must meet the minimum purchase requirement                                        | `discount.min_amount_not_reached`   |
| Minimum quantity | Total line quantity must meet the minimum quantity requirement                                  | `discount.min_quantity_not_reached` |

### Discount Application Modes

Discounts support two application scopes via `discount->apply_to`:

* **Order** (`DiscountApplyTo::Order`). Applies to all cart lines
* **Specific products**. Applies only to lines matching the products/variants configured on the discount

And two discount types:

* **Percentage**. Each applicable line gets `(lineSubtotal × value / 100)`
* **Fixed amount**. The fixed amount is distributed proportionally across applicable lines based on their share of the total subtotal

## Order Conversion

The `CreateOrderFromCartAction` converts a completed cart into an order. This is the bridge between the storefront cart and the order management system.

```php theme={null}
use Shopper\Cart\Actions\CreateOrderFromCartAction;

$action = resolve(CreateOrderFromCartAction::class);
$order = $action->execute($cart);
```

### What Happens

The entire operation runs inside a database transaction with a `FOR UPDATE` lock on the cart:

1. **Reserves the discount**. If a coupon is applied, locks the discount row with `lockForUpdate`, validates the per-user limit against `orders.discount_id`, and increments `total_use` atomically only if the global `usage_limit` has not been reached
2. **Calculates totals**. Runs the full pipeline to get final amounts
3. **Creates order addresses**. Copies cart shipping/billing addresses to `OrderAddress` records
4. **Creates the order**. With `price_amount`, `tax_amount`, `currency_code`, all foreign keys, and the discount snapshot (`discount_id`, `discount_code`, `discount_type`, `discount_value_at_apply`, `discount_currency_code`)
5. **Creates order items and reserves stock**. One order item per cart line, including the discount amount from adjustments. For every line whose purchasable tracks inventory, stock is reserved through the `StockReserver` contract under a `lockForUpdate` row lock on the stockable. If the reservable quantity is less than the line quantity, `InsufficientStockException` is thrown and the whole transaction rolls back
6. **Creates order tax lines**. Via `CreateOrderTaxLinesAction`
7. **Marks cart as completed**. Sets `completed_at` to prevent further modifications
8. **Dispatches `CartCompleted` event**. With the cart and the created order

<Note>
  If the cart has already been completed, `CartCompletedException` is thrown before any work begins. The `FOR UPDATE` lock prevents race conditions from concurrent checkout attempts.
</Note>

### Discount Limit Exceptions

Two failure paths around discount limits can short-circuit the transaction. `DiscountLimitReachedException::global($code)` is thrown when the global `usage_limit` was exhausted between cart validation and order commit. `DiscountLimitReachedException::perUser($code)` is thrown when the customer has already redeemed a discount with `usage_limit_per_user` set. In both cases the transaction rolls back and no order is created.

```php theme={null}
use Shopper\Cart\Actions\CreateOrderFromCartAction;
use Shopper\Cart\Exceptions\DiscountLimitReachedException;

try {
    $order = resolve(CreateOrderFromCartAction::class)->execute($cart);
} catch (DiscountLimitReachedException $e) {
    return back()->with('error', 'This discount can no longer be used.');
}
```

For the full enforcement model, see [Usage Limit Enforcement](/v2/discounts#usage-limit-enforcement) on the Discounts page.

### Stock Reservation

Stock is reserved during checkout, inside the same transaction that creates the order, rather than asynchronously after the order commits. This closes the oversell window where two concurrent checkouts could each read the same last unit and both succeed. Reservation is delegated to the `Shopper\Core\Contracts\StockReserver` contract:

```php theme={null}
namespace Shopper\Core\Contracts;

use Illuminate\Database\Eloquent\Model;
use Shopper\Core\Models\Contracts\Stockable;

interface StockReserver
{
    public function reserve(Stockable $product, int $quantity, ?Model $reference = null, ?int $userId = null): int;
}
```

The default implementation, `LockingStockReserver`, takes a `lockForUpdate` row lock on the stockable, decrements the available quantity, and returns the amount actually reserved. The action throws `InsufficientStockException` when that amount is short of the requested quantity. Only purchasables that return `true` from `tracksInventory()` are reserved, so virtual and external products are skipped.

To plug in a custom strategy, such as reserving against an external warehouse system, bind your own implementation in a service provider:

```php theme={null}
use Shopper\Core\Contracts\StockReserver;

$this->app->bind(StockReserver::class, WarehouseStockReserver::class);
```

## Events

| Event           | Dispatched When                    | Properties                   |
| --------------- | ---------------------------------- | ---------------------------- |
| `CartCompleted` | Cart is converted to an order      | `Cart $cart`, `Order $order` |
| `CouponApplied` | Coupon code is set on a cart       | `Cart $cart`, `string $code` |
| `CouponRemoved` | Coupon code is cleared from a cart | `Cart $cart`                 |

```php theme={null}
use Shopper\Cart\Events\CartCompleted;
use Shopper\Cart\Events\CouponApplied;
use Shopper\Cart\Events\CouponRemoved;
```

## Configuration

Publish the configuration file:

```bash theme={null}
php artisan vendor:publish --tag=shopper-cart-config
```

```php theme={null}
// config/shopper/cart.php

return [
    'pipelines' => [
        'cart' => [
            CalculateLines::class,
            ApplyDiscounts::class,
            CalculateTax::class,
            Calculate::class,
        ],
    ],

    'session' => [
        'key' => 'shopper_cart',     // Session key for storing cart ID
        'auto_create' => false,      // Auto-create cart when current() is called
    ],

    'prune_after_days' => 30,        // Days before abandoned carts are pruned

    'models' => [
        'cart' => Cart::class,       // Swappable via model contracts
        'cart_line' => CartLine::class,
    ],
];
```

### Model Swapping

The `Cart` and `CartLine` models implement core contracts (`CartContract`, `CartLineContract`) and use the `HasModelContract` trait. You can replace them with your own models:

```php theme={null}
// config/shopper/cart.php
'models' => [
    'cart' => App\Models\Cart::class,
    'cart_line' => App\Models\CartLine::class,
],
```

Your custom models must implement the corresponding contracts from `Shopper\Core\Models\Contracts`.

## Abandoned Carts

A cart is considered "abandoned" when it has items but no activity for a configurable period. Shopper tracks this automatically and provides both an admin interface and programmatic tools to manage abandoned carts.

### Admin Panel

The admin panel includes an Abandoned Carts page under the Orders section. It lists all carts that are not completed, have at least one line item, and have been inactive for longer than the configured threshold. Administrators can view cart contents, the customer (if authenticated), and the associated channel.

### Configuration

Two config keys in `config/shopper/cart.php` control abandoned cart behavior:

```php theme={null}
'abandoned_after_minutes' => 60,  // Cart is "abandoned" after 60 minutes of inactivity
'prune_after_days' => 30,         // Abandoned carts are deleted after 30 days
```

### Querying Abandoned Carts

To find abandoned carts programmatically, for example to send recovery emails:

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

$abandonedCarts = Cart::query()
    ->whereNull('completed_at')
    ->whereHas('lines')
    ->whereNotNull('customer_id')
    ->where('updated_at', '<', now()->subMinutes(
        config('shopper.cart.abandoned_after_minutes', 60)
    ))
    ->with(['customer', 'lines.purchasable'])
    ->get();
```

### Pruning

Abandoned carts that are too old are cleaned up with a scheduled command:

```bash theme={null}
php artisan shopper:prune-carts
php artisan shopper:prune-carts --days=7
```

By default, carts that haven't been updated in the last 30 days (configurable via `prune_after_days`) are deleted. Only carts where `completed_at` is `null` are pruned. Completed carts are kept as historical records tied to their orders.

Schedule the command in your `routes/console.php`:

```php theme={null}
use Illuminate\Support\Facades\Schedule;

Schedule::command('shopper:prune-carts')->daily();
```

## Storefront Implementation

This section shows how to build a complete cart experience in your storefront. The examples are based on the patterns used in the [Shopper demo store](https://demo.laravelshopper.dev).

### Cart Session Helper

A common pattern is to create a `cartSession()` helper that handles cart creation with the correct zone and channel. This avoids repeating the creation logic across your controllers and components:

```php theme={null}
// app/helpers.php

use Shopper\Cart\CartSessionManager;
use Shopper\Cart\Models\Cart;
use Shopper\Core\Models\Channel;

function cartSession(): Cart
{
    $session = resolve(CartSessionManager::class);
    $cart = $session->current();

    if (! $cart) {
        $defaultChannel = Channel::query()->scopes('default')->first();

        $cart = $session->create([
            'currency_code' => shopper_currency(),
            'channel_id' => $defaultChannel?->id,
            'zone_id' => session('zone_id'),
            'customer_id' => auth()->id(),
        ]);
    }

    return $cart;
}
```

Register the helper in your `composer.json`:

```json theme={null}
{
    "autoload": {
        "files": ["app/helpers.php"]
    }
}
```

### Associating Cart on Login

When a guest adds items to their cart and then logs in, the cart should be associated with their account. Register a listener for the `Login` event in your `AppServiceProvider`:

```php theme={null}
use Illuminate\Auth\Events\Login;
use Illuminate\Support\Facades\Event;
use Shopper\Cart\CartSessionManager;

public function boot(): void
{
    Event::listen(Login::class, function (Login $event): void {
        resolve(CartSessionManager::class)->associate($event->user);
    });
}
```

### Add to Cart Action

Create a dedicated action class that handles adding both products and variants to the cart:

```php theme={null}
namespace App\Actions\Cart;

use App\Models\Product;
use App\Models\ProductVariant;
use Shopper\Cart\CartManager;
use Shopper\Cart\Models\CartLine;

final readonly class AddToCart
{
    public function __construct(
        private CartManager $cartManager,
    ) {}

    public function handle(Product $product, ?ProductVariant $variant = null): CartLine
    {
        $purchasable = $variant ?? $product;

        return $this->cartManager->add(cartSession(), $purchasable);
    }
}
```

### Livewire Cart Component

Here is a complete Livewire component for displaying and managing the cart as a slide-over panel:

```php theme={null}
namespace App\Livewire;

use App\Models\ProductVariant;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Livewire\Attributes\On;
use Livewire\Component;
use Shopper\Cart\CartManager;
use Shopper\Cart\CartSessionManager;
use Shopper\Cart\Models\CartLine;

final class ShoppingCart extends Component
{
    public int|float $subtotal = 0;

    /** @var Collection<int, CartLine> */
    public Collection $items;

    public function mount(): void
    {
        $this->loadCart();
    }

    #[On('cartUpdated')]
    public function loadCart(): void
    {
        $cart = resolve(CartSessionManager::class)->current();

        if (! $cart) {
            $this->items = collect();
            $this->subtotal = 0;

            return;
        }

        $cart->load('lines.purchasable');
        $cart->lines->loadMorph('purchasable', [
            ProductVariant::class => ['product'],
        ]);

        $this->items = $cart->lines;

        $context = resolve(CartManager::class)->calculate($cart);
        $this->subtotal = $context->subtotal;
    }

    public function removeFromCart(int $lineId): void
    {
        resolve(CartManager::class)->remove(cartSession(), $lineId);

        $this->loadCart();
        $this->dispatch('cartUpdated');
    }

    public function render(): View
    {
        return view('livewire.shopping-cart');
    }
}
```

The corresponding Blade view displays each cart line with its product details and a remove button:

```blade theme={null}
<div>
    @if ($items->isEmpty())
        <p>Your cart is empty.</p>
    @else
        <ul>
            @foreach ($items as $line)
                <li class="flex items-center justify-between py-4">
                    <div>
                        <p class="font-medium">
                            @if ($line->purchasable instanceof \App\Models\ProductVariant)
                                {{ $line->purchasable->product->name }} - {{ $line->purchasable->name }}
                            @else
                                {{ $line->purchasable->name }}
                            @endif
                        </p>
                        <p class="text-sm text-gray-500">
                            Qty: {{ $line->quantity }}
                            &middot;
                            {{ shopper_money_format($line->unit_price_amount, cartSession()->currency_code) }}
                        </p>
                    </div>
                    <button wire:click="removeFromCart({{ $line->id }})">Remove</button>
                </li>
            @endforeach
        </ul>

        <div class="border-t pt-4">
            <p class="font-medium">
                Subtotal: {{ shopper_money_format($subtotal, cartSession()->currency_code) }}
            </p>
        </div>
    @endif
</div>
```

### Cart Header Button

A small component that displays the cart item count in the navigation. It listens for the `cartUpdated` event to refresh automatically:

```php theme={null}
namespace App\Livewire;

use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Component;
use Shopper\Cart\CartSessionManager;

final class ShoppingCartButton extends Component
{
    public int $count = 0;

    public function mount(): void
    {
        $this->updateCount();
    }

    #[On('cartUpdated')]
    public function updateCount(): void
    {
        $cart = resolve(CartSessionManager::class)->current();

        $this->count = $cart ? $cart->lines->sum('quantity') : 0;
    }

    public function render(): View
    {
        return view('livewire.shopping-cart-button');
    }
}
```

### Displaying Cart Totals

Use the `CartManager::calculate()` method to get the full breakdown. The returned `CartPipelineContext` contains all computed values:

```php theme={null}
$context = resolve(CartManager::class)->calculate($cart);
```

```blade theme={null}
<div>
    <p>Subtotal: {{ shopper_money_format($context->subtotal, $cart->currency_code) }}</p>

    @if ($context->discountTotal > 0)
        <p>Discount: -{{ shopper_money_format($context->discountTotal, $cart->currency_code) }}</p>
    @endif

    @if ($context->taxTotal > 0)
        <p>Tax: {{ shopper_money_format($context->taxTotal, $cart->currency_code) }}</p>
    @endif

    <p class="font-bold">Total: {{ shopper_money_format($context->total, $cart->currency_code) }}</p>
</div>
```

### Applying a Coupon

Validate and apply a discount code to the cart. The actual discount calculation happens during the pipeline when `calculate()` is called:

```php theme={null}
use Shopper\Cart\Exceptions\InvalidDiscountException;

try {
    resolve(CartManager::class)->applyCoupon($cart, $couponCode);
} catch (InvalidDiscountException $e) {
    return back()->with('error', $e->getMessage());
}
```

### Setting Checkout Addresses

Add shipping and billing addresses to the cart before converting to an order:

```php theme={null}
use Shopper\Core\Enum\AddressType;

$manager = resolve(CartManager::class);

$manager->addAddress($cart, AddressType::Shipping, [
    'first_name' => $request->first_name,
    'last_name' => $request->last_name,
    'address_1' => $request->address,
    'city' => $request->city,
    'postal_code' => $request->postal_code,
    'country_id' => $request->country_id,
]);

$manager->addAddress($cart, AddressType::Billing, $billingData);
```

### Converting to Order

When the customer completes checkout, convert the cart into an order. The action runs inside a database transaction, calculates final totals, creates order records, and marks the cart as completed. If a discount limit is hit at commit time, catch `DiscountLimitReachedException` and surface a friendly message:

```php theme={null}
use Shopper\Cart\Actions\CreateOrderFromCartAction;
use Shopper\Cart\Exceptions\DiscountLimitReachedException;
use Shopper\Cart\Facades\Cart;

try {
    $order = resolve(CreateOrderFromCartAction::class)->execute(Cart::current());
} catch (DiscountLimitReachedException $e) {
    return back()->with('error', 'This discount can no longer be used.');
}

Cart::forget();
```

<Note>
  After converting the cart to an order, call `Cart::forget()` to clear the session. The cart is marked as completed and cannot be modified, but the session should be cleaned up so `Cart::current()` returns `null`. See the [Payments](/v2/payments) page for processing the order payment.
</Note>
