Magento – Magento 2.3 – Customer address attribute not saving in checkout from

customer-addresscustomer-attributemagento2magento2.3shipping-address

I have created a custom custom_address attribute "a_mobile_phone". I can save the attribute in the admin and the customer address edit form.

I have added this attribute to the checkout address form with no issue but it is not saving in the database.

My attribute is used in form:
enter image description here

I have a plugin:

//Vendor/Module/etc/frontend/di.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
            <plugin name="add-mobile-phone-field"
                    type="Vendor\Module\Plugin\LayoutProcessorPlugin" sortOrder="10"/>
        </type>
    </config>

My file:

//Vendor/Module/Plugin/LayoutProcessorPlugin.php
public function afterProcess(
        \Magento\Checkout\Block\Checkout\LayoutProcessor $subject,
        array  $jsLayout
    ) {
        $customAttributeCode = 'a_mobile_phone';

        $customField = [
            'component' => 'Magento_Ui/js/form/element/abstract',
            'config' => [
                // customScope is used to group elements within a single form (e.g. they can be validated separately)
                'customScope' => 'shippingAddress.custom_attributes',
                'customEntry' => null,
                'template' => 'ui/form/field',
                'elementTmpl' => 'ui/form/element/input',
                'tooltip' => [
                    'description' => 'Pour la livraison',
                ],
            ],
            'dataScope' => 'shippingAddress.custom_attributes' . '.' . $customAttributeCode,
            'label' => 'Téléphone portable',
            'provider' => 'checkoutProvider',
            'sortOrder' => 200,
            'validation' => [
                'required-entry' => true
            ],
            'options' => [],
            'filterBy' => null,
            'customEntry' => null,
            'required' => true,
            'visible' => true,
            'value' => '' // value field is used to set a default value of the attribute
        ];

        $jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
        ['shippingAddress']['children']['shipping-address-fieldset']['children'][$customAttributeCode] = $customField;

        return $jsLayout;
    }

My fieldset:

//Vendor/Module/etc/fieldset.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:DataObject/etc/fieldset.xsd">
    <scope id="global">
        <fieldset id="sales_convert_quote_address">
            <field name="a_mobile_phone">
                <aspect name="to_order_address" />
                <aspect name="to_customer_address" />
            </field>
        </fieldset>
    </scope>
</config>

The mixin:

//Vendor/Module/view/frontend/requirejs-config.js
var config = {
    config: {
        mixins: {
            'Magento_Checkout/js/action/set-shipping-information': {
                'Vendor_Module/js/action/set-shipping-information-mixin': true
            }
        }
    }
};

The mixin file:

//Vendor/Module/view/frontend/web/js/action/set-shipping-information-mixin.js
define([
    'jquery',
    'mage/utils/wrapper',
    'Magento_Checkout/js/model/quote'
], function ($, wrapper, quote) {
    'use strict';

    return function (setShippingInformationAction) {

        return wrapper.wrap(setShippingInformationAction, function (originalAction) {
            var shippingAddress = quote.shippingAddress();
            if (shippingAddress['extension_attributes'] === undefined) {
                shippingAddress['extension_attributes'] = {};
            }

            shippingAddress['extension_attributes']['a_mobile_phone'] = jQuery('[name="a_mobile_phone"]').val();

            return originalAction();
        });
    };
});

The extension attribute :

//Vendor/Module/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\Sales\Api\Data\OrderAddressInterface">
        <attribute code="a_mobile_phone" type="string" />
    </extension_attributes>
    <extension_attributes for="Magento\Customer\Api\Data\AddressInterfacee">
        <attribute code="a_mobile_phone" type="string" />
    </extension_attributes>
    <extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
        <attribute code="a_mobile_phone" type="string" />
    </extension_attributes>
    <extension_attributes for="Magento\Checkout\Api\Data\ShippingInformationInterface">
        <attribute code="a_mobile_phone" type="string" />
    </extension_attributes>
</config>

With all this my attribute is displayed in the checkout.
When I set the value in a new address I have the code displayed by the knockoutJs.
enter image description here

But my biggest issue is that the attribute value is not save in the DB when I pass the order.

What am I missing to save the attribute value ? I followed the magento doc for this : https://devdocs.magento.com/guides/v2.3/howdoi/checkout/checkout_new_field.html

EDIT 1 :

So I managed to save the data to the address with :

Adding the field to 2 table in the DB :

$installer->getConnection()
                ->addColumn(
                    $setup->getTable('quote_address'),
                    "a_mobile_phone",
                    ['type' => Table::TYPE_TEXT, 'nullable' => true, 'comment' => 'Phone for delivery']
                );

            $installer->getConnection()
                ->addColumn(
                    $setup->getTable('sales_order_address'),
                    "a_mobile_phone",
                    ['type' => Table::TYPE_TEXT, 'nullable' => true, 'comment' => 'Phone for delivery']
                );

Adding a new mixin :

'Magento_Checkout/js/model/shipping-save-processor/payload-extender': {
                'Vendor_Module/js/model/shipping-save-processor/payload-extender': true
            }

The mixin : Vendor/Module/view/frontnend/web/js/model/shipping-save-processor/payload-extender.js

define([
    'jquery',
    'mage/utils/wrapper'
], function (
    jQuery,
    wrapper
) {
    'use strict';

    return function (processor) {
        return wrapper.wrap(processor, function (proceed, payload) {
            payload = proceed(payload);

            var shippingAddress =  payload.addressInformation.shipping_address;
            var aMobilePhone = jQuery('[name="custom_attributes[a_mobile_phone]"]').val();

            if(aMobilePhone == "" || aMobilePhone == null){
                if(shippingAddress.customAttributes == "undefined" || shippingAddress.customAttributes == null){
                    aMobilePhone = null;
                } else {
                    if(shippingAddress.customAttributes.a_mobile_phone == "undefined" || shippingAddress.customAttributes.a_mobile_phone == null) {
                        aMobilePhone = null;
                    } else {
                        aMobilePhone = shippingAddress.customAttributes.a_mobile_phone.value;
                    }
                }
            }

            var goneExtentionAttributes = {
                'aMobilePhone': aMobilePhone
            };
            payload.addressInformation.extension_attributes = _.extend(
                payload.addressInformation.extension_attributes,
                goneExtentionAttributes
            );

            return payload;
        });
    };
});

Modify set-shipping-information-mixin.js as suggested by Amit Bera :

 $.each(shippingAddress.customAttributes, function(index, eachCustomAttribute){
        console.log(eachCustomAttribute);
        if(eachCustomAttribute.attribute_code == 'a_mobile_phone')
        {
     shippingAddress['extension_attributes']['a_mobile_phone'] = eachCustomAttribute.value ;
        }
    });

A new plugin on : Magento\Checkout\Model\ShippingInformationManagement

class ShippingAddressManagementPlugin
{
    /**
     * @var LoggerInterface $logger
     */
    protected $logger;

    /**
     * ShippingAddressManagementPlugin constructor.
     * @param LoggerInterface $logger
     */
    public function __construct(
        LoggerInterface $logger
    ) {
        $this->logger = $logger;
    }

    /**
     * @param ShippingAddressManagement $subject
     * @param $cartId
     * @param AddressInterface $address
     */
    public function beforeAssign(
        ShippingAddressManagement $subject,
        $cartId,
        AddressInterface $address
    ) {
        $extAttributes = $address->getExtensionAttributes();
        if (!empty($extAttributes)) {
            try {
                $address->setAMobilePhone($extAttributes->getAMobilePhone());
            } catch (\Exception $e) {
                $this->logger->critical($e->getMessage());
            }
        }
    }
}

I have just one issue left, the attribute code "a_mobile_phone" displayed inside the address summary (see under)

enter image description here

EDIT 2

Thanks to @coderGeek i have fixed th last issue :

I have modified the requirejs-config.js file and added the default.html one

//file : Vendor/Module/view/web/template/shipping-address/address-renderer/default.html
<div class="shipping-address-item" css="'selected-item' : isSelected() , 'not-selected-item':!isSelected()">
    <text args="address().prefix"/> <text args="address().firstname"/> <text args="address().middlename"/>
    <text args="address().lastname"/> <text args="address().suffix"/><br/>
    <text args="_.values(address().street).join(', ')"/><br/>
    <text args="address().city "/>, <span text="address().region"></span> <text args="address().postcode"/><br/>
    <text args="getCountryName(address().countryId)"/><br/>
    <a if="address().telephone" attr="'href': 'tel:' + address().telephone" text="address().telephone"></a><br/>

    <each args="data: address().customAttributes, as: 'element'">
        <each args="data: Object.keys(element), as: 'attribute'">
            <if args="typeof element[attribute] === 'object'">
                <if args="element[attribute].label">
                    <text args="element[attribute].label"/>
                </if>
                <ifnot args="element[attribute].label">
                    <if args="element[attribute].value">
                        <text args="element[attribute].value"/>
                    </if>
                </ifnot>
                <br/>
            </if>
            <if args="typeof element[attribute] === 'string'">
                <if args="attribute == 'value'">
                    <text args="element[attribute]"/>
                    <br/>
                </if>
            </if>
        </each>
    </each>

    <button visible="address().isEditable()" type="button"
            class="action edit-address-link"
            click="editAddress">
        <span translate="'Edit'"></span>
    </button>
    <!-- ko if: (!isSelected()) -->
    <button type="button" click="selectAddress" class="action action-select-shipping-item">
        <span translate="'Ship Here'"></span>
    </button>
    <!-- /ko -->
</div>

Best Answer

TO Save this attribute value to Order address, you need to use Below event:

  • sales_model_service_quote_submit_before
  • sales_model_service_quote_submit_success

At those events you need to copy the field value from quote address to order address object then, the value will save to sales_order_address table.

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 instance="StackExchange\Magento\Observer\Sales\ModelServiceQuoteSubmitBefore" name="stackexchange_magento_observer_sales_modelservicequotesubmitbefore_sales_model_service_quote_submit_before"/>
    </event>
    <event name="sales_model_service_quote_submit_success">
        <observer instance="StackExchange\Magento\Observer\Sales\ModelServiceQuoteSubmitSuccess" name="stackexchange_magento_observer_sales_modelservicequotesubmitsuccess_sales_model_service_quote_submit_success"/>
    </event>
</config>

Observer class: ModelServiceQuoteSubmitBefore.php

<?php
namespace StackExchange\Magento\Observer\Sales;

use Magento\Framework\Event\Observer;

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

    /**
     * Execute observer
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     */
     public function execute(Observer $observer)
    {
        $order = $observer->getEvent()->getOrder();
        $quote = $observer->getEvent()->getQuote();

        try {
            if ($quote->getBillingAddress()) {
                $order->getBillingAddress()->setAMobilePhone($quote->getBillingAddress()->getAMobilePhone());
            }

            if (!$quote->isVirtual()) {
                $order->getShippingAddress()->setAMobilePhone($quote->getShippingAddress()->getAMobilePhone());
            }
        } catch (\Exception $e) {
            // add logger
        }
        return $this;
    }
}

Observer class 2: ModelServiceQuoteSubmitSuccess.php

<?php
namespace StackExchange\Magento\Observer\Sales;

use Magento\Framework\Event\Observer;

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

    /**
     * Execute observer
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     */
     public function execute(Observer $observer)
    {

        /** @var \Magento\Quote\Model\Quote $quote */
        $quote = $observer->getEvent()->getQuote();
        $order =  $observer->getEvent()->getOrder();

        if (!$quote->getId() || !$order->getId()) {
            return $this;
        }


        try {
            if ($quote->getBillingAddress()) {
                $order->getBillingAddress()->setAMobilePhone($quote->getBillingAddress()->getAMobilePhone());
            }

            if (!$quote->isVirtual()) {
                $order->getShippingAddress()->setAMobilePhone($quote->getShippingAddress()->getAMobilePhone());
            }
        } catch (\Exception $e) {
            // add logger
        }
        return $this;
    }
}

Changes at mixin files

From

shippingAddress['extension_attributes']['a_mobile_phone'] = jQuery('[name="a_mobile_phone"]').val();

to

 $.each(shippingAddress.customAttributes, function(index, eachCustomAttribute){
        console.log(eachCustomAttribute);
        if(eachCustomAttribute.attribute_code == 'a_mobile_phone')
        {
     shippingAddress['extension_attributes']['a_mobile_phone'] = eachCustomAttribute.value ;
        }
    });

If the first observer is working then do not need to use second observer ModelServiceQuoteSubmitSuccess

Related Topic