How to add a simple note to Magento one step checkout

Sometimes, the additional information in an order is very useful. It can be an additional choice or a message from a customer. Unfortunately, Magento 2 does not provide any additional forms on the Magento one page checkout steps by default.


So, in the post, we will add a simple note field on the checkout, it can be a different element of form but we think the text field is more in demand.

First, we need to create a custom module. In our case, we will create Atwix_SimpleNote module.
The initial files:
app/code/Atwix/SimpleNote/registration.php

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Atwix_SimpleNote',
    __DIR__
);

app/code/Atwix/SimpleNote/etc/module.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Atwix_SimpleNote" setup_version="0.0.1">
        <sequence>
            <module name="Magento_Sales" />
        </sequence>
    </module>
</config>

We need to add the attributes to the quote and order entities. Also, to have an ability to see it on the order’s grid, we need to add the column in the sales_order_grid table.
Adding a setup script classes for adding the attribute to the tables:

app/code/Atwix/SimpleNote/Setup/SchemaInformation.php

<?php

namespace Atwix\SimpleNote\Setup;

/**
 * Class SchemaInformation
 *
 * @codeCoverageIgnore
 */
class SchemaInformation
{
    /**
     * Attribute code
     */
    const ATTRIBUTE_SIMPLE_NOTE = 'simple_note';
}

app/code/Atwix/SimpleNote/Setup/InstallData.php

<?php

namespace Atwix\SimpleNote\Setup;

use Atwix\SimpleNote\Setup\SetupService\AddSimpleNoteFieldToQuoteAndOrderService;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

/**
 * Class InstallData
 *
 * @codeCoverageIgnore
 */
class InstallData implements InstallDataInterface
{
    /**
     * @var AddSimpleNoteFieldToQuoteAndOrderService
     */
    protected $addSimpleNoteFieldToQuoteAndOrderService;

    /**
     * InstallData constructor.
     *
     * @param AddSimpleNoteFieldToQuoteAndOrderService $addSimpleNoteFieldToQuoteAndOrderService
     */
    public function __construct(AddSimpleNoteFieldToQuoteAndOrderService $addSimpleNoteFieldToQuoteAndOrderService)
    {
        $this->addSimpleNoteFieldToQuoteAndOrderService = $addSimpleNoteFieldToQuoteAndOrderService;
    }

    /**
     * Installs data for a module
     *
     * @param ModuleDataSetupInterface $setup
     * @param ModuleContextInterface   $context
     *
     * @return void
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $setup->startSetup();

        $this->addSimpleNoteFieldToQuoteAndOrderService->execute($setup);

        $setup->endSetup();
    }
}

app/code/Atwix/SimpleNote/Setup/SetupService/AddSimpleNoteFieldToQuoteAndOrderService.php

<?php

namespace Atwix\SimpleNote\Setup\SetupService;

use Atwix\SimpleNote\Setup\SchemaInformation;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Quote\Setup\QuoteSetup;
use Magento\Quote\Setup\QuoteSetupFactory;
use Magento\Sales\Setup\SalesSetup;
use Magento\Sales\Setup\SalesSetupFactory;

/**
 * Class AddSimpleNoteFieldToQuoteAndOrderService
 *
 * @codeCoverageIgnore
 */
class AddSimpleNoteFieldToQuoteAndOrderService
{
    /**
     * @var QuoteSetupFactory
     */
    protected $quoteSetupFactory;

    /**
     * @var SalesSetupFactory
     */
    protected $salesSetupFactory;

    /**
     * AddSimpleNoteFieldToQuoteAndOrderService constructor.
     *
     * @param QuoteSetupFactory $quoteSetupFactory
     * @param SalesSetupFactory $salesSetupFactory
     */
    public function __construct(
        QuoteSetupFactory $quoteSetupFactory,
        SalesSetupFactory $salesSetupFactory
    ) {
        $this->quoteSetupFactory = $quoteSetupFactory;
        $this->salesSetupFactory = $salesSetupFactory;
    }

    /**
     * @param ModuleDataSetupInterface $dataSetup
     */
    public function execute(ModuleDataSetupInterface $dataSetup)
    {
        $attributeAttr = [
            'type' => Table::TYPE_TEXT,
            'comment' => 'Simple Note',
        ];

        $this->addAttributeToQuote(
            SchemaInformation::ATTRIBUTE_SIMPLE_NOTE,
            $attributeAttr,
            $dataSetup
        );
        $this->addAttributeToOrder(
            SchemaInformation::ATTRIBUTE_SIMPLE_NOTE,
            $attributeAttr,
            $dataSetup
        );
        $this->addAttributeToOrderGrid(
            SchemaInformation::ATTRIBUTE_SIMPLE_NOTE,
            $attributeAttr,
            $dataSetup
        );
    }

    /**
     * Add attribute to quote
     *
     * @param string $attributeCode
     * @param array $attributeAttr
     * @param ModuleDataSetupInterface $dataSetup
     */
    protected function addAttributeToQuote($attributeCode, $attributeAttr, $dataSetup)
    {
        /** @var QuoteSetup $quoteSetup */
        $quoteSetup = $this->quoteSetupFactory->create(
            [
                'resourceName' => 'quote_setup',
                'setup' => $dataSetup,
            ]
        );
        $quoteSetup->addAttribute('quote', $attributeCode, $attributeAttr);
    }

    /**
     * Add attribute to order
     *
     * @param string $attributeCode
     * @param array $attributeAttr
     * @param ModuleDataSetupInterface $dataSetup
     */
    protected function addAttributeToOrder($attributeCode, $attributeAttr, $dataSetup)
    {
        /** @var SalesSetup $salesSetup */
        $salesSetup = $this->salesSetupFactory->create(
            [
                'resourceName' => 'sales_setup',
                'setup' => $dataSetup,
            ]
        );
        $salesSetup->addAttribute('order', $attributeCode, $attributeAttr);
    }

    /**
     * Add attribute to order grid
     *
     * @param string $attributeCode
     * @param array $attributeAttr
     * @param ModuleDataSetupInterface $dataSetup
     */
    protected function addAttributeToOrderGrid($attributeCode, $attributeAttr, $dataSetup)
    {
        $dataSetup->getConnection()->addColumn(
            $dataSetup->getTable('sales_order_grid'),
            $attributeCode,
            $attributeAttr
        );
    }
}

Adding Magento API classes to save the data:

app/code/Atwix/SimpleNote/Api/Data/SimpleNoteInterface.php

<?php

namespace Atwix\SimpleNote\Api\Data;

/**
 * Interface SimpleNoteInterface
 */
interface SimpleNoteInterface
{
    /**
     * Get Simple Note
     *
     * @return string
     */
    public function getSimpleNote();

    /**
     * Set Simple Note
     *
     * @param string $simpleNote
     *
     * @return void
     */
    public function setSimpleNote($simpleNote);
}

app/code/Atwix/SimpleNote/Api/SimpleNoteManagementInterface.php

<?php

namespace Atwix\SimpleNote\Api;

/**
 * Interface for saving the checkout note to the quote for orders
 *
 * @api
 */
interface SimpleNoteManagementInterface
{
    /**
     * @param int $cartId
     * @param \Atwix\SimpleNote\Api\Data\SimpleNoteInterface $simpleNote
     *
     * @return string
     */
    public function saveSimpleNote(
        $cartId,
        \Atwix\SimpleNote\Api\Data\SimpleNoteInterface $simpleNote
    );
}

Declaring the Magento API:

app/code/Atwix/SimpleNote/etc/webapi.xml

<?xml version="1.0"?>

<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">

    <route url="/V1/carts/mine/set-simple-note" method="PUT">
        <service class="Atwix\SimpleNote\Api\SimpleNoteManagementInterface" method="saveSimpleNote"/>
        <resources>
            <resource ref="self" />
        </resources>
        <data>
            <parameter name="cartId" force="true">%cart_id%</parameter>
        </data>
    </route>
</routes>

Model:
app/code/Atwix/SimpleNote/Model/Data/SimpleNote.php

<?php

namespace Atwix\SimpleNote\Model\Data;

use Atwix\SimpleNote\Api\Data\SimpleNoteInterface;
use Atwix\SimpleNote\Setup\SchemaInformation;
use Magento\Framework\Api\AbstractSimpleObject;

/**
 * Class SimpleNote
 */
class SimpleNote extends AbstractSimpleObject implements SimpleNoteInterface
{
    /**
     * @inheritdoc
     */
    public function getSimpleNote()
    {
        return $this->_get(SchemaInformation::ATTRIBUTE_SIMPLE_NOTE);
    }

    /**
     * @inheritdoc
     */
    public function setSimpleNote($simpleNote)
    {
        return $this->setData(SchemaInformation::ATTRIBUTE_SIMPLE_NOTE, $simpleNote);
    }
}

app/code/Atwix/SimpleNote/Model/SimpleNoteManagement.php

<?php
namespace Atwix\SimpleNote\Model;

use Atwix\SimpleNote\Api\Data\SimpleNoteInterface;
use Atwix\SimpleNote\Api\SimpleNoteManagementInterface;
use Atwix\SimpleNote\Setup\SchemaInformation;
use Exception;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Quote\Api\CartRepositoryInterface;

/**
 * Class SimpleNoteManagement
 */
class SimpleNoteManagement implements SimpleNoteManagementInterface
{
    /**
     * Quote repository.
     *
     * @var CartRepositoryInterface
     */
    protected $quoteRepository;

    /**
     * SimpleNoteManagement constructor.
     *
     * @param CartRepositoryInterface $quoteRepository
     */
    public function __construct(CartRepositoryInterface $quoteRepository)
    {
        $this->quoteRepository = $quoteRepository;
    }

    /**
     * Save simple note number in the quote
     *
     * @param int $cartId
     * @param SimpleNoteInterface $simpleNote
     *
     * @return null|string
     *
     * @throws CouldNotSaveException
     * @throws NoSuchEntityException
     */
    public function saveSimpleNote(
        $cartId,
        SimpleNoteInterface $simpleNote
    ) {
        $quote = $this->quoteRepository->getActive($cartId);
        if (!$quote->getItemsCount()) {
            throw new NoSuchEntityException(__('Cart %1 doesn\'t contain products', $cartId));
        }
        $sn = $simpleNote->getSimpleNote();

        try {
            $quote->setData(SchemaInformation::ATTRIBUTE_SIMPLE_NOTE, strip_tags($sn));
            $this->quoteRepository->save($quote);
        } catch (Exception $e) {
            throw new CouldNotSaveException(__('The simple note # number could not be saved'));
        }

        return $sn;
    }
}

di.xml
app/code/Atwix/SimpleNote/etc/di.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="Atwix\SimpleNote\Api\Data\SimpleNoteInterface" type="Atwix\SimpleNote\Model\Data\SimpleNote" />
    <preference for="Atwix\SimpleNote\Api\SimpleNoteManagementInterface" type="Atwix\SimpleNote\Model\SimpleNoteManagement" />

</config>

Now we need to save the data from quote to the order. Adding the observer and declared the event:

app/code/Atwix/SimpleNote/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_model_service_quote_submit_before">
        <observer name="atwix_add_simplenote_to_order" instance="Atwix\SimpleNote\Observer\AddSimpleNoteToOrderObserver" />
    </event>
</config>

Observer:
app/code/Atwix/SimpleNote/Observer/AddSimpleNoteToOrderObserver.php

<?php

namespace Atwix\SimpleNote\Observer;

use Atwix\SimpleNote\Setup\SchemaInformation;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Quote\Model\Quote;
use Magento\Sales\Model\Order;

/**
 * Class AddSimpleNoteToOrderObserver
 */
class AddSimpleNoteToOrderObserver implements ObserverInterface
{
    /**
     * Transfer the Simple Note from the quote to the order
     * event sales_model_service_quote_submit_before
     *
     * @param Observer $observer
     *
     * @return void
     */
    public function execute(Observer $observer)
    {
        /* @var $order Order */
        $order = $observer->getEvent()->getOrder();

        /** @var $quote Quote $quote */
        $quote = $observer->getEvent()->getQuote();
        /** @var string $simpleNote */
        $simpleNote = $quote->getData(SchemaInformation::ATTRIBUTE_SIMPLE_NOTE);

        $order->setData(SchemaInformation::ATTRIBUTE_SIMPLE_NOTE, $simpleNote);
    }
}

We also need to save the data in the order grid table, but it is not required if you do not want to display the data in the admin order grid. For it, we need to add virtual type in the di.xml
Adding the code to the di.xml

app/code/Atwix/SimpleNote/etc/di.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="Atwix\SimpleNote\Api\Data\SimpleNoteInterface" type="Atwix\SimpleNote\Model\Data\SimpleNote" />
    <preference for="Atwix\SimpleNote\Api\SimpleNoteManagementInterface" type="Atwix\SimpleNote\Model\SimpleNoteManagement" />

    <virtualType name="Magento\Sales\Model\ResourceModel\Order\Grid" type="Magento\Sales\Model\ResourceModel\Grid">
        <arguments>
            <argument name="columns" xsi:type="array">
                <item name="simple_note" xsi:type="string">sales_order.simple_note</item>
            </argument>
        </arguments>
    </virtualType>
</config>

So, now we can add the simple note to the checkout and show the field on the payment step:
app/code/Atwix/SimpleNote/view/frontend/layout/checkout_index_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.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="steps" xsi:type="array">
                                    <item name="children" xsi:type="array">
                                        <item name="billing-step" xsi:type="array">
                                            <item name="children" xsi:type="array">
                                                <item name="payment" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="beforeMethods" xsi:type="array">
                                                            <item name="children" xsi:type="array">
                                                                <item name="order-simple-note" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Atwix_SimpleNote/js/view/order-simple-note</item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

app/code/Atwix/SimpleNote/view/frontend/web/js/view/order-simple-note.js

define(
    [
        'jquery',
        'ko',
        'uiComponent',
        'Atwix_SimpleNote/js/action/save-order-simple-note',
        'Magento_Checkout/js/model/quote'
    ],
    function ($, ko, Component, saveOrderSimpleNote, quote) {
        'use strict';

        return Component.extend({
            defaults: {
                template: 'Atwix_SimpleNote/order_simple_note',
                simpleNote: ''
            },

            initObservable: function () {
                return this._super().observe(['simpleNote'])
            },

            saveSimpleNote: function () {
                var simpleNote = this.simpleNote();
                saveOrderSimpleNote.save(simpleNote);
            }
        });
    }
);

Also, we can use a different type of form, in our example, it will be a text field:
app/code/Atwix/SimpleNote/view/frontend/web/template/order_simple_note.html

<div class="order-simple-note-wrapper">
    <div class="field">
        <label class="label" for="order-simple-note">
            <span data-bind="i18n: 'Add a note to this order (optional)'"></span>
        </label>
        <input type="text"
               class="input-text"
               id="order-simple-note"
               data-bind="value: simpleNote, event: {change: saveSimpleNote}"
               placeholder="Add note"
        />
    </div>
</div>

The component for saving checkout in the quote:
app/code/Atwix/SimpleNote/view/frontend/web/js/action/save-order-simple-note.js

define(
    [
        'jquery',
        'Magento_Checkout/js/model/quote',
        'Magento_Checkout/js/model/url-builder',
        'Magento_Checkout/js/model/error-processor',
        'mage/url'
    ],
    function ($, quote, urlBuilder, errorProcessor, urlFormatter) {
    'use strict';

    return {
        /**
         * Save SimpleNote ibn the quote
         *
         * @param simpleNote
         */
        save: function (simpleNote) {
            if (simpleNote) {
                var quoteId = quote.getQuoteId();
                var url;

                url = urlBuilder.createUrl('/carts/mine/set-simple-note', {});

                var payload = {
                    cartId: quoteId,
                    simpleNote: {
                        simpleNote: simpleNote
                    }
                };

                if (!payload.simpleNote.simpleNote) {

                    return true;
                }

                var result = true;

                $.ajax({
                    url: urlFormatter.build(url),
                    data: JSON.stringify(payload),
                    global: false,
                    contentType: 'application/json',
                    type: 'PUT',
                    async: false
                }).done(
                    function (response) {
                        result = true;
                    }
                ).fail(
                    function (response) {
                        result = false;
                        errorProcessor.process(response);
                    }
                );

                return result;
            }
        }
    };
});

Before we added the possibility to save the data in the order grid table, now we can show the note in the order view grid:
app/code/Atwix/SimpleNote/view/adminhtml/ui_component/sales_order_grid.xml

<?xml version="1.0" encoding="UTF-8"?>

<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <columns name="sales_order_columns">
        <column name="simple_note">
            <settings>
                <filter>text</filter>
                <label translate="true">Simple note</label>
            </settings>
        </column>
    </columns>
</listing>

Also, you can display the note in the different places, the note is available from an order, you only need to overwrite necessary template and print the data.
For example for showing the note in the customer order view page you need to overwrite:

vendor/magento/module-sales/view/frontend/templates/order/info.phtml
and get the note:

<?php echo $_order->getData('simple_note') ?>

As you can see the process of adding additional fields to the checkout needs a lot of effort, but it allows to have additional functionality and additional opportunity to interact with a customer.