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.
For the offline Payment method with the configuration setting from the admin panel, you can enable/disable the 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"/> <module name="Magento_OfflinePayments"/> <module name="Magento_Payment"/> <module name="Magento_Quote"/> </sequence> </module> </config>
Create a system.xml file to Display settings on the Payment methods tab on 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\Newprocessing</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> <field id="ip_address" translate="label" type="textarea" sortOrder="11" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Allowed IP Address</label> <comment>Separate by comma in case of multiple values and leave it empty if you want to allow method for all ip addresses </comment> </field> </group> </section> </system> </config>
All the nodes are self-explanatory. It will be displayed on Stores -> Configuration -> Sales -> Payment Methods section.
sort_order used for the payment method on the billing step position.
order_status will be pending for the Offline payment method.
allowspecific used to display payment for all the countries or select a specific country.
ip_address field used to Restrict IP Address to display this checkout to only specific available IP addresses.
Create a config.xml file to the default value set inside the Payment Configuration step in the Admin Panel.
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>processing</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>
-
-
- model tag is used to display the Payment facade that will be used to create virtualType in the di.xml file.
- order_status is used to show the status of an order after placing an order. (Pending / Processing)
- can_use_internal – Value 1 is used to display the payment method when placing an order from the admin panel.
- can_use_checkout – Value 1 is used to display the payment method on the front-end checkout page.
- is_offline node will notify its offline payment method. you don’t need to call third-party payment API to capture/settle transactions.
-
The Main File is the di.xml declares 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 make 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 the order detail page.
- valueHandlerPool argument used to the 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 { public const CODE = 'custompurchaseorder'; }
Define your payment method code, I have used “custompurchaseorder” as the payment method 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 requirements.
Given Logic is for the restriction of IP addresses to display our custom payment method:
Create events.xml file,
<?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="payment_method_is_active"> <observer name="ip_restriction_payment_method" instance="Jesadiya\PurchaseOrderPayment\Observer\IpBasedPaymentRestriction"/> </event> </config>
To write Logic of IP-based restrictions, We have to create an Observer file,
app/code/Jesadiya/PurchaseOrderPayment/Observer\IpBasedPaymentRestriction.php
<?php declare(strict_types=1); namespace Jesadiya\PurchaseOrderPayment\Observer; use Jesadiya\PurchaseOrderPayment\Model\Ui\ConfigProvider; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Store\Model\ScopeInterface; class IpBasedPaymentRestriction implements ObserverInterface { public function __construct( private readonly ScopeConfigInterface $scopeConfig, private readonly RemoteAddress $remoteAddressFactory ) { } /* * Method will restrict payment methods * 'Purchse order Payment' * based on customer IP address * */ public function execute(Observer $observer) { $method = $observer->getMethodInstance(); $result = $observer->getResult(); if ($method->getCode() === ConfigProvider::CODE) { $allowedIpAddresses = $this->scopeConfig->getValue( 'payment/' . $method->getCode() . '/ip_address', ScopeInterface::SCOPE_STORE ); if (empty($allowedIpAddresses)) { return; } if (!in_array( $this->getIpAddress(), array_map('trim', explode(',', $allowedIpAddresses))) ) { $result->setIsAvailable(false); } } } /** * @return string|null */ private function getIpAddress(): ?string { if (!$this->remoteAddress) { $this->remoteAddress = $this->remoteAddressFactory->getRemoteAddress(); } return $this->remoteAddress; } }
Now Run the Command using CLI from the 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 on the checkout page,