Extending comOrderItem / adding custom products to cart

Hey there,

The store I’m building will allow some personalisation of products (think Moonpig greetings cards) so I want to extend the order items with name, message, card design etc. I’m assuming these can be added to the properties in the comSimpleObject, but is there a way to extend the comOrderItem itself so each bit of information can be stored in a table field?

Thanks!

Hey Chris,

Indeed you can add those to the properties of you use a custom add to cart script for which I can get you a sample.

Coming up shortly in 0.11 is a way for custom product types to display additional info on the order view, so if you’d couple the order item properties with a custom product, you can bridge the gap with the merchant that way.

What’s your goal behind wanting to store the data in different fields?

Hey Mark,

One of the fields will be used to store the occasion, such as Easter, Christmas etc. It’s likely we’ll want to run some custom reporting based on this field, so it makes sense to store these separately, rather than having to search through the properties text field.

As well as storing the data, we’d need to be able to access it in the order notification emails and in the manager when viewing an order. Sound possible?

Hey Mark,

Is there any chance you could send me over that custom add to cart script please?
Digging into the code, I can see that the order detail view is created using ItemGrid.php - is there a reason for this not being created with Twig? Would be awesome to be able to alter/extend this view or is that a 0.11 enhancement?

Cheers!

Was looking for this code just now, so figured I’d post it here even though it’s a rather old topic :wink: Sorry for not getting back to you before.

First, load up Commerce and the current order

<?php
use modmore\Commerce\Frontend\Checkout\Standard;
use modmore\Commerce\Frontend\Steps\Cart;

// Instantiate the Commerce class
$path = $modx->getOption('commerce.core_path', null, MODX_CORE_PATH . 'components/commerce/') . 'model/commerce/';
$params = ['mode' => $modx->getOption('commerce.mode')];
/** @var Commerce|null $commerce */
$commerce = $modx->getService('commerce', 'Commerce', $path, $params);
if (!($commerce instanceof Commerce)) {
    return '<p class="error">Oops! It is not possible to view your cart currently. We\'re sorry for the inconvenience. Please try again later.</p>';
}

if ($commerce->isDisabled()) {
    return $commerce->adapter->lexicon('commerce.mode.disabled.message');
}

$order = \comOrder::loadUserOrder($commerce);

Following that, there’s a few things we can do with the order.

Option 1: Add a product by its ID to the cart; this is exactly the same as a regular add to cart request:

$productId = 1234; // Change this ;)
$process = new Standard($commerce, $order);
$process->currentKey = 'cart';
$cartStep = new Cart($process, []);
$cartStep->setOrder($order);
$cartStep->addProductToCart($productId, ['quantity' => 1]);

// Redirect to the cart or something

Option 2: create an order item based on an existing product, but tweak our item as needed

$product = $commerce->adapter->getObject('comProduct', ['id' => 1234]);
if (!($product instanceof comProduct)) {
   return 'Could not find product';
}
$item = $commerce->adapter->newObject('comOrderItem'); 
// This will set the product ID, sku, name, link, description, price,
// image, delivery_type and tax_group for you 
$item->fromProduct($product); 

// Add extra info into the properties (note, not indexed/searchable)
$item->setProperty('occasion', 'Easter'); 

// Or change an item, like set a different name and price:
$item->set('name', 'Product Deluxe');
$item->set('price', $item->get('price') + 2500);
// IMPORTANT: if you change the price while the item is linked to a
// product (through `fromProduct` or setting `product`), you need to
// mark the price as manual to prevent Commerce automatically 
// reverting it to the normal product price: 
$item->set('is_manual_price', true);

// Add to order
$order->addItem($item);

Option 3: create an order item without a product reference:

$item = $commerce->adapter->newObject('comOrderItem'); 
$item->set('currency', $order->get('currency')); 
$item->fromArray([
  'delivery_type' => $deliveryTypeId, 
  'tax_group' => $taxGroupId, 
  'sku' => 'Product Code',
  'name' => 'Name',
  'description' => '...', 
  'price' => 1334, // in cents!!
  'quantity' => 1,
]);
$order->addItem($item);

This logic deals with adding custom order items to the cart. You can add properties (as shown in the option 2 example) which will be automatically available in emails in the item.properties array.

When using the ItemData module you can also get arbitrary data to be stored with an order item, without using a custom snippet like this, and that also has the benefit of showing the information in the backend and cart automatically as well. If you want to achieve the same with custom add to cart logic, you’ll want to take a peek into core/components/commerce/src/Modules/Cart/ItemData.php to see how it’s doing that.

Important note for anyone manually creating comOrderItem objects based on the above code with custom prices + a product: Commerce will start checking if product prices are still up-to-date in 1.0.

The change has to do with the introduction of price types and unfortunately may have a breaking effect on certain integrations.

This will affect order items:

  • with the product set to a valid product ID
  • with a price that does not match the stored product price

If those 2 conditions are met, the prices will start to revert back to the standard price for the product after the update. (If you don’t set a product, or do not set a different price, this does not affect you.)

To fix that, you need to mark your order item as having a manual price:

$item->set('is_manual_price', true);

It’s possible to include that line already (before the upgrade is available, which is still a few weeks out), as xPDO discards fields that are not in the schema.

A new interesting addition coming in 1.2.0-rc3 (< 2 weeks) is the ability to add extra fees to an item that are added into the subtotal (before discount/tax calculations) through Item Price Adjustments.

Price adjustments aren’t entirely new; they already existed for discounts. That’s now been extended for price increases as well with a new comOrderItemExtraAdjustment class.

This is especially useful for things like configurators where the customer can select options which (through a custom add to cart snippet as this topic discusses) are added to an item. Also things like mandatory per-item insurance could be done with this. One client use case that triggered it was being able of charging an extra fee for gift wrapping specific items.

There’s 3 options. A per-item extra fee, a per-item-quantity fee, or a percentage fee. Given a comOrderItem $item object, here’s some example code on how that would work:

    // Adding a fixed fee only applied once, regardless of quantity
    $fixed = $this->adapter->newObject('comOrderItemExtraAdjustment');
    $fixed->fromArray([
        'key' => 'some-fixed-fee',
        'name' => 'Some fixed fee',
        'price_change' => 250,
        'price_change_per_quantity' => false,
        'show_on_order' => true,
    ]);
    $item->addPriceAdjustment($fixed);

    // Adding a fixed fee, once per item quantity
    $perItem = $this->adapter->newObject('comOrderItemExtraAdjustment');
    $perItem->fromArray([
        'key' => 'adj-per-quantity',
        'name' => 'Gift wrapping (cost per item)',
        'price_change' => 150,
        'price_change_per_quantity' => true,
        'show_on_order' => true,
    ]);
    $item->addPriceAdjustment($perItem);

    // Adding a percentage fee
    $percentage = $this->adapter->newObject('comOrderItemExtraAdjustment');
    $percentage->fromArray([
        'key' => 'adj-percentage',
        'name' => 'Insurance',
        'price_change_percentage' => 2.25,
        'show_on_order' => true,
    ]);
    $item->addPriceAdjustment($percentage);

Setting show_for_order makes sure you can access it in the front-end template through item.adjustments.

The default frontend/checkout/cart/items.twig template has been adjusted with the following which can serve as an example of how to show these types of extra fees. First it filters it only on extra adjustments (so discounts, i.e. coupons, are not shown here - discounts are rendered differently, typically) and then it shows the name of the adjustment and the total impact it has on the item price.

{% set adjustments = item.adjustments|filter(v => v.type == 'extra') %}
{% if adjustments|length > 0 %}
    {% for adjustment in adjustments %}
        <br>
        &plus;&nbsp;{{ adjustment.name }}
        ({{ adjustment.total_change_formatted }})
    {% endfor %}
{% endif %}

Hi there,
interesting updates :slight_smile:

Can I just check the recommended options to start with:

  • duplicate the Cart snippet for the one of the customisation above and use it on the “cart page”
    (once upgraded the Cart2 snippet will not be overwritten and also upgraded manually just like modified)

  • add the product add to cart form into FormIt snippet for using the preHooks, so that after all preHooks worked then Commerce would load the normal Cart snippet with already triggered custom code

Please suggest, thanks

The code samples in this topic are meant as a standalone snippet, usually placed on the product page. The cart snippet isn’t changed or replaced - this happens before that.

I’m not sure what use case you have in mind, but if you can explain that (in new topic perhaps), we’re happy to offer suggestions on how to accomplish that. The code in this topic could be one of the options but perhaps you need something completely different.

Hey Mark,
OK, thanks.

If we are assuming to place a snippet like this one on a product page template then what trigger should be used to execute that code for adding to the Cart:

  • either a custom additional product or
  • add this product but with a modified properties like extra depending on HTML selector

PS: I have noticed the default add to cart action is landing a customer on the Cart page but I believe this can be changed to keep a user on the same product page, right?

Hope it makes sense