Skip to content

Cart Management

Flow ID: CF-05 | Module(s): eshop, Cart domain | Complexity: High | Last Updated: 2026-04-28

Business Overview

The shopping cart allows customers to collect products before checkout. Ecommercen has two independent cart implementations:

  1. Modern Database Cart (REST API) — persists in shop_cart + shop_cart_item tables, identified by JWT token (customers) or X-Cart-Token header (guests). Used by SPA/headless frontends.
  2. Legacy Session Cart — persists in $_SESSION['cart_contents'], used by the server-rendered storefront.

What customers experience:

  • Add product variants (SKUs) to cart with quantities and optional customizations
  • Automatic quantity merging for identical items (same SKU + same options)
  • Different customization options create separate cart rows
  • Coupons can be applied/removed
  • Guest carts persist across sessions (DB cart) or until session expires (legacy)
  • On login, guest carts merge into the customer's existing cart

Key business behaviors:

  • Stock validation occurs at add-to-cart time (legacy) or at checkout time (modern)
  • Cart items reference product_code_id (specific SKU), not just product ID
  • Gift rules and bundle pricing are evaluated on every cart render
  • Cart contents feed directly into the checkout pipeline

API Reference

Modern REST Endpoints (Database Cart)

MethodPathAuthDescription
GET/rest/cartGuest or Customer JWTGet current cart with items and totals
POST/rest/cart/itemsGuest or Customer JWTAdd item to cart
POST/rest/cart/items/{id}Guest or Customer JWTUpdate item quantity
DELETE/rest/cart/items/{id}Guest or Customer JWTRemove item
DELETE/rest/cartGuest or Customer JWTClear entire cart
POST/rest/cart/couponGuest or Customer JWTApply coupon code
DELETE/rest/cart/couponGuest or Customer JWTRemove coupon
POST/rest/cart/claimCustomer JWT requiredMerge guest cart into customer cart

Browse endpoints interactively in the API Reference.

Legacy AJAX Endpoints (Session Cart)

MethodPathControllerDescription
GET/api/cartAdvApiCartController::index()Get session cart as JSON
POST/api/cart/cartDataAdvApiCartController::cartData()Get cart with VAT context for checkout (see below)
POST/api/cart/updateAdvApiCartController::update()Add/update single item (see below)
POST/api/cart/massUpdateAdvApiCartController::massUpdate()Batch add/update multiple items (see below)
DELETE/api/cart/destroyAdvApiCartController::destroy()Clear session cart

cartData() — Checkout-aware cart fetch

Used during checkout to get cart contents with correct VAT calculation based on the customer's address context. Accepts POST body with address/invoice parameters and returns the same cart JSON as index() but with VAT adjusted for the delivery destination.

Parameters (POST form data):

  • invoice (bool, optional) — whether the order is an invoice (affects VAT)
  • useAddress (string, optional) — which address determines VAT area: ORDER_ADDRESS_ESHOP (default), ORDER_ADDRESS_BILLING, or ORDER_ADDRESS_SHIPPING
  • billingCountry (string, optional) — country code for billing address (default GR)
  • shippingCountry (string, optional) — country code for shipping address (default GR)

Response: Same cart JSON structure as index(){cartContents, liveData, ...}.

massUpdate() — Batch cart operations

Adds or updates multiple cart items in a single request. Each item is validated first; if any item fails validation, the entire batch is rejected and the current cart state is returned with errors. After all items are processed, bundle labeling is re-evaluated and after-add-to-cart recommendations are computed.

Request body (JSON array):

json
[
  {
    "productCodeId": 123,
    "quantity": 2,
    "cartProductCustomizations": null,
    "recommendationId": "abc",
    "recommendationType": "ai_recommendation"
  }
]

Fields per item:

  • productCodeId (int, required) — SKU identifier
  • quantity (int, required) — desired quantity (0 = remove from cart)
  • cartProductCustomizations (object, optional) — customization schema data
  • recommendationId (string, optional) — tracking ID for recommendation attribution
  • recommendationType (string, optional) — recommendation source type (BasketTrackType enum value)

Response: Cart JSON with optional recommendation key containing suggested products.

Side effects: Reports cart events to Manago, Advisable AI, Meta Conversions API, and Matomo when applicable.


Implementation 1: Modern Database Cart

Architecture

ComponentPathPurpose
REST Controllersrc/Rest/Cart/Controllers/Cart.php8 endpoints; delegates to CartResource with eager-loaded relations
Domain Servicesrc/Domains/Cart/CartService.phpBusiness logic
Cart Entitysrc/Domains/Cart/Cart/Repository/Entity.phpMaps shop_cart
CartItem Entitysrc/Domains/Cart/CartItem/Repository/Entity.phpMaps shop_cart_item
Totals Calculatorsrc/Domains/Cart/CartTotalsCalculator.phpPrice calculation
Cart Repositorysrc/Domains/Cart/Cart/Repository/Repository.phpRead repository; now injected into controller for eager relation loading
Cart Resourcesrc/Rest/Cart/Resources/Cart/Resource.phpCanonical wire shape for the REST cart response; controller delegates here
CartItem Resourcesrc/Rest/Cart/Resources/CartItem/Resource.phpItem serialization; includes nested productCode relation
CartItem Collectionsrc/Rest/Cart/Resources/CartItem/Collection.phpCollection wrapper for cart items
CartTotals Resourcesrc/Rest/Cart/Resources/CartTotals/Resource.phpSchema-only OpenAPI component for totals block

REST Response Structure

The controller resolves a 4-level deep relation graph in a single batched CartRepository::get() call, eliminating N+1 queries:

Cart (shop_cart)
  └── items [ONE_TO_MANY] → CartItem (shop_cart_item)
        └── productCode [BELONGS_TO] → ProductCode (shop_product_codes)
              ├── product [BELONGS_TO] → Product (shop_products)
              │     ├── translations [MUI]
              │     └── vendor [BELONGS_TO] → Vendor
              │           └── translations [MUI]
              └── media [ONE_TO_MANY] → Media (shop_product_media)

Each item in the items array includes the nested productCode object (product name, vendor, media) when loaded. Totals are computed by CartTotalsCalculator::calculate() and merged into the response envelope as { subtotal: float, itemCount: int } (src/Rest/Cart/Controllers/Cart.php:41-51, 106-135).

Database Schema

shop_cart table:

ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
customer_idINT NULLFor logged-in customers (UNIQUE)
cart_tokenVARCHAR(64) NULLFor guests — 64 hex chars (UNIQUE)
currency_codeVARCHAR(3) DEFAULT 'EUR'Cart currency
coupon_codeVARCHAR(50) NULLApplied discount code
notesTEXT NULLDelivery notes
created_at / updated_atDATETIMETimestamps

shop_cart_item table:

ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
cart_idINTFK to shop_cart (CASCADE delete)
product_code_idINTProduct SKU reference
qtyINT DEFAULT 1Quantity
optionsJSON NULLCustomization data
added_atDATETIMEWhen added

Guest Cart Token Lifecycle

  1. First request: Client calls POST /rest/cart/items without JWT
  2. Token generated: bin2hex(random_bytes(32)) = 64 random hex characters
  3. Client stores token: Returned in response as cart.cartToken
  4. Subsequent requests: Client sends X-Cart-Token: <token> header
  5. On login: Client calls POST /rest/cart/claim with JWT + old token → merges carts

Cart Merging on Login

CartService::claimCart($cartToken, $customerId):

  • If customer already has a cart: guest items merged into customer cart (smart qty merging), guest cart deleted
  • If customer has no cart: guest cart record updated with customer_id, cart_token cleared

Item Deduplication

Same product_code_id + identical JSON options = quantity merged. Different options = separate rows. Comparison is exact JSON string match.


Implementation 2: Legacy Session Cart

Architecture

ComponentPathPurpose
Cart Libraryapplication/libraries/Cart.phpSession-based storage
API Controllerecommercen/api/controllers/AdvApiCartController.phpAJAX endpoints
Cart Resourceecommercen/libraries/AdvCartResource.phpView model builder
Front Controllerecommercen/core/Adv_front_controller.phpCart init on every page
Vue Storeassets/vue/store/actions.jsClient-side response handling, toast feedback, analytics tags

Frontend Response Handling

When a user adds or updates a cart item on the storefront, the Vue store handles the AJAX response and decides which success toast to display. The call chain is:

  1. addToCartClicked dispatches api.addToCart(payload) to the legacy /api/cart/update endpoint.
  2. The response and the original payload (which carries the new quantity and productCodeId) are passed to dispatch('reportCartEvents', { productData, item: payload, response }).
  3. reportCartEvents (assets/vue/store/actions.js:187) looks up the previous quantity by finding the matching item in state.cart.cartContents. The lookup applies Number() coercion on both sides (Number(cartItem.productCodeId) === Number(item.productCodeId)) because the server-rendered initial cart state carries string-typed IDs while action payloads are numeric. It then mutates the response object: if the new quantity is less than the previous quantity (including full removal), it sets response.removeFromCart = true; otherwise it sets response.addToCart = true. Analytics tags fire in the same function: GA remove-from-cart fires when item.quantity === 0; GA add-to-cart fires otherwise — independently of the toast flag.
  4. dispatch('setCartData', cartData) reads the flags and shows a green success toast: cart.product_added when response.addToCart is set, cart.product_removed when response.removeFromCart is set.

Session Storage

Cart stored in $_SESSION['cart_contents'] as array keyed by row ID (MD5 hash):

[rowId] => {
    rowId: string,           // MD5(productCodeId + JSON(options))
    productCodeId: int,
    productId: int,
    quantity: int,
    options: {
        timestamp: int,
        recommendation: {id, type},
        cartProductCustomizations: array,
        appliedBundles: array
    }
}

Stock Validation

Legacy cart validates stock at add time via AdvApiCartController::cartHandle():

  • Calls product_model->getProductIdByProductCodeIdWithStock()
  • Blocks add if requestedQty > availableStock
  • Modern DB cart does NOT validate stock at add time — validation happens at checkout

CartResource (Every Page)

On every storefront page load, Front_c constructor initializes CartResource::getInstance() which lazily builds:

  • getCart() — cart items from session
  • getGifts() — matching gift rules for cart contents
  • getGiftRules() — active gift rules including Rule 13 (cheapest free)
  • getCoupon() — auto-applied default coupon if valid
  • getStockErrors() — live stock validation per item

Business Rules

RuleLegacyModern DB
Storage$_SESSION — lost on session expiryDatabase — persists indefinitely
Guest identitySession ID (implicit)Cart token (64 hex chars, explicit)
Stock validationAt add-to-cart timeAt checkout time
Cart mergingN/A (session persists through login)Explicit POST /rest/cart/claim
Coupon validationAuto-applied default + validation in cartStored as string, validated at checkout
Bundle pricingApplied via applyBundlePricingToCartLiveData()Not yet implemented
Gift rulesEvaluated every render via AdvCartResourceNot yet implemented
Multi-deviceLost on new sessionPersistent if token saved
Zero qty = removeYesYes
Same SKU + same optionsMerged (MD5 row ID)Merged (JSON comparison)

Client Extension Points

DI Container Overrides

php
// custom/Domains/container.php
$services->alias(
    Advisable\Domains\Cart\CartService::class,
    Custom\Domains\Cart\CartService::class
);

Legacy Overrides

ComponentOverride In
Cart libraryapplication/libraries/Cart.php (already in application/)
Cart API controllerapplication/modules/api/controllers/AdvApiCartController.php
CartResourceapplication/libraries/AdvCartResource.php
Coupon modelapplication/modules/coupons/models/Adv_coupons_model.php
Gift modelapplication/modules/eshop/models/Adv_gifts_model.php

Configuration

KeyPurpose
negative_stock (per product)Allow pre-orders beyond stock
cart_limit (per product)Max quantity per cart
PRODUCT_BUNDLES.ENABLEDEnable bundle pricing in cart
GIFT_PACKAGING.ENABLED / GIFT_PACKAGING.COSTGift wrapping option

Data Model

shop_cart — Modern cart header

ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
customer_idINT NULLLogged-in customer FK (UNIQUE)
cart_tokenVARCHAR(64) NULLGuest token — 64 hex chars (UNIQUE)
currency_codeVARCHAR(3) DEFAULT 'EUR'Cart currency
coupon_codeVARCHAR(50) NULLApplied discount code
notesTEXT NULLDelivery notes
created_atDATETIMECart creation time
updated_atDATETIMELast modification time

shop_cart_item — Modern cart line items

ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
cart_idINTFK to shop_cart (CASCADE delete)
product_code_idINTProduct SKU reference (FK to shop_product_codes.id)
qtyINT DEFAULT 1Quantity
optionsJSON NULLCustomization data (schema-based)
added_atDATETIMEWhen the item was added

shop_order_basket — Permanent order items

ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
order_idINTFK to shop_order
product_code_idINTSKU reference at time of order
quantityINTOrdered quantity
priceDECIMAL(10,4)Unit price at time of order
discountDECIMAL(5,2)Discount applied
vat_percentDECIMAL(5,2)VAT rate applied
is_giftTINYINT(1) DEFAULT 0Whether this is a gift item

Other tables involved

TablePurpose
product_codesSKU variants with stock (referenced by product_code_id)
coupons / coupon_rulesCoupon definitions and validation rules
gifts / gift_requirementsGift rule definitions and requirement conditions
tmp_shop_order_basketHistorical cart analytics (cron-maintained snapshots)

Live Testing Notes

Legacy cart API (/api/cart/update):

  • Requires JSON body (not form-encoded) — MY_Input::setInputStreamAsPost() does json_decode() on raw input. Form-encoded POST causes array_merge() TypeError.
  • Add-to-cart button on storefront sends: POST /api/cart/update?aatcr=true with {"productCodeId": N, "quantity": 1, "invoice": false, "billingCountry": null, "shippingCountry": null, "cartProductCustomizations": {}}
  • Response includes full cart state: cartContents, giftContents, giftRules, cartErrors, defaultCoupon, liveData[]
  • liveData contains per-item: productUrl, productName, images (4 sizes via /mediastream/), weight, points, discount info, oldPrice, finalPrice, stock, barcodes, vendorName
  • aatcr=true query param triggers "after add to cart recommendations" modal

REST cart API (/rest/cart):

  • Uses X-Cart-Token header for anonymous cart identification — returned on first addItem, must be sent back for subsequent requests
  • totals.subtotal reads the price from the parent shop_product.price column (resolved via productCode.productproduct_codes has no price column). CartTotalsCalculator::calculate() prefers the already-hydrated relation to avoid N+1 lookups and falls back to ProductCodeRepository::get($id, ['product' => []]) when the cart item was loaded without the graph. Discounts (discount_persent, special_discount_percent) and VAT are not applied at this stage.
  • Both addItem and updateItem accept quantity on the wire. The DB column is still named qty (unchanged for the cart snapshot shape on the order), but the REST field is uniformly quantity.
  • Cart claim endpoint (POST /rest/cart/claim) merges guest → customer cart

Known Issues & Security Gaps

  1. Gift rules not implemented in REST cartAdvCartResource in the legacy layer evaluates gift rules on every render, but the REST Cart controller has no equivalent. CartTotalsCalculator returns only subtotal and itemCount; free-gift eligibility is not computed. (src/Domains/Cart/CartTotalsCalculator.php)

  2. Bundle pricing not implemented in REST cart — The legacy cart applies bundle discount rules during price computation. The REST CartTotalsCalculator reads the unit price from shop_product.price via the hydrated productCode.product relation (src/Domains/Cart/CartTotalsCalculator.php:35-53), but applies no bundle membership check. Bundle discounts are silently absent from REST cart totals.

  3. No stock validation at add-to-cart time (REST path)CartService::addItem() inserts rows into shop_cart_item without checking shop_product.stock or shop_product.negative_stock. Stock is only enforced at checkout. A customer can add out-of-stock items to their REST cart. (src/Domains/Cart/Cart/Service.php)

  4. Cart merge on login not implemented in REST layer — The legacy frontend merges the guest cart into the authenticated cart on login (ecommercen/eshop/models/Adv_cart_model.php). The REST auth flow has no equivalent merge step. Guest cart items are lost when a customer logs in via the REST API.

Wiki Guides: Gift rule types and configuration — see Gifts Module. For REST API patterns see REST API Modules.

Shared Patterns