Table of content

Magento 2 Certified Professional Developer Guide


Section 8: Customizing the Checkout Process

8.1 Demonstrate ability to use quote, quote item, address, and shopping cart rules in checkout

Quote contains data for creating an order. This data is temporary and can be modified by the user. When the order is created, the user can no longer change this data in it.

Magento uses Quote for the following purposes:

  • Store products in the cart along with their cost, quantity and options
  • Store selected billing address and shipping address
  • Store shipping costs
  • Store subtotals of prices, additional prices (shipping costs, taxes, etc.) and coupons application to determine the total price
  • Store selected payment method

The following tables are aimed at storing Quote in the database:

  • quote
  • quote_address
  • quote_address_item
  • quote_id_mask
  • quote_item
  • quote_item_option
  • quote_payment
  • quote_shipping_rate

Quote is the responsibility of the Magento\Quote\Model\Quote model. The Magento\Quote\Model\Quote\Address model is responsible for Quote Address. Quote usually has 2 addresses (billing, shipping), but may contain more if there are several delivery addresses or none at all. If quote does not have an address, then Totals will not take into account Price Rules associated with the country. If the quote contains virtual products only, then the delivery address is not taken into account and checked using the isVirtual () method of the Magento\Quote\Model\Quote class for quote with virtual products, only the billing address is taken into account. For products in quote, the Magento\Quote\Model\Quote\Item model is responsible. For payment in quote, the Magento\Quote\Model\Quote\Payment model is responsible.

Rules in checkout

With Cart Price Rules, we can modify the final price at the checkout and the shipping costs. It is also possible to configure rules via the admin panel. There is a flexible set of conditions, under which the rules apply. We can add a coupon for applying the discount and indicate its percentage. Also, we can apply a discount or include free shipping to the cart that meets certain conditions. We can see all the settings in the Marketing-> Cart Price Rules-> Add New Rule section. As conditions, we can apply:

  • Product attribute combination
  • Products subselection
  • Conditions combination
  • Subtotal
  • Total Items Quantity
  • Total Weight
  • Payment Method
  • Shipping Method
  • Shipping Postcode
  • Shipping Region
  • Shipping State/Province
  • Shipping Country

The “Magento \ SalesRule \ Model \ Rule” model is responsible for the cart rules, and with it we can programmatically add or get a specific cart rule.

The more there are cart rules without coupons for the current website and for the current customer group, the slower is the processing of cart rules. It is not recommended to have a lot of such cart rules.

Interesting quote fields or methods:

If the “trigger_recollect” flag is set, the “quote” will also be updated when the price changes or the product is set to “disabled” status. The same happens if we change the rules of the CatalogRules catalog, for we can find these methods in the class Magento \ Quote \ Model \ ResourceModel markQuotesRecollectOnCatalogRules () and markQuotesRecollect ().

Quote is also extendable through Magento \ Framework \ Model \ AbstractExtensibleModel, for it supports “extension_attributes”. You can register the extension through the extension_attributes.xml file:

<extension_attributes for="Magento\Quote\Api\Data\CartInterface">
<attribute code="shipping_assignments"                type="Magento\Quote\Api\Data\ShippingAssignmentInterface[]" />
</extension_attributes>

Use “Quote Repository Plugin” to fill in the values with the afterLoad (), beforeSave (), or whenever () functions. Quote does not use “custom_attributes” since they are not EAVs.

Quote address custom attributes:
  • “CE” \Magento\Quote\Model\Quote\Address\CustomAttributeList getAttributes() return empty array. To implement it, we need to write a plugin.
  • “EE” \Magento\CustomerCustomAttributes\Model\Quote\Address\CustomAttributeList::getAttributes return “customer address attributes” + “customer attributes”
Quote item useful methods:
  • Magento\Quote\Model\Quote\Item checkData() is called after adding to cart and updating options
    • Magento\Quote\Model\Quote\Item setQty() – triggers stock validation
    • Magento\Catalog\Model\Product\Type\AbstractType checkProductBuyState() – check if the product can be bought
  • Magento\Quote\Model\Quote\Item setCustomPrice()
  • Magento\Quote\Model\Quote\Item getCalculationPrice() gets original custom price applied before tax calculation
  • Magento\Quote\Model\Quote\Item isChildrenCalculated() checks if there are children calculated or parent item when we have parent quote item and its children
  • Magento\Quote\Model\Quote\Item isShipSeparately() – Checking if we can ship products separately (each child separately) or each parent product item can be shipped only like one item
  • Magento\Quote\Model\Quote\Item\Compare::compare merges items and adds quantity instead of a new item
  • Magento\Quote\Model\Quote\Item representProduct() – compares quote item with some new product, checks product id and custom options
  • Magento\Quote\Model\Quote\Item compareOptions() – check if two options array are identical. First options array is prerogative, and second options array checked compared to the first one.

Describe how to modify these models and effectively use them in customizations

Inventory validation
  • Use Magento\Quote\Model\Quote\Item setQty() function to declare quote item quantity
  • Use event “sales_quote_item_qty_set_after”
  • Use function \Magento\CatalogInventory\Model\Quote\Item\QuantityValidator::validate to check product inventory data when quote item quantity declaring.
Add to cart
  • Use function Magento\Quote\Model\Quote addProduct() to add a product to the shopping cart Returns error message if product type instance can’t prepare product.
  • Use function Magento\Catalog\Model\Product\Type\AbstractType prepareForCartAdvanced() to initialize product(s) to add to cart process. The advanced version of function that prepares product for cart (processMode) can be specified there.
  • Use function Magento\Quote\Model\Quote\Item\Processor::prepare to set quantity and custom price for quote item
  • Use event “sales_quote_product_add_after”
  • Use event “sales_quote_save_after”, “sales_quote_save_before”
  • Use event “checkout_cart_add_product_complete”
Cart update
  • Use function \Magento\Quote\Model\Quote updateItemUpdate() quote item information
  • Use event “sales_quote_save_after”, “sales_quote_save_before”

Describe how to customize the process of adding a product to the cart

  • Applying a plugin to the Magento\Catalog\Model\Product\Type\AbstractType prepareForCartAdvanced() method. This function is used to prepare the product for adding to the cart (that is, the “Quote” object) and is called from the Magento \ Quote \ Model \ Quote addProduct () method.
  • Applying a plugin to the Magento\Quote\Model\Quote::addProduct method
  • Applying a plugin to the Magento\Quote\Model\Quote::addItem method
  • With the help of “catalog_product_type_prepare_full_options” event at the condition of full validation, or “catalog_product_type_prepare_lite_options” at the condition of partial validation, the event is launched right before the product with configurable options is configured into “Quote” element.
  • Applying a plugin to the \Magento\Quote\Model\Quote\Item\Processor::prepare method, we can modify the number of products and their custom prices.
  • With the help of “sales_quote_product_add_after” event, we can modify attribute values or product price.
  • Using the “sales_quote_add_item” event.
  • In the catalog_attributes.xml file in <group name=”quote_item”> group, we describe the product attributes that get into “quote”; for example, <attribute name=”sku”/>.

Which different scenarios should you take into account?

  • Adding into the shopping cart from the catalog
  • Adding into the shopping cart from the wishlist
  • Adding all items from the wishlist into the shopping cart
  • Create an order from the admin / user part of the website
  • “Reorder” from the admin / user part of the website
  • Configuration of the added product is modification of custom options
  • “Quotes” merge at the client authorization in case he or she added products as a guest and already have a “Quote”

8.2 Demonstrate ability to use totals models

Describe how to modify the price calculation process in the shopping cart. How can you add a custom totals model or modify existing totals models?

Custom totals can be used to add an additional tax or discount in Magento Checkout or modify the existing ones.

First, we need to create the <module_dir>/etc/sales.xml file in our module. This file is applied for registration of all the available Magento totals.

<module_dir>/etc/sales.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd">
    <section name="quote">
        <group name="totals">
            <item name="custom_total" instance="Vendor\Module\Model\Totals\Custom" sort_order="500"/>
        </group>
    </section>
</config>

Here, we set \Vendor\Module\Model\Totals\Custom class as the model of our custom total. This class should inherit \Magento\Quote\Model\Quote\Address\Total\AbstractTotal and realize methods collect and fetch. Collect method is applied to calculate our total, while fetch method returns the value together with total’s code and its name.

Also, \Magento\Quote\Model\Quote\Address\Total $total parameter allows you to affect the result of the other total classes. But, depending on the task, it may be reasonable to use plugins to modify their values.

<module_dir>/Model/Total/Custom.php

<?php
Namespace Vendor\Module\Model\Total;

class Custom extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{
    /**
     * Custom constructor.
     */
    public function __construct()
    {
        $this->setCode('custom_total');
    }

    /**
     * @param \Magento\Quote\Model\Quote $quote
     * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     * @return $this
     */
    public function collect(
        \Magento\Quote\Model\Quote $quote,
        \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment,
        \Magento\Quote\Model\Quote\Address\Total $total
    ) {
        parent::collect($quote, $shippingAssignment, $total);

        $items = $shippingAssignment->getItems();
        if (!count($items)) {
            return $this;
        }

        //we will add an additional amount of 150 to the order as an example
        $amount = 150;

        $total->setTotalAmount('custom_total', $amount);
        $total->setBaseTotalAmount('custom_total', $amount);
        $total->setCustomAmount($amount);
        $total->setBaseCustomAmount($amount);
        $total->setGrandTotal($total->getGrandTotal() + $amount);
        $total->setBaseGrandTotal($total->getBaseGrandTotal() + $amount);

        return $this;
    }

    /**
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     */
    protected function clearValues(\Magento\Quote\Model\Quote\Address\Total  $total)
    {
        $total->setTotalAmount('subtotal', 0);
        $total->setBaseTotalAmount('subtotal', 0);
        $total->setTotalAmount('tax', 0);
        $total->setBaseTotalAmount('tax', 0);
        $total->setTotalAmount('discount_tax_compensation', 0);
        $total->setBaseTotalAmount('discount_tax_compensation', 0);
        $total->setTotalAmount('shipping_discount_tax_compensation', 0);
        $total->setBaseTotalAmount('shipping_discount_tax_compensation', 0);
        $total->setSubtotalInclTax(0);
        $total->setBaseSubtotalInclTax(0);
    }

    /**
     * @param \Magento\Quote\Model\Quote $quote
     * @param \Magento\Quote\Model\Quote\Address\Total $total
     * @return array
     */
    public function fetch(
        \Magento\Quote\Model\Quote  $quote,
         \Magento\Quote\Model\Quote\Address\Total $total
    ) {
        return [
            'code' => $this->getCode(),
            'title' => 'Custom Total’,
            'value' => 150
        ];
    }

    /**
     * @return \Magento\Framework\Phrase
     */
    public function getLabel()
    {
        return __('Custom Total’);
    }
}

Displaying Custom Total

Cart and checkout pages

We use knockout.js to display our totals. Therefore, to make our total appear at the cart and checkout pages, we need to add a new JS component into checkout_cart_index.xml and checkout_index_index.xml layouts.

<module_dir>/view/frontend/layout/checkout_cart_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.cart.totals">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="block-totals" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="custom_total" xsi:type="array">
                                    <item name="component" xsi:type="string">Vendor_Module/js/view/checkout/cart/totals/custom_total</item>
                                    <item name="sortOrder" xsi:type="string">20</item>
                                    <item name="config" xsi:type="array">
                                        <item name="template" xsi:type="string">Vendor_Module/checkout/cart/totals/custom_total</item>
                                        <item name="title" xsi:type="string">Custom Total</item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

<module_dir>/view/frontend/layout/checkout_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.root">
            <arguments>
                <argument name="jsLayout" xsi:type="array">
                    <item name="components" xsi:type="array">
                        <item name="checkout" xsi:type="array">
                            <item name="children" xsi:type="array">
                                <item name="sidebar" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="summary" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="totals" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="custom_total" xsi:type="array">
                                                            <item name="component"  xsi:type="string">Vendor_Module/js/view/checkout/cart/totals/custom_total</item>
                                                            <item name="sortOrder" xsi:type="string">20</item>
                                                            <item name="config" xsi:type="array">
                                                                <item name="template" xsi:type="string">Vendor_Module/checkout/cart/totals/custom_total</item>
                                                                <item name="title" xsi:type="string">Custom Total</item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                                <item name="cart_items" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="details" xsi:type="array">
                                                            <item name="children" xsi:type="array">
                                                                <item name="subtotal" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Magento_Tax/js/view/checkout/summary/item/details/subtotal</item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

We can also create the components themselves, together with HTML templates:

<module_dir>/view/frontend/web/js/view/checkout/cart/totals/custom_total.js

define(
    [
        'Vendor_Module/js/view/checkout/summary/custom_total'
    ],
    function (Component) {
        'use strict';

        return Component.extend({

            /**
             * @override
             */
            isDisplayed: function () {
                return this.getPureValue() !== 0;
            }
        });
    }
);

<module_dir>/view/frontend/web/template/checkout/cart/totals/custom_total.html

<!-- ko if: isDisplayed() -->
<tr class="totals custom_total excl">

    <th class="mark" colspan="1" scope="row" data-bind="text: title"></th>
    <td class="amount">
        <span class="price" data-bind="text: getValue()"></span>
    </td>
</tr>
<!-- /ko →

<module_dir>/view/frontend/web/js/view/checkout/summary/custom_total.js

define(
    [
        'Magento_Checkout/js/view/summary/abstract-total',
        'Magento_Checkout/js/model/quote',
        'Magento_Catalog/js/price-utils',
        'Magento_Checkout/js/model/totals'
    ],
    function (Component, quote, priceUtils, totals) {
        "use strict";
        return Component.extend({
            defaults: {
                isFullTaxSummaryDisplayed: window.checkoutConfig.isFullTaxSummaryDisplayed || false,
                template: 'Vendor_Module/checkout/summary/custom_total'
            },
            totals: quote.getTotals(),
            isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false,

            isDisplayed: function() {
                return this.isFullMode() && this.getPureValue() !== 0;
            },

            getValue: function() {
                var price = 0;
                if (this.totals()) {
                    price = totals.getSegment('custom_total').value;
                }
                return this.getFormattedPrice(price);
            },
            getPureValue: function() {
                var price = 0;
                if (this.totals()) {
                    price = totals.getSegment('custom_total').value;
                }
                return price;
            }
        });
    }
);

getValue and getPureValue methods return the values of our custom total, but getValue method formats the value, adding two decimal digits and the actual currency symbol.

<module_dir>/view/frontend/web/template/checkout/summary/custom_total.html

<!-- ko if: isDisplayed() -->
<tr class="totals custom_total excl">
    <th class="mark" scope="row">
        <span class="label" data-bind="text: title"></span>
        <span class="value" data-bind="text: getValue()"></span>
    </th>
    <td class="amount">
        <span class="price" data-bind="text: getValue(), attr: {'data-th': title}"></span>
    </td>
</tr>
<!-- /ko -->
Order Emails

To add a new total display into order email, add a new block in sales_email_order_items layout.

<module_dir>/view/frontend/layout/sales_email_order_items.xml

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
   <body>
       <referenceBlock name="order_totals">
           <block class="Vendor\Module\Block\Order\CustomTotal" name="order.totals.custom" />
       </referenceBlock>
   </body>
</page>

<module_dir>/Block/Order/CustomTotal.php

<?php
namespace Vendor\Module\Block\Order;
class CustomTotal extends \Magento\Framework\View\Element\AbstractBlock
{
   public function initTotals()
   {
       $orderTotalsBlock = $this->getParentBlock();
       $order = $orderTotalsBlock->getOrder();
       if ($order->getCustomAmount() > 0) {
           $orderTotalsBlock->addTotal(new \Magento\Framework\DataObject([
               'code'       => 'custom_total',
               'label'      => __('Custom Total'),
               'value'      => $order->getCustomAmount(),
               'base_value' => $order->getCustomBaseAmount(),
           ]), 'subtotal');
       }
   }
}

8.3 Demonstrate ability to customize the shopping cart

Describe how to implement shopping cart rules. What is the difference between sales rules and catalog rules?

Cart rules in Magento 2 is a system to set discounts and promotions. Compared to catalog rules, cart rules are applied to the current user’s shopping cart, not to each separate product; they may also require that a certain coupon code is entered to get them. Due to this, the discounts are not visible at the catalog pages, but still allow to apply different factors for getting them, like the number of products in the customer’s shopping cart, their total price, the categories they belong to, and much more.

Creating cart rules

Commonly, cart rules are created from the admin panel, but they can also be created programmatically.

Magento 2 Certified Professional Developer Guide Screenshot 78

Log in to the admin panel and navigate to Marketing -> Cart Price Rules. Press  “Add New Rule” button and select the rule name, relationship with store and customer groups, conditions for discount and the coupon relevance.

Creating new cart rule programmatically

To create a shipping cart rule programmatically, you have to inject \Magento\SalesRule\Model\RuleFactory class:

<?php
....

protected $ruleFactory

public function __construct(
        \Magento\SalesRule\Model\RuleFactory $ruleFactory
    ) {
        $this->rulesFactory = $ruleFactory
    }

And then use it like this:

<?php
…

$ruleData = [
            "name" => "Custom Cart Rule",
            "description" => "Buy some products and get one more free",
            "from_date" => null,
            "to_date" => null,
            "uses_per_customer" => "0",
            "is_active" => "1",
            "stop_rules_processing" => "0",
            "is_advanced" => "1",
            "product_ids" => null,
            "sort_order" => "0",
            "simple_action" => "buy_x_get_y",
            "discount_amount" => "1.0000",
            "discount_qty" = >null,
            "discount_step" => "3",
            "apply_to_shipping" => "0",
            "times_used" => "0",
            "is_rss" => "1",
            "coupon_type" => "NO_COUPON",
            "use_auto_generation" => "0",
            "uses_per_coupon" => "0",
            "simple_free_shipping" => "0",
            "customer_group_ids" => [0, 1, 2, 3],
            "website_ids" => [1],
            "coupon_code" => null,
            "store_labels" => [],
            "conditions_serialized" => '',
            "actions_serialized" => ''
        ];

$ruleModel = $this->ruleFactory->create();
$ruleModel->setData($ruleData);
$ruleModel->save();
Creating conditions and actions for the rule

For the sake of demonstration, let us create the condition that sets a discount for a certain product, if customer has added more than five items of this product.

<?php
....

protected $ruleFactory

protected $productFoundConditionFactory;

protected $productConditionFactory;

public function __construct(
        \Magento\SalesRule\Model\RuleFactory $ruleFactory,
        \Magento\SalesRule\Model\Rule\Condition\Product\FoundFactory $productFoundConditionFactory,
        \Magento\SalesRule\Model\Rule\Condition\ProductFactory $productConditionFactory,
    ) {
        $this->rulesFactory = $ruleFactory;
        $this->productFoundConditionFactory = $productFoundConditionFactory;
        $this->productConditionFactory = $productConditionFactory;
    }

…

$discount = ‘25’;
$sku = ‘PRODUCT_SKU’;

$shoppingCartPriceRule = $this->rulesFactory->create();
$shoppingCartPriceRule->setName('25% off with multiple products - ' . $sku)
   ->setDescription('Get 25% off with two or more products)
   ->setFromDate('2000-01-01')
   ->setToDate(NULL)
   ->setUsesPerCustomer('0')
   ->setCustomerGroupIds(array('0','1','2','3',))
   ->setIsActive('1')
   ->setStopRulesProcessing('0')
   ->setIsAdvanced('1')
   ->setProductIds(NULL)
   ->setSortOrder('1')
   ->setSimpleAction(‘by_percent’)
   ->setDiscountAmount($discount)
   ->setDiscountQty(NULL)
   ->setDiscountStep('0')
   ->setSimpleFreeShipping('0')
   ->setApplyToShipping('0')
   ->setTimesUsed('0')
   ->setIsRss('0')
   ->setWebsiteIds(array('1',))
   ->setCouponType('1')
   ->setCouponCode(NULL)
   ->setUsesPerCoupon(NULL);

$productFoundCondition = $this->productFoundConditionFactory->create()
   ->setType('Magento\SalesRule\Model\Rule\Condition\Product\Found')
   ->setValue(1) // 1 == FOUND
   ->setAggregator('all'); // match ALL conditions

$productCondition = $this->productConditionFactory->create()
   ->setType('Magento\SalesRule\Model\Rule\Condition\Product')
   ->setAttribute('sku')
   ->setOperator('==')
   ->setValue($sku);
$productFoundCondition->addCondition($productCondition);

$shoppingCartPriceRule->getConditions()->addCondition($productFoundCondition);

$skuCondition = $this->productConditionFactory->create()
   ->setType('Magento\SalesRule\Model\Rule\Condition\Product')
   ->setAttribute('sku')
   ->setOperator('==')
   ->setValue($sku);
$shoppingCartPriceRule->getActions()->addCondition($skuCondition);

$qtyCondition = $this->productConditionFactory->create()
   ->setType('Magento\SalesRule\Model\Rule\Condition\Product')
   ->setAttribute('quote_item_qty')
   ->setOperator('>=')
   ->setValue('2');
$shoppingCartPriceRule->getActions()->addCondition($qtyCondition);

$shoppingCartPriceRule->save();

What are the limitations of the native sales rules engine? How do sales rules affect performance?

For each customer, there can be only one active coupon. Rules cannot add other products to the cart and apply to the item they were assigned to work with.

Cart rules can slow down the process of adding a product to the cart, as well as checkout and shopping cart pages speed. Performance impact from cart rules can increase in case there is a large number of cart rules with a wide area of application (without connection to website or customer group) and without coupons.

Describe add-to-cart logic in different scenarios. What is the difference in adding a product to the cart from the product page, from the wishlist, by clicking Reorder, and during quotes merge?

  • Adding from the product page:
    • Action: \Magento\Checkout\Controller\Cart\Add
    • Available events: checkout_cart_add_product_complete, checkout_cart_product_add_after, sales_quote_product_add_After, sales_quote_add_item
    • Based on the passed product and proper amount of cart model, the corresponding quote item is generated.
  •  Adding from the wishlist:
    • Action: \Magento\Wishlist\Controller\Index\Cart
    • Available events: checkout_cart_product_add_after, sales_quote_product_add_after, sales_quote_add_item
  •  Reorder:
    • Action: \Magento\Sales\Controller\AbstractController\Reorder
    • Available events: checkout_cart_product_add_after, sales_quote_product_add_after, sales_quote_add_item
  • Quote merge:
    • Method: \Magento\Quote\Model\Quote::merge
    • Available events: sales_quote_add_item

Describe the difference in behavior of different product types in the shopping cart. How are configurable and bundle products rendered?

Magento 2 uses renderers to display products in the shopping cart. Each product type (e.g. simple, configurable, bundle) has a renderer; they are registered in the renderer list with layout instructions as child blocks of the \Magento\Framework\View\Element\RendererList block under the name “checkout.cart.item.renderers”.

To the example’s sake, let us examine a renderer for configurable products:

vendor/magento/module-configurable-product/view/frontend/layout/checkout_cart_item_renderers.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.cart.item.renderers">
            <block class="Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable" as="configurable" template="Magento_Checkout::cart/item/default.phtml">
                <block class="Magento\Checkout\Block\Cart\Item\Renderer\Actions" name="checkout.cart.item.renderers.configurable.actions" as="actions">
                    <block class="Magento\Checkout\Block\Cart\Item\Renderer\Actions\Edit" name="checkout.cart.item.renderers.configurable.actions.edit" template="Magento_Checkout::cart/item/renderer/actions/edit.phtml"/>
                    <block class="Magento\Checkout\Block\Cart\Item\Renderer\Actions\Remove" name="checkout.cart.item.renderers.configurable.actions.remove" template="Magento_Checkout::cart/item/renderer/actions/remove.phtml"/>
                </block>
            </block>
        </referenceBlock>
    </body>
</page>

Where \Magento\ConfigurableProduct\Block\Cart\Item\Renderer\Configurable class is applied as renderer and contains methods for displaying the child product picture instead of the parent product picture.

How can you create a custom shopping cart renderer?

First, we need to create a block, \Vendor\Module\Block\Cart\CustomRenderer for example, that will be responsible for html content of a product. The product that we need to display is set in \Magento\Checkout\Block\Cart\AbstractCart::getItemHtml block.

public function getItemHtml(\Magento\Quote\Model\Quote\Item $item)
	{
    	$renderer = $this->getItemRenderer($item->getProductType())->setItem($item);
    	return $renderer->toHtml();
	}

Therefore, the product will be available in the template of our block by calling $product = $block->getItem();

Also, renderer should be recorded in the list:

<module_dir>/view/frontend/layout/checkout_cart_item_renderers.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.cart.item.renderers">
            <block class="\Vendor\Module\Block\Cart\CustomRenderer" as="custom-type" template="Magento_Checkout::cart/item/default.phtml">
                <block class="Magento\Checkout\Block\Cart\Item\Renderer\Actions" name="checkout.cart.item.renderers.custom.actions" as="actions">
                    <block class="Magento\Checkout\Block\Cart\Item\Renderer\Actions\Edit" name="checkout.cart.item.renderers.custom.actions.edit" template="Magento_Checkout::cart/item/renderer/actions/edit.phtml"/>
                    <block class="Magento\Checkout\Block\Cart\Item\Renderer\Actions\Remove" name="checkout.cart.item.renderers.custom.actions.remove" template="Magento_Checkout::cart/item/renderer/actions/remove.phtml"/>
                </block>
            </block>
        </referenceBlock>
    </body>
</page>

Afterwards, all the “custom-type” type products will use our class for display.

Describe the available shopping cart operations. Which operations are available to the customer on the cart page?

  • Modify product quantity
  • Update the cart (required after the quantities are modified)
  • Modify configurable/bundle product options
  • Delete the product
  • Place the product into wishlist (if enabled)
  • Add / delete the coupon
  • Go to checkout
  • Go to multi shipping settings (if enabled)

How can you customize cart edit functionality?

Let checkout/cart/configure call action \Magento\Checkout\Controller\Cart\Configure, which, in its turn, allows the user to edit the product, already added to the shopping cart.

Depending on your wish to customize, you can modify either the action or the corresponding layout / templates.

How would you create an extension that deletes one item if another was deleted?

To create an extension that deletes one item if another was deleted, use observer for sales_quote_remove_item event.

<module_dir>/etc/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_quote_remove_item">
        <observer name="removeCartItem" instance="Vendor\Module\Observer\RemoveCartItem" />
    </event>
</config>

<module_dir>/Observer/RemoveCartItem.php

namespace Vendor\Module\Observer;

class RemoveCartItem implements \Magento\Framework\Event\ObserverInterface
{

  public function execute(\Magento\Framework\Event\Observer $observer)
  {
    $deletedItem = $observer->getData('quote_item');
    //We can also check id or sku of the deleted cart item, and depending on the result, delete the other one.  
  }
}

How do you add a field to the shipping address?

To add a new field into shipping address, you need to:

  1. Add a field into layout.
    Because shipping and billing addresses forms are generated dynamically, we can do it with a plugin for \Magento\Checkout\Block\Checkout\LayoutProcessor::process method.
  1. Create a JS mixin for sending additional information.
    We need to modify Magento_Checkout/js/action/set-shipping-information component.
  1. Add a new field into address model.
    Using extension attributes/<module_dir>/etc/extension_attributes.xml

    <?xml version="1.0"?>
    
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
        <extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
            <attribute code="custom_field" type="string" />
        </extension_attributes>
    </config>
  1. Get the value at the server side.
    Use getExtensionAttributes method of the Magento\Checkout\Api\Data\ShippingInformationInterface interface.

To learn more, follow the link:

https://devdocs.magento.com/guides/v2.2/howdoi/checkout/checkout_new_field.html

8.4 Demonstrate ability to customize shipping and payment methods

Describe shipping methods architecture. How can you create a new shipping method? What is the relationship between carriers and rates?

Creation of custom shipping method starts with creating system.xml and config.xml files in our module. The first one defines the options, available for the configuration of a new shipping method, while the second one sets the default values.

<module_dir>/adminhtml/system.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="carriers" translate="label" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1">
            <group id="customshipping" translate="label" type="text" sortOrder="0" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Custom Shipping</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="name" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Method Name</label>
                </field>
                <field id="price" translate="label" type="text" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Price</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                </field>
                <field id="showmethod" translate="label" type="select" sortOrder="92" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Method if Not Applicable</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

Required parameters for each shipping method:

  • active – shipping method enabled / disabled.
  • model – path to shipping method model.
  • title – shipping carrier name; the parameter displays at the front end.
  • sallowspecific – determines whether the shipping method is available for all countries, or for the certain ones.
  • sort_order – the order of shipping methods display in the list.

We also use several additional configuration parameters:

  • price;
  • name;
  • showmethod – whether the shipping method is displayed even if it can not be applied to the actual cart / customer.

Config.xml file sets default values for the parameters from system.xml file.

<module_dir>/etc/config.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <carriers>
            <customshipping>
                <active>1</active>
                <model>Vendor\Module\Model\Carrier\CustomShipping</model>
                <name>Custom Shipping</name>
                <price>5.00</price>
                <title>Custom Shipping</title>
                <sallowspecific>0</sallowspecific>
                <sort_order>100</sort_order>
            </customshipping>
        </carriers>
    </default>
</config>

When configuration files are created, we create our shipping method model:

<module_dir>/Model/Carrier/CustomShipping.php

<?php
 
namespace Vendor\Module\Model\Carrier;
  
class CustomShipping
    extends \Magento\Shipping\Model\Carrier\AbstractCarrier
    implements \Magento\Shipping\Model\Carrier\CarrierInterface
{
    /**
 	* Constant defining shipping code for method
 	*/
    const SHIPPING_CODE = 'customshipping';
 
    /**
 	* @var string
 	*/
    protected $_code = self::SHIPPING_CODE;
 
    /**
 	* @var \Magento\Shipping\Model\Rate\ResultFactory
 	*/
    protected $rateResultFactory;
 
    /**
 	* @var \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
 	*/
    protected $rateMethodFactory;
 
    /**
 	* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
 	* @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
 	* @param \Psr\Log\LoggerInterface $logger
 	* @param \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory
 	* @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory
 	* @param array $data
 	*/
    public function __construct(
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
        \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
        array $data = []
    ) {
        $this->rateResultFactory = $rateResultFactory;
        $this->rateMethodFactory = $rateMethodFactory;
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
    }
 
    /**
 	* @return array
 	*/
    public function getAllowedMethods()
    {
        return [self::SHIPPING_CODE => $this->getConfigData('name')];
    }
 
    /**
 	* @param \Magento\Quote\Model\Quote\Address\RateRequest $request
 	* @return bool|\Magento\Shipping\Model\Rate\Result
 	*/
    public function collectRates(\Magento\Quote\Model\Quote\Address\RateRequest  $request)
    {
        if (!$this->getActiveFlag()) {
            return false;
        }
        
        /** @var \Magento\Shipping\Model\Rate\Result $result */
        $result = $this->rateResultFactory->create();
 
        /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
        $method = $this->rateMethodFactory->create();
 
        $method->setCarrier(self::SHIPPING_CODE);
        // Get the title from the configuration, as defined in system.xml
        $method->setCarrierTitle($this->getConfigData('title'));
 
        $method->setMethod(self::SHIPPING_CODE);
        // Get the title from the configuration, as defined in system.xml
        $method->setMethodTitle($this->getConfigData('name'));
 
        // Get the price from the configuration, as defined in system.xml
        $amount = $this->getConfigData('price');
 
        $method->setPrice($amount);
        $method->setCost($amount);
 
        $result->append($method);
 
        return $result;
    }
}

Shipping method model should inherit \Magento\Shipping\Model\Carrier\AbstractCarrier class and realize \Magento\Shipping\Model\Carrier\CarrierInterface interface; it also should have at least two methods:

  • getAllowedMethods:
    • Should return the options array, available for our method (standard or fast delivery).
  • collectRates:
    • When returned, false deletes the given shipping method from the list of available ones.
    • An instance of \Magento\Shipping\Model\Rate\Result with the list of available methods.

$_code parameters in this model should contain a unique shipping method code; in our case, it’s “customshipping”.

$this->getConfigData method allows to get the configuration values for our method (based on the available in system.xml file).

Each shipping carrier can contain one or several shipping rates.

Describe how to troubleshoot shipping methods and rate results. Where do shipping rates come from? How can you debug the wrong shipping rate being returned?

collectRates method can be applied to check the availability of the given method for a certain customer. For instance, if we need to limit the method availability to certain postcodes only, this check should be performed there.

This method can be used to set up shipping methods. The call is performed in collectCarrierRates method of the \Magento\Shipping\Model\Shipping class.

Describe how to troubleshoot payment methods. What are the different payment flows?

In Magento 2, there are three payment method types:

  1. Gateway – payment data is passed first into Magento, and then – to the merchant. To enhance safety, use payment data tokenization.
  2. Offline – the payment method that does not provide for the connection with any third-party payment provider. Examples: Check/Money Order, Bank Transfer, Purchase Order and Cash on Delivery
  3. Hosted – redirects the customer to the payment page that is not a part of Magento 2.

Each gateway payment method is divided into a different number of commands:

  • fetch_transaction_information
  • order
  • authorize
  • Capture
  • refund
  • cancel
  • void
  • acceptPaymen
  • denyPaymen

Each command should be realized as a separate class; therefore, payment methods debugging should begin with the class of the corresponding command.

Vlad-Yunusov banner
Vlad-Yunusov

Tell us about your project

Get in touch with our team. Send us an email at [email protected] or call us 1 650 353 2301

Send request