Step-by-step traces of cart operations with events and plugins
Adding a product to the shopping cart via CartItemRepositoryInterface::save()
Get or create active quote for customer/guest
$quote = $this->quoteRepository->getActive($cartId);
// For guest: resolves masked ID to quote ID first
Fetch product with required options validation
$product = $this->productRepository->getById(
$cartItem->getSku(),
false,
$quote->getStoreId()
);
Add product to quote with buy request processing
$result = $quote->addProduct($product, $request);
// Dispatches: sales_quote_product_add_after
// Creates Quote\Item with options
Recalculate all cart totals with new item
$quote->collectTotals();
// Runs all collectors: subtotal, discount, shipping, tax, grand_total
Persist quote with items, addresses, and totals
$this->quoteRepository->save($quote);
// Dispatches: sales_quote_save_after
Events Fired
sales_quote_product_add_after, sales_quote_item_qty_set_after, sales_quote_collect_totals_before, sales_quote_collect_totals_after, sales_quote_save_after
The complete totals calculation sequence when Quote::collectTotals() is called
Reset totals and prepare addresses for collection
// Dispatches: sales_quote_collect_totals_before
$this->setTotalsCollectedFlag(false);
$this->setSubtotal(0)->setBaseSubtotal(0);
Each address runs its own totals collection
foreach ($this->getAllAddresses() as $address) {
$address->setCollectShippingRates(true);
$address->collectTotals();
}
Each collector modifies the quote/address totals
// Collector order from sales.xml:
// 1. subtotal - Calculate item prices
// 2. discount - Apply cart rules/coupons
// 3. shipping - Calculate shipping cost
// 4. tax - Calculate taxes
// 5. grand_total - Sum everything
Sum address totals into quote-level totals
$this->setSubtotal(
$this->getSubtotal() + $address->getSubtotal()
);
$this->setGrandTotal(
$this->getGrandTotal() + $address->getGrandTotal()
);
Mark totals as collected and dispatch event
$this->setTotalsCollectedFlag(true);
// Dispatches: sales_quote_collect_totals_after
The order placement flow via CartManagementInterface::placeOrder()
Run all validation rules before order creation
$validationResult = $this->quoteValidator->validate($quote);
// Checks: addresses, shipping method, payment, min amount
if (!$validationResult->isValid()) {
throw new LocalizedException($validationResult->getErrors());
}
Prevent concurrent order placement (QuoteMutex)
$this->quoteMutex->execute(
[$cartId],
function () use ($quote, $paymentMethod) {
return $this->submitQuote($quote, $paymentMethod);
}
);
Convert quote to order using QuoteManagement
$order = $this->quoteManagement->submit($quote);
// Dispatches: sales_model_service_quote_submit_before
// Creates Order from Quote data
// Dispatches: sales_model_service_quote_submit_success
Mark quote as inactive and link to order
$quote->setIsActive(false);
$quote->setReservedOrderId($order->getIncrementId());
$this->quoteRepository->save($quote);
Observers handle post-order tasks
// SubmitObserver handles:
// - Inventory deduction (via sales_order_place_after)
// - Email notifications
// - Customer balance deduction
Critical: Quote Mutex
The QuoteMutex prevents race conditions where a customer might double-click "Place Order" and create duplicate orders. Always use the mutex when modifying quote state during order placement.
Creating and managing anonymous shopping carts with masked IDs
GuestCartManagementInterface::createEmptyCart()
// POST /V1/guest-carts
$quoteId = $this->quoteManagement->createEmptyCart();
// Creates quote with customer_id = NULL, is_active = 1
Create UUID token for anonymous access
$maskedId = $this->quoteIdMaskFactory->create();
$maskedId->setQuoteId($quoteId)
->setMaskedId($this->randomGenerator->getUniqueHash());
$maskedId->save();
// Returns masked ID like: "pwa-abc123xyz789"
All guest operations resolve masked ID first
// MaskedQuoteIdToQuoteIdInterface
$quoteId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId);
// Throws NoSuchEntityException if not found
Guest cart merges with customer cart on authentication
// GuestCartManagementInterface::assignCustomer()
$this->quoteManagement->assignCustomer(
$guestQuoteId,
$customerId,
$storeId
);
// Items from guest cart are merged into customer's active cart
Applying discount codes via CouponManagementInterface::set()
Check code exists and is active
$quote->getShippingAddress()->setCollectShippingRates(true);
$quote->setCouponCode($couponCode);
$quote->collectTotals();
Discount collector processes cart rules
// Magento_SalesRule handles discount calculation
// - Finds rules matching coupon code
// - Validates conditions (cart amount, customer group, etc.)
// - Calculates discount per item/shipping
Confirm coupon was actually applied
if ($quote->getCouponCode() !== $couponCode) {
throw new NoSuchEntityException(
__('The coupon code isn\'t valid.')
);
}
$this->quoteRepository->save($quote);
Why Verification?
The coupon may be rejected if conditions aren't met (wrong customer group, cart total too low, expired, usage limit reached). The collector will silently clear the coupon if invalid, so we must verify it persisted.