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:
- Sorts fields by their dependencies (topological sort).
- For each field, runs its resolvers in order.
- 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:
| Method | Description |
|---|---|
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:
| Method | Description |
|---|---|
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 |
setData from inside a resolversetData is async — await 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:
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
}
]);
});
};
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:
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'] }
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.
| Field | Description |
|---|---|
cart_id | Database ID |
uuid | Unique cart identifier |
currency | Store currency, from getStoreCurrency() (admin setting → shop.currency config → USD) |
status | Cart status (1 for an active cart) |
sid | Session ID that owns the cart |
user_ip | IP address the cart was created from |
created_at / updated_at | Timestamps |
customer_id | Customer reference (null for guests) |
customer_group_id | Customer group reference |
customer_email | Customer email address |
customer_full_name | Customer display name |
total_qty | Total quantity of items |
total_weight | Total weight of items |
packages | JSONB parcel proposal built from the items' package dimensions. Overridable with addProcessor('cartPackages', ...). |
sub_total | Sum of all items' line_total |
sub_total_incl_tax | Sum of all items' line_total_incl_tax |
sub_total_with_discount | Subtotal after item discounts |
sub_total_with_discount_incl_tax | Subtotal after item discounts, including tax |
tax_amount | Tax on the items |
tax_amount_before_discount | Tax on the items before discounts are applied |
shipping_tax_amount | Tax on the shipping fee |
total_tax_amount | Item tax plus shipping tax |
discount_amount | Applied discount |
coupon | Applied coupon code |
grand_total | Final total |
no_shipping_required | true when every item is a non-shippable (digital) product |
shipping_address_id / shipping_address | Shipping address reference and the loaded row |
billing_address_id / billing_address | Billing address reference and the loaded row |
shipping_method_data | JSONB snapshot of the selected shipping method (provider code, method code, quoted cost, fingerprint, timestamp) |
shipping_fee_draft | Shipping 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_percent | Tax rate applied to the shipping fee |
shipping_fee_excl_tax | Shipping cost before tax |
shipping_fee_incl_tax | Shipping cost after tax |
shipping_note | Customer's delivery note |
payment_method | Selected payment method code (validated against the available methods) |
payment_method_name | Display name of the selected payment method |
items | The cart items |
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
| Field | Description |
|---|---|
cart_item_id | Database ID |
uuid | Unique item identifier |
cart_id | Parent cart reference |
product_id | Product reference |
product_uuid | Product UUID |
product_sku | Product SKU |
product_name | Product display name |
group_id | Attribute group of the product |
category_id | Category the product belongs to |
thumbnail | Product thumbnail URL |
productUrl | Storefront URL of the product |
variant_group_id | Variant group reference |
variant_options | Selected variant options |
qty | Quantity (validated against stock) |
product_weight | Product weight |
package_length, package_width, package_height, package_weight | Shipping package dimensions used by the packing strategy |
no_shipping_required | true for a non-shippable (digital) product |
tax_class_id | Tax class reference |
tax_percent | Tax percentage |
tax_amount | Item tax amount |
tax_amount_before_discount | Item tax amount before discounts |
product_price | Unit price |
product_price_incl_tax | Unit price including tax |
final_price | Unit price actually charged. Today it returns product_price verbatim — see the warning below. |
final_price_incl_tax | product_price_incl_tax verbatim |
line_total | Line total: final_price * qty |
line_total_incl_tax | Line total including tax: final_price_incl_tax * qty |
discount_amount | Item discount |
line_total_with_discount | Line total after the item discount |
line_total_with_discount_incl_tax | Line total after the item discount, including tax |
removeUrl | URL that removes this item from the cart |
Two naming traps here:
- There is no
totalcart-item field. The line total isline_total(andline_total_incl_tax). final_priceis not "price after discounts". Its resolver returnsproduct_priceunchanged. Discounts are applied at the line level: readdiscount_amountandline_total_with_discountinstead.
Stock Validation
Cart item fields automatically validate quantities against product inventory:
- If
manage_stockis enabled andqtyexceeds 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
- Registry and Processors — How field registration works
- Extension Development — Creating extensions
- Payment Method Development — Custom payment methods
Support us
EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.