How to Create Custom Payment Method using Adapter Class Magento 2?

Create a custom offline payment gateway in Magento 2 without the use of AbstractModel.

Using class Magento\Payment\Model\Method\Adapter with virtualType to make it compatible with the latest Magento standard practice to create a payment method.

The offline Payment method with the configuration setting from the admin panel, you can enable/disable payment method with the Payment method title. We will create a simple Purchase Order Payment method.

You can see the new payment method from the admin panel,
Stores > Settings > Configuration > Sales > Payment Methods.

The module name is, Jesadiya_PurchaseOrderPayment
We start with the registration.php file of the module,

<?php declare(strict_types=1);

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Jesadiya_PurchaseOrderPayment',
    __DIR__
);

Create a module.xml file,

Path: app/code/Jesadiya/PurchaseOrderPayment/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="Jesadiya_PurchaseOrderPayment">
        <sequence>
            <module name="Magento_Checkout"/>
        </sequence>
    </module>
</config>

Create system.xml file to Display settings on Payment methods tab in the Configuration page,
Path: app/code/Jesadiya/PurchaseOrderPayment/etc/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="payment">
            <group id="custompurchaseorder" translate="label" type="text" sortOrder="10" showInDefault="1"
                   showInWebsite="1" showInStore="1">
                <label><![CDATA[Custom Purchase Order]]></label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1"
                       showInStore="0" canRestore="1">
                    <label><![CDATA[Enabled]]></label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1"
                       showInStore="1" canRestore="1">
                    <label><![CDATA[Title]]></label>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1"
                       showInStore="0">
                    <label><![CDATA[Sort Order]]></label>
                    <frontend_class>validate-number</frontend_class>
                </field>
                <field id="order_status" translate="label" type="select" sortOrder="20" showInDefault="1"
                       showInWebsite="1" showInStore="0" canRestore="1">
                    <label><![CDATA[New Order Status]]></label>
                    <source_model>Magento\Sales\Model\Config\Source\Order\Status\NewStatus</source_model>
                </field>
                <field id="allowspecific" translate="label" type="allowspecific" sortOrder="50" showInDefault="1"
                       showInWebsite="1" showInStore="0" canRestore="1">
                    <label><![CDATA[Payment from Applicable Countries]]></label>
                    <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="51" showInDefault="1"
                       showInWebsite="1" showInStore="0">
                    <label><![CDATA[Payment from Specific Countries]]></label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                    <can_be_empty>1</can_be_empty>
                </field>
            </group>
        </section>
    </system>
</config>

All the node is understandable like Title used to display title name on checkout page.

sort_order used for the payment method on billing step position.

order_status will be pending for the Offline payment method.

allowspecific used to display payment for all the country or select a specific country.

Create a config.xml file to the default value set inside the Payment Configuration step in Admin Panel.

Offline Payment Method using Magento Standard Way Adapter Class

Path: app/code/Jesadiya/PurchaseOrderPayment/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>
        <payment>
            <custompurchaseorder>
                <active>1</active>
                <title>Custom Purchase Order</title>
                <order_status>pending</order_status>
                <allowspecific>0</allowspecific>
                <model>PurchaseOrderPaymentFacade</model>
                <group>offline</group>
                <can_use_internal>1</can_use_internal>
                <can_use_checkout>1</can_use_checkout>
                <is_offline>1</is_offline>
            </custompurchaseorder>
        </payment>
    </default>
</config>

A model tag used to display the Payment facade that will be used to create virtualType in di.xml file.

can_use_internal – Value 1 used to display only in the admin panel.

can_use_checkout – Value 1 used to display the payment method in the front end checkout page.

is_offline node will notify its offline payment method. you don’t require to call third party payment API to capture/settle transactions.

Main File is the di.xml to declare all the configuration and payment method facade without the use of Deprecated abstractMethod.

Set a relation using the di.xml file

Path: app/code/Jesadiya/PurchaseOrderPayment/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">
    <!-- Payment Method Facade configuration -->
    <virtualType name="PurchaseOrderPaymentFacade" type="Magento\Payment\Model\Method\Adapter">
        <arguments>
            <argument name="code" xsi:type="const">Jesadiya\PurchaseOrderPayment\Model\Ui\ConfigProvider::CODE
            </argument>
            <argument name="formBlockType" xsi:type="string">Magento\Payment\Block\Form</argument>
            <argument name="infoBlockType" xsi:type="string">Magento\Payment\Block\Info</argument>
            <argument name="valueHandlerPool" xsi:type="object">Magento\Payment\Gateway\Config\ValueHandlerPool
            </argument>
        </arguments>
    </virtualType>

    <!-- Configuration reader -->
    <type name="Magento\Payment\Gateway\Config\Config">
        <arguments>
            <argument name="methodCode" xsi:type="const">Jesadiya\PurchaseOrderPayment\Model\Ui\ConfigProvider::CODE
            </argument>
        </arguments>
    </type>

    <!-- Value handlers infrastructure -->
    <type name="Magento\Payment\Gateway\Config\ValueHandlerPool">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="default" xsi:type="string">Magento\Payment\Gateway\Config\ConfigValueHandler</item>
            </argument>
        </arguments>
    </type>
    <type name="Magento\Payment\Gateway\Config\ConfigValueHandler">
        <arguments>
            <argument name="configInterface" xsi:type="object">Magento\Payment\Gateway\Config\Config</argument>
        </arguments>
    </type>
</config>

We have to create PurchaseOrderPaymentFacade for the adapter class which we have defined as a model node in config.xml.

A discussion of the PurchaseOrderPaymentFacade,

code argument is the path of class which contains the payment method code.

formBlockType argument you can use your custom class to made admin panel level changes for the payment method in the admin sales order view page.

infoBlockType argument you can use a custom class to make changes on the frontend in order detail page.

valueHandlerPool argument used to pool of value handlers used for queries to configuration.

Create a Model class to define our payment method code,
Path: app/code/Jesadiya/PurchaseOrderPayment/Model/Ui/ConfigProvider.php

<?php declare(strict_types=1);

namespace Jesadiya\PurchaseOrderPayment\Model\Ui;

use Magento\Checkout\Model\ConfigProviderInterface;

abstract class ConfigProvider implements ConfigProviderInterface
{
    const CODE = 'custompurchaseorder';
}

Define your payment method code, I have used “custompurchaseorder” as the payment code.

Now Declare the payment method in the layout using checkout_index_index.xml,

Path: app/code/Jesadiya/PurchaseOrderPayment/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="renders" xsi:type="array">
                                                            <!-- merge payment method renders here -->
                                                            <item name="children" xsi:type="array">
                                                                <item name="custom-payments" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Jesadiya_PurchaseOrderPayment/js/view/payment/custom-payments</item>
                                                                    <item name="methods" xsi:type="array">
                                                                        <item name="custompurchaseorder" xsi:type="array">
                                                                            <item name="isBillingAddressRequired" xsi:type="boolean">true</item>
                                                                        </item>
                                                                    </item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

Create a .js component which we have declared in the layout file that registers the renderer in the billing step.

Path: app/code/Jesadiya/PurchaseOrderPayment/view/frontend/web/js/view/payment/custom-payments.js

define([
    'uiComponent',
    'Magento_Checkout/js/model/payment/renderer-list'
], function (Component, rendererList) {
    'use strict';

    rendererList.push(
        {
            type: 'custompurchaseorder', // must equals the payment code
            component: 'Jesadiya_PurchaseOrderPayment/js/view/payment/method-renderer/purchaseorder-method'
        }
    );

    /** Add view logic here if you needed */
    return Component.extend({});
});

Create a js component to define a template file that contains our payment method html form,

Path: app/code/Jesadiya/PurchaseOrderPayment/view/frontend/web/js/view/payment/method-renderer/purchaseorder-method.js

define([
    'Magento_Checkout/js/view/payment/default'
], function (Component) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Jesadiya_PurchaseOrderPayment/payment/purchaseorder-form'
        }
    });
});

Create a form template for the payment method component,

Path: app/code/Jesadiya/PurchaseOrderPayment/view/frontend/web/template/payment/purchaseorder-form.html

<!--
/**
 * Payment form template
 */
-->
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}">
    <div class="payment-method-title field choice">
        <input type="radio"
               name="payment[method]"
               class="radio"
               data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
        <label data-bind="attr: {'for': getCode()}" class="label">
            <span data-bind="text: getTitle()"></span>
        </label>
    </div>
    <div class="payment-method-content">
        <!-- ko foreach: getRegion('messages') -->
        <!-- ko template: getTemplate() --><!-- /ko -->
        <!--/ko-->
        <div class="payment-method-billing-address">
            <!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) -->
            <!-- ko template: getTemplate() --><!-- /ko -->
            <!--/ko-->
        </div>

        <div class="checkout-agreements-block">
            <!-- ko foreach: $parent.getRegion('before-place-order') -->
                <!-- ko template: getTemplate() --><!-- /ko -->
            <!--/ko-->
        </div>
        <div class="actions-toolbar" id="review-buttons-container">
            <div class="primary">
                <button class="action primary checkout"
                        type="submit"
                        data-bind="
                        click: placeOrder,
                        attr: {title: $t('Place Order')},
                        enable: (getCode() == isChecked()),
                        css: {disabled: !isPlaceOrderActionAllowed()}
                        "
                        data-role="review-save">
                    <span data-bind="i18n: 'Place Order'"></span>
                </button>
            </div>
        </div>
    </div>
</div>

The form template will be rendered on the billing step of checkout. You can modify the above template as per your requirement.

Now Run the Command using CLI from Magento root folder,

Install the code and Clean the cache using CLI,

php bin/magento setup:upgrade
php bin/magento cache:clean

The output will look like in checkout page,

Custom Payment Method
Custom Payment Method Using Adapter