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:- CartManager handles cart operations: add, update, remove, apply coupons, calculate totals
- CartSessionManager persists the current cart in the session for storefront use
- Pipeline system calculates totals through a configurable chain of pipes (subtotals → discounts → taxes → total)
- DiscountValidator enforces coupon rules: eligibility, usage limits, zones, minimum amounts
- CreateOrderFromCartAction converts a cart into an order within a database transaction
Cart Manager
TheCartManager 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 implementsPriceable, typically a Product or ProductVariant. The manager looks up the unit price from the purchasable’s price list using the cart’s currency.
Stock Validation
Everyadd() 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.
allow_backorder set to true bypass stock checks entirely.
Updating a Line
Removing Lines
Applying Coupons
TheapplyCoupon() method validates that the discount code exists in the database before setting it on the cart. The actual discount calculation happens during the pipeline.
Adding Addresses
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
TheCartSessionManager 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()returnsnullif no cart exists in the session andauto_createisfalsecurrent()returnsnullif the session cart has been completed (it won’t return stale carts)create()stores the new cart’s ID in the session and sets thecurrency_codefromshopper_currency()associate()sets thecustomer_idon the current cart. Call this after loginforget()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 aCartPipelineContext, 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 inconfig/shopper/cart.php. Add a custom pipe to handle shipping costs, loyalty points, or any other calculation:
__invoke(CartPipelineContext $context, Closure $next):
Discount Validation
When a coupon is applied during the pipeline, theDiscountValidator 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 viadiscount->apply_to:
- Order (
DiscountApplyTo::Order). Applies to all cart lines - Specific products. Applies only to lines matching the products/variants configured on the discount
- 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
TheCreateOrderFromCartAction 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 aFOR UPDATE lock on the cart:
- Reserves the discount. If a coupon is applied, locks the discount row with
lockForUpdate, validates the per-user limit againstorders.discount_id, and incrementstotal_useatomically only if the globalusage_limithas not been reached - Calculates totals. Runs the full pipeline to get final amounts
- Creates order addresses. Copies cart shipping/billing addresses to
OrderAddressrecords - 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) - 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
StockReservercontract under alockForUpdaterow lock on the stockable. If the reservable quantity is less than the line quantity,InsufficientStockExceptionis thrown and the whole transaction rolls back - Creates order tax lines. Via
CreateOrderTaxLinesAction - Marks cart as completed. Sets
completed_atto prevent further modifications - Dispatches
CartCompletedevent. 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.
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 theShopper\Core\Contracts\StockReserver contract:
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
TheCart and CartLine models implement core contracts (CartContract, CartLineContract) and use the HasModelContract trait. You can replace them with your own models:
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 inconfig/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: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 acartSession() helper that handles cart creation with the correct zone and channel. This avoids repeating the creation logic across your controllers and components:
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 theLogin 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:Cart Header Button
A small component that displays the cart item count in the navigation. It listens for thecartUpdated event to refresh automatically:
Displaying Cart Totals
Use theCartManager::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 whencalculate() 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, catchDiscountLimitReachedException 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.