Skip to content

Product Models

This reference documents all Twig-accessible properties and methods for product-related models. All prices are stored as integers in base currency units (cents) — use the |currency filter for display.

Product

The Product model is the central model for all product data. It is typically accessed via the Catalog component as product or through category/group relationships.

Core Properties

PropertyTypeDescription
idintPrimary key
namestringProduct name
slugstringURL slug
baseidstringBase identifier for URL generation
skustringSKU code
titlestringSEO/page title
descriptionstringFull description (HTML)
short_descriptionstringShort description
is_enabledboolWhether the product is active
created_atCarbonCreation date
updated_atCarbonLast update date
available_atCarbonAvailability date (for pre-orders)

Price Properties

PropertyTypeDescription
priceintRaw base price (no tax, no rules)
costintCost/wholesale price
is_on_saleboolWhether a discount applies (manual sale, catalog rules, or both)
sale_price_or_discountstringManual sale value: "5000" (fixed), "20%" (percent), or "-500" (offset)
original_priceintBase price without tax — respects tier pricing and quantity
original_sale_priceintSale price without tax — checks manual sale, then catalog rules
final_priceintDisplay price with tax (if tax display is enabled)
final_sale_priceintSale price with tax (if tax display is enabled)
sale_price_reductionintAmount saved: original_price - original_sale_price

TIP

Use final_price and final_sale_price for storefront display — they automatically apply tax display settings. Use original_price and original_sale_price when you need raw values without tax.

twig
{{ product.final_price|currency }}

{% if product.is_on_sale %}
    <del>{{ product.final_price|currency }}</del>
    <ins>{{ product.final_sale_price|currency }}</ins>
    <span>Save {{ product.sale_price_reduction|currency }}</span>
{% endif %}

Physical Properties

PropertyTypeDescription
weightfloatWeight
widthfloatWidth
heightfloatHeight
depthfloatDepth
volumefloatComputed: width × height × depth

Inventory Properties

PropertyTypeDescription
track_inventoryboolWhether stock is tracked
units_in_stockintCurrent stock quantity
hide_if_out_of_stockboolHide product when out of stock
allow_negative_stockboolAllow stock to go below zero
stock_alert_thresholdintLow stock notification threshold
allow_pre_orderboolAccept orders when out of stock

Visibility Properties

PropertyTypeDescription
is_visible_searchboolShow in search results
is_visible_catalogboolShow in catalog listings
is_visible_user_groupboolRestrict visibility to specific user groups

Review Properties

PropertyTypeDescription
reviews_ratingfloatCached average rating (0–5)
reviews_countintCached total approved reviews

Variant Properties

PropertyTypeDescription
use_variantsboolWhether variants are enabled
variant_generationstringGeneration mode
allow_price_tiersboolWhether tier pricing is enabled

Relationships

PropertyTypeDescription
categoriesCollection<Category>Associated categories
related_productsCollection<Product>Related products
manufacturerManufacturerManufacturer/brand
product_typeProductTypeProduct type (controls feature flags)
tax_classTaxClassTax class
imagesCollection<File>Product images
filesCollection<File>Downloadable files
optionsCollection<ProductOption>Configurable options (Size, Color)
extrasCollection<ProductExtra>Local extras (product-specific add-ons)
all_extrasCollection<ProductExtra>Combined local extras + extra set extras
propertiesCollection<ProductProperty>Specifications (Material, Weight)
price_tiersCollection<PriceTier>Volume pricing tiers
visible_price_tiersCollection<PriceTier>Tiers filtered for the current user's group (falls back to generic tiers)
variantsCollection<ProductVariant>Product variants
bundle_itemsCollection<ProductBundleItem>Bundle slots
reviewsCollection<ProductReview>Product reviews
custom_groupsCollection<CustomGroup>Custom product groups
user_groupsCollection<UserGroup>Visible-to user groups
extra_setsCollection<ProductExtraSet>Assigned extra option sets

Methods

MethodReturnsDescription
pageUrl($pageName)stringCMS page URL for the product
getBreadcrumbPath()array|nullParent category chain for breadcrumbs
getPrimaryCategory()Category|nullFirst associated category
isVisible()boolWhether product is enabled and not archived
isOutOfStock()boolWhether stock is below threshold
getOriginalPrice($qty, $groupId)intBase price with tier pricing
getOriginalSalePrice($qty, $groupId)intSale price (manual or catalog rules)
getFinalPrice($qty, $groupId)intDisplay price with tax
getFinalSalePrice($qty, $groupId)intSale price with tax
getSalePriceReduction($qty, $groupId)intDiscount amount
evalTierPrice($qty, $groupId)intEvaluate tier price for quantity
getCompiledRulePrice($qty, $groupId)int|nullCatalog rule price (null if none)
getAverageRating()floatCached average review rating
getReviewsCount()intCached approved review count
resolveVariant($options)ProductVariant|nullFind variant matching options (throws on mismatch)
resolveVariantSafe($options)ProductVariant|nullFind variant matching options (returns null on mismatch)

Complete Example

twig
<div class="product-detail">
    {# Images #}
    {% for image in product.images %}
        <img src="{{ image.path|resize(600, 600) }}" alt="{{ product.name }}" />
    {% endfor %}

    <h1>{{ product.name }}</h1>
    <p class="sku">SKU: {{ product.sku }}</p>

    {# Manufacturer #}
    {% if product.manufacturer %}
        <p>By <a href="#">{{ product.manufacturer.name }}</a></p>
    {% endif %}

    {# Pricing #}
    {% if product.is_on_sale %}
        <del>{{ product.final_price|currency }}</del>
        <strong>{{ product.final_sale_price|currency }}</strong>
    {% else %}
        <strong>{{ product.final_price|currency }}</strong>
    {% endif %}

    {# Tier pricing #}
    {% set visibleTiers = product.visible_price_tiers %}
    {% if visibleTiers is not empty %}
        <table class="tier-pricing">
            <tr><th>Quantity</th><th>Price</th></tr>
            <tr>
                <td>1+</td>
                <td>{{ product.final_price|currency }}</td>
            </tr>
            {% for tier in visibleTiers %}
                <tr>
                    <td>{{ tier.quantity_label }}</td>
                    <td>{{ tier.price|currency }}</td>
                </tr>
            {% endfor %}
        </table>
    {% endif %}

    {# Stock #}
    {% if product.track_inventory %}
        {% if product.isOutOfStock() %}
            {% if product.allow_pre_order %}
                <span class="badge">Pre-Order</span>
            {% else %}
                <span class="badge">Out of Stock</span>
            {% endif %}
        {% else %}
            <span>{{ product.units_in_stock }} in stock</span>
        {% endif %}
    {% endif %}

    {# Options #}
    {% for option in product.options %}
        <div class="form-group">
            <label>{{ option.name }}</label>
            <select name="product_options[{{ option.name }}]">
                {% for value in option.values %}
                    <option value="{{ value }}">{{ value }}</option>
                {% endfor %}
            </select>
        </div>
    {% endfor %}

    {# Extras #}
    {% for extra in product.all_extras %}
        <label>
            <input type="checkbox" name="product_extras[{{ extra.id }}]" />
            {{ extra.description }}
            {% if extra.final_price > 0 %}
                (+{{ extra.final_price|currency }})
            {% endif %}
        </label>
    {% endfor %}

    {# Properties / Specifications #}
    {% if product.properties is not empty %}
        <h3>Specifications</h3>
        <dl>
            {% for property in product.properties %}
                <dt>{{ property.name }}</dt>
                <dd>{{ property.value }}</dd>
            {% endfor %}
        </dl>
    {% endif %}

    {# Related products #}
    {% if product.related_products is not empty %}
        <h3>Related Products</h3>
        {% for related in product.related_products %}
            <a href="{{ related.pageUrl() }}">{{ related.name }}</a>
        {% endfor %}
    {% endif %}

    {# Reviews summary #}
    {% if product.reviews_count > 0 %}
        <div>
            {{ product.reviews_rating|number_format(1) }}/5
            ({{ product.reviews_count }} reviews)
        </div>
    {% endif %}

    {# Description #}
    <div class="description">{{ product.description|raw }}</div>
</div>

ProductOption

Options are selectable attributes like Size or Color. They determine which variant is selected when variants are enabled. Options do not add cost — use extras for priced add-ons.

Properties

PropertyTypeDescription
idintPrimary key
namestringOption name (e.g., "Color", "Size")
valuesarrayAvailable values (e.g., ["Red", "Blue", "Green"])
hashstringMD5 hash of name (used as form field key)
sort_orderintDisplay order
valuestring|nullCurrently selected value (set in cart/order context)

Displaying Options

twig
{# Product page — option selection #}
{% for option in product.options %}
    <div class="form-group">
        <label>{{ option.name }}</label>
        <select name="product_options[{{ option.name }}]">
            {% for value in option.values %}
                <option value="{{ value }}">{{ value }}</option>
            {% endfor %}
        </select>
    </div>
{% endfor %}

{# Cart/order context — selected value #}
{% for option in item.options %}
    <span>{{ option.name }}: {{ option.value }}</span>
{% endfor %}

INFO

When submitting to onAddToCart, options use product_options[OptionName] as the field name. The cart resolves the matching variant automatically.


ProductExtra

Extras are paid or free add-ons that customers can optionally select, such as gift wrapping, extended warranty, or engraving. Unlike options, extras have their own price and can affect weight and dimensions.

Properties

PropertyTypeDescription
idintPrimary key
descriptionstringExtra label/description
group_namestringGroup name for organizing extras
priceintRaw price in cents
original_priceintPrice without tax
final_priceintPrice with tax (if tax display is enabled)
weightfloatAdditional weight
widthfloatWidth
heightfloatHeight
depthfloatDepth
volumefloatComputed: width × height × depth
hashstringMD5 hash of description
sort_orderintDisplay order
imagesCollection<File>Extra images

Local vs Global Extras

  • Local extras (product.extras) — defined directly on a product.
  • Global extras — defined in an Extra Set and assigned to multiple products.
  • All extras (product.all_extras) — merges both into a single collection.

Always use product.all_extras to display extras on the storefront.

Displaying Extras

twig
{% for extra in product.all_extras %}
    <label>
        <input type="checkbox" name="product_extras[{{ extra.id }}]" />
        {% if extra.group_name %}
            <strong>{{ extra.group_name }}:</strong>
        {% endif %}
        {{ extra.description }}
        {% if extra.final_price > 0 %}
            (+{{ extra.final_price|currency }})
        {% else %}
            (Free)
        {% endif %}
    </label>
{% endfor %}

Extras in Cart/Order Context

When retrieved from a cart item or order item, extras have originalPrice and finalPrice properties set from the time of purchase:

twig
{% for extra in item.extras %}
    <span>+ {{ extra.description }}: {{ extra.finalPrice|currency }}</span>
{% endfor %}

ProductProperty

Properties are display-only specifications like Material, Dimensions, or Color. They have no effect on pricing or cart behavior — they are purely informational.

Properties

PropertyTypeDescription
idintPrimary key
namestringProperty name (e.g., "Material")
valuestringProperty value (e.g., "Cotton")
sort_orderintDisplay order

Displaying Properties

twig
{% if product.properties is not empty %}
    <table class="specifications">
        <tbody>
            {% for property in product.properties %}
                <tr>
                    <th>{{ property.name }}</th>
                    <td>{{ property.value }}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endif %}

ProductVariant

Variants are specific combinations of product options, each with its own SKU, price, stock, and dimensions. When a customer selects options (e.g., Color: Red, Size: Large), the system resolves the matching variant.

Properties

PropertyTypeDescription
idintPrimary key
namestringAuto-generated name (e.g., "Red / Large")
skustring|nullVariant-specific SKU (falls back to product)
priceint|nullOverride price (null uses product price)
costint|nullOverride cost price
compare_priceint|nullCompare-at price
weightfloat|nullOverride weight
widthfloat|nullOverride width
heightfloat|nullOverride height
depthfloat|nullOverride depth
units_in_stockint|nullVariant-specific stock
stock_alert_thresholdint|nullLow stock threshold
barcodestringBarcode/UPC
is_enabledboolWhether variant is available
is_defaultboolDefault variant selection

Relationships

PropertyTypeDescription
productProductParent product
variant_optionsCollection<ProductVariantOption>Selected option values
variant_pricesCollection<VariantPrice>Tier/group pricing overrides
imagesCollection<File>Variant-specific images (falls back to product images if empty)

Methods

MethodReturnsDescription
getCompiledPrice($qty, $groupId)intResolved price with tiers and user groups
getEffectiveWeight()floatVariant weight or product fallback
getEffectiveWidth()floatVariant width or product fallback
getEffectiveHeight()floatVariant height or product fallback
getEffectiveDepth()floatVariant depth or product fallback
getEffectiveSku()stringVariant SKU or product fallback
getEffectiveUnitsInStock()intVariant stock or product fallback
isOutOfStock()boolWhether variant is out of stock

Static Methods

MethodReturnsDescription
computeHash($options)stringGenerate hash from option array
findByOptions($product, $options)ProductVariant|nullFind variant by options

How Variant Resolution Works

When a customer submits product_options with onAddToCart, the cart component calls product.resolveVariant() to find the matching variant:

  1. The option values are normalized and hashed using computeHash().
  2. The hash is looked up in the variants table.
  3. If found and enabled, the variant's price, SKU, and stock are used.
  4. If not found, an exception is thrown.

For template use, resolveVariantSafe() is the preferred method — it returns null instead of throwing when the variant is not found or not available. This is useful when displaying price and availability on the product page while the customer is still selecting options:

twig
{# Resolve variant from posted options (safe — returns null on mismatch) #}
{% set postedOptions = post('product_options', {}) %}
{% set variant = product.resolveVariantSafe(postedOptions) %}

{# Variant-aware price display #}
{% if variant %}
    {{ variant.getCompiledPrice(1)|currency }}
{% else %}
    {{ product.final_sale_price|currency }}
{% endif %}

{# Variant-aware images (falls back to product images) #}
{% set images = (variant and variant.images is not empty) ? variant.images : product.images %}

{# Variant availability check #}
{% if variant and not variant.is_enabled %}
    <div class="alert">This combination is currently unavailable.</div>
{% elseif variant and variant.isOutOfStock() %}
    <div class="alert">Out of stock.</div>
{% endif %}
twig
{# Variant info in cart/order context #}
{% if item.variant %}
    <p>{{ item.variant.name }}</p>
    <p>SKU: {{ item.variant.getEffectiveSku() }}</p>
{% endif %}

PriceTier

Price tiers provide volume-based pricing. When a customer adds a quantity that meets or exceeds a tier threshold, the tier price is used instead of the base price.

Properties

PropertyTypeDescription
idintPrimary key
quantityintMinimum quantity for this tier
priceintPrice at this tier (in cents)
quantity_labelstringFormatted label: "X or more"
user_group_labelstringUser group name or "Any User"

Relationships

PropertyTypeDescription
user_groupUserGroup|nullRestrict tier to a user group

Displaying Tier Pricing

Use visible_price_tiers to display tiers appropriate for the current user. This attribute automatically shows user-group-specific tiers when available, falling back to generic tiers otherwise.

twig
{% set visibleTiers = product.visible_price_tiers %}
{% if visibleTiers is not empty %}
    <h4>Volume Pricing</h4>
    <table>
        <thead>
            <tr><th>Quantity</th><th>Unit Price</th></tr>
        </thead>
        <tbody>
            <tr>
                <td>1+</td>
                <td>{{ product.final_price|currency }}</td>
            </tr>
            {% for tier in visibleTiers %}
                <tr>
                    <td>{{ tier.quantity_label }}</td>
                    <td>{{ tier.price|currency }}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endif %}

ProductBundleItem

Bundle items represent slots within a bundle product. Each slot offers multiple product choices for the customer to select from.

Properties

PropertyTypeDescription
idintPrimary key
namestringSlot name (e.g., "Processor", "Memory")
descriptionstringSlot description
control_typestringUI control: dropdown, radio, or checkbox
is_requiredboolWhether a selection is mandatory
sort_orderintDisplay order

Relationships

PropertyTypeDescription
productProductParent bundle product
item_productsCollection<BundleItemProduct>Available product choices

BundleItemProduct

Each product choice within a bundle slot, with optional price overrides.

Properties

PropertyTypeDescription
idintPrimary key
productProductThe product being offered
default_quantityintDefault quantity when selected
allow_manual_quantityboolLet customer change quantity
is_defaultboolPre-selected by default
is_activeboolVisible to customers
price_override_modestringPrice mode: default, fixed, fixed-discount, percentage-discount
price_or_discountintAmount for the price mode (in cents)
sort_orderintDisplay order

Methods

MethodReturnsDescription
getEffectivePrice()intCalculated price based on override mode

Displaying Bundle Items

twig
{% for slot in product.bundle_items %}
    <div class="bundle-slot">
        <h4>{{ slot.name }}{% if slot.is_required %} *{% endif %}</h4>
        {% if slot.description %}
            <p>{{ slot.description }}</p>
        {% endif %}

        {% if slot.control_type == 'dropdown' %}
            <select name="bundle_items[{{ slot.id }}]"
                    {% if slot.is_required %}required{% endif %}>
                <option value="">— Select —</option>
                {% for choice in slot.item_products %}
                    {% if choice.is_active %}
                        <option value="{{ choice.product.id }}"
                                {% if choice.is_default %}selected{% endif %}>
                            {{ choice.product.name }}
                            ({{ choice.getEffectivePrice()|currency }})
                        </option>
                    {% endif %}
                {% endfor %}
            </select>

        {% elseif slot.control_type == 'radio' %}
            {% for choice in slot.item_products %}
                {% if choice.is_active %}
                    <label>
                        <input type="radio" name="bundle_items[{{ slot.id }}]"
                               value="{{ choice.product.id }}"
                               {% if choice.is_default %}checked{% endif %} />
                        {{ choice.product.name }}
                        ({{ choice.getEffectivePrice()|currency }})
                    </label>
                {% endif %}
            {% endfor %}

        {% elseif slot.control_type == 'checkbox' %}
            {% for choice in slot.item_products %}
                {% if choice.is_active %}
                    <label>
                        <input type="checkbox" name="bundle_items[{{ slot.id }}][]"
                               value="{{ choice.product.id }}"
                               {% if choice.is_default %}checked{% endif %} />
                        {{ choice.product.name }}
                        ({{ choice.getEffectivePrice()|currency }})
                    </label>
                {% endif %}
            {% endfor %}
        {% endif %}
    </div>
{% endfor %}

ProductType

Product types control which features and tabs are available on the product form. They are primarily a backend concept, but can be useful in templates to conditionally render features.

Properties

PropertyTypeDescription
idintPrimary key
namestringType name
codestringAPI code
has_filesboolSupports downloadable files
has_shippingboolRequires shipping
has_inventoryboolTracks inventory
has_optionsboolSupports options
has_extrasboolSupports extras
has_bundlesboolSupports bundles
has_variantsboolSupports variants
is_defaultboolDefault type for new products

Using in Templates

twig
{% if product.product_type.has_shipping %}
    <p>Ships within 3-5 business days</p>
{% else %}
    <p>Digital delivery — instant access after purchase</p>
{% endif %}

{% if product.product_type.has_files %}
    <p>Includes downloadable files</p>
{% endif %}