Skip to main content

Cart Field System

The cart field system is the foundation of EverShop's checkout logic. Every piece of data on a cart — from the subtotal and tax amount to the shipping address and coupon code — is defined as a field with its own calculation logic. Extensions can add new fields to inject custom data or business logic into the cart.

How Cart Fields Work

Each cart field is an object with three properties:

{
key: string; // Field name (e.g., 'sub_total', 'tax_amount')
resolvers: Function[]; // Array of functions that calculate the field's value
dependencies?: string[]; // Other fields this field depends on
}

When the cart is loaded or updated, EverShop:

  1. Sorts fields by their dependencies (topological sort).
  2. For each field, runs its resolvers in order.
  3. The final resolver's return value becomes the field's value.

Field Resolvers

Resolvers are functions that calculate a field's value. Inside a resolver, this gives you access to the cart (or cart item) data:

{
key: 'sub_total',
resolvers: [
async function resolver() {
// Access cart data via 'this'
const items = this.getItems();
let subTotal = 0;
for (const item of items) {
subTotal += item.getData('line_total');
}
return subTotal;
}
],
dependencies: ['items'] // Depends on items being calculated first
}

Each resolver receives the field's incoming value as its only argument and returns the new value. When several resolvers are registered for the same key they are chained — each one receives the previous one's return value.

Context Methods Available in Resolvers

Inside a cart field resolver, this provides:

MethodDescription
this.getData(key)Get a field's current value. Throws if no field with that key is registered.
this.getItems()Get all cart items (an array of Item)
this.setError(field, message)Set a validation error on a field. Pass a falsy message to clear it.
this.getTriggeredField()Get which field triggered the recalculation
this.getRequestedValue()Get the value that was passed to setData for the triggered field

Inside a cart item field resolver, this provides:

MethodDescription
this.getData(key)Get the item field's current value
this.setError(field, message)Set a validation error
this.getTriggeredField()Get which field triggered the recalculation
this.getRequestedValue()Get the value that was passed to setData for the triggered field
await this.getProduct()Async. Get the product data for this item (memoized per item)
this.getCart()Get the parent Cart object
Do not call setData from inside a resolver

setData is asyncawait cart.setData('customer_email', email) — and it triggers a full rebuild of every field. Calling it from inside a resolver throws:

Can not set value when object is building

To change a field's value from within a resolver, simply return the value you want. To read the value being set, use this.getRequestedValue() together with this.getTriggeredField():

{
key: 'gift_message',
resolvers: [
async function resolver() {
return this.getTriggeredField() === 'gift_message'
? this.getRequestedValue()
: this.getData('gift_message');
}
]
}

Adding Custom Cart Fields

Register custom cart fields in your extension's bootstrap.ts using the cartFields processor:

extensions/my-ext/src/bootstrap.ts
import { addProcessor } from '@evershop/evershop/lib/util/registry';

export default () => {
addProcessor('cartFields', (fields) => {
return fields.concat([
{
key: 'gift_message',
resolvers: [
async function resolver() {
// Accept the new value when this field is the one being set,
// otherwise keep what the cart already holds.
return this.getTriggeredField() === 'gift_message'
? this.getRequestedValue() || ''
: this.getData('gift_message') || '';
}
]
},
{
key: 'gift_wrap_fee',
resolvers: [
async function resolver() {
return this.getData('gift_message') ? 5.0 : 0;
}
],
dependencies: ['gift_message'] // Calculate after gift_message
}
]);
});
};
warning

A resolver can only read fields that are actually registered. this.getData('some_unregistered_key') throws Field some_unregistered_key not existed. List every field you read in dependencies so it is guaranteed to be resolved first.

Adding Custom Cart Item Fields

Similarly, use the cartItemFields processor:

extensions/my-ext/src/bootstrap.ts
import { addProcessor } from '@evershop/evershop/lib/util/registry';

export default () => {
addProcessor('cartItemFields', (fields) => {
return fields.concat([
{
key: 'personalization_text',
resolvers: [
async function resolver() {
return this.getTriggeredField() === 'personalization_text'
? this.getRequestedValue() || ''
: this.getData('personalization_text') || '';
}
]
}
]);
});
};

Field Dependencies

The dependencies array ensures fields are calculated in the correct order. If field B depends on field A, field A is always calculated first:

// Calculated first
{ key: 'sub_total', resolvers: [...] }

// Calculated second (depends on sub_total)
{ key: 'discount_amount', resolvers: [...], dependencies: ['sub_total'] }

// Calculated third (depends on both)
{ key: 'grand_total', resolvers: [...], dependencies: ['sub_total', 'discount_amount'] }
warning

Circular dependencies (A depends on B, B depends on A) will cause an error during cart calculation.

Built-in Cart Fields

The fields below are registered by core. Note that they do not all come from the checkout module — coupon, discount_amount and grand_total come from promotion, and the customer_* fields come from customer. If a module is disabled its fields do not exist, and getData on them throws.

FieldDescription
cart_idDatabase ID
uuidUnique cart identifier
currencyStore currency, from getStoreCurrency() (admin setting → shop.currency config → USD)
statusCart status (1 for an active cart)
sidSession ID that owns the cart
user_ipIP address the cart was created from
created_at / updated_atTimestamps
customer_idCustomer reference (null for guests)
customer_group_idCustomer group reference
customer_emailCustomer email address
customer_full_nameCustomer display name
total_qtyTotal quantity of items
total_weightTotal weight of items
packagesJSONB parcel proposal built from the items' package dimensions. Overridable with addProcessor('cartPackages', ...).
sub_totalSum of all items' line_total
sub_total_incl_taxSum of all items' line_total_incl_tax
sub_total_with_discountSubtotal after item discounts
sub_total_with_discount_incl_taxSubtotal after item discounts, including tax
tax_amountTax on the items
tax_amount_before_discountTax on the items before discounts are applied
shipping_tax_amountTax on the shipping fee
total_tax_amountItem tax plus shipping tax
discount_amountApplied discount
couponApplied coupon code
grand_totalFinal total
no_shipping_requiredtrue when every item is a non-shippable (digital) product
shipping_address_id / shipping_addressShipping address reference and the loaded row
billing_address_id / billing_addressBilling address reference and the loaded row
shipping_method_dataJSONB snapshot of the selected shipping method (provider code, method code, quoted cost, fingerprint, timestamp)
shipping_fee_draftShipping cost read from the shipping_method_data snapshot. The promotion module chains a second resolver onto this key that zeroes it for a free-shipping coupon — a good example of resolver chaining.
shipping_fee_tax_percentTax rate applied to the shipping fee
shipping_fee_excl_taxShipping cost before tax
shipping_fee_incl_taxShipping cost after tax
shipping_noteCustomer's delivery note
payment_methodSelected payment method code (validated against the available methods)
payment_method_nameDisplay name of the selected payment method
itemsThe cart items
warning

There is no shipping_method cart field. The legacy cart.shipping_method / cart.shipping_method_name columns were dropped along with the shipping_method table; the selected method now lives entirely in the shipping_method_data JSONB field. Read the method code with cart.getData('shipping_method_data')?.method_code.

Built-in Cart Item Fields

FieldDescription
cart_item_idDatabase ID
uuidUnique item identifier
cart_idParent cart reference
product_idProduct reference
product_uuidProduct UUID
product_skuProduct SKU
product_nameProduct display name
group_idAttribute group of the product
category_idCategory the product belongs to
thumbnailProduct thumbnail URL
productUrlStorefront URL of the product
variant_group_idVariant group reference
variant_optionsSelected variant options
qtyQuantity (validated against stock)
product_weightProduct weight
package_length, package_width, package_height, package_weightShipping package dimensions used by the packing strategy
no_shipping_requiredtrue for a non-shippable (digital) product
tax_class_idTax class reference
tax_percentTax percentage
tax_amountItem tax amount
tax_amount_before_discountItem tax amount before discounts
product_priceUnit price
product_price_incl_taxUnit price including tax
final_priceUnit price actually charged. Today it returns product_price verbatim — see the warning below.
final_price_incl_taxproduct_price_incl_tax verbatim
line_totalLine total: final_price * qty
line_total_incl_taxLine total including tax: final_price_incl_tax * qty
discount_amountItem discount
line_total_with_discountLine total after the item discount
line_total_with_discount_incl_taxLine total after the item discount, including tax
removeUrlURL that removes this item from the cart
warning

Two naming traps here:

  • There is no total cart-item field. The line total is line_total (and line_total_incl_tax).
  • final_price is not "price after discounts". Its resolver returns product_price unchanged. Discounts are applied at the line level: read discount_amount and line_total_with_discount instead.

Stock Validation

Cart item fields automatically validate quantities against product inventory:

  • If manage_stock is enabled and qty exceeds available stock, an error is set on the item.
  • If the product is out of stock, an error is set immediately.
  • These errors are tracked per-item and can be queried.

See Also



Support us


EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.