Skip to main content
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.

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.
Adding the same purchasable twice increments the existing line’s quantity instead of creating a duplicate.

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.
Products with allow_backorder set to true bypass stock checks entirely.

Updating a Line

Removing Lines

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.
Removing a coupon also deletes all line adjustments that were created by the discount pipeline.

Adding Addresses

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

Calculating Totals

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.

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

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

Relationships

Methods

CartLine

Relationships

CartAddress

The full_name accessor combines first_name and last_name.

CartLineAdjustment

CartLineTaxLine

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

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:
Your custom pipe must implement __invoke(CartPipelineContext $context, Closure $next):

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

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.

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

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.
For the full enforcement model, see 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:
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:

Events

Configuration

Publish the configuration file:

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:
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:

Querying Abandoned Carts

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

Pruning

Abandoned carts that are too old are cleaned up with a scheduled command:
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:

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.

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:
Register the helper in your composer.json:

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:

Add to Cart Action

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

Livewire Cart Component

Here is a complete Livewire component for displaying and managing the cart as a slide-over panel:
The corresponding Blade view displays each cart line with its product details and a remove button:

Cart Header Button

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

Displaying Cart Totals

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

Applying a Coupon

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

Setting Checkout Addresses

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

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:
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 page for processing the order payment.