Sometimes you might need more than the standard out-of-the-box payment method. Perhaps you have an agreement with a credit card processor and a solution for their platform is not available. Maybe you need to interject specific logic into the checkout flow to support an ERP. By building a custom payment method, you will be able to customize the fields the user will interact with on checkout and highly customize the API logic used to implement the payment method.
As an example, we’ll look at a scenario where a credit card processor doesn’t have a solution available for Magento 2. So we’ll examine how to add a credit card based payment method to Magento 2. We’ll examine the backend and frontend changes required to make the payment method work and where we can rely on native Magento logic. (Unless otherwise specified, all the following code examples and file paths will be relative to a new Magento module named ClassyLlama_LlamaCoin. For more detailed examples of the code here, you can refer to this GitHub.)
Foundation of a New Payment Method
Configuration
First, in our new module we’ll need to add configuration settings for our payment method. For a custom payment method, you will likely have many additional configuration settings, but here we’re only going to add a few very basic settings. We will need an “enabled” setting, a title to display for our payment method, and a setting for which credit card types we will accept. These will be added in etc/adminhtml/system.xml
. These fields, active
, title
, and cctypes
, are used by the core classes we’re extending so make sure to match the field ids exactly. Additionally, it’s important to make sure these fields are added in the payment
config namespace as Magento references this namespace when loading configuration fields.
<?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="classyllama_llamacoin" translate="label comment" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Llama Coin</label>
<field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Enabled</label>
<source_model>MagentoConfigModelConfigSourceYesno</source_model>
</field>
<field id="title" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Title</label>
</field>
<field id="cctypes" translate="label" type="multiselect" sortOrder="65" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Credit Card Types</label>
<source_model>MagentoPaymentModelSourceCctype</source_model>
</field>
</group>
</section>
</system>
</config>
Now we can define default values for the configuration settings we created. Create a new file in the etc
folder named config.xml
and define your values. There’s an additional field that has been added that we didn’t define in the system.xml
file: the field model
. This field defines the payment method model for our payment method.
<?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>
<classyllama_llamacoin>
<title>Llama Coin</title>
<model>ClassyLlamaLlamaCoinModelLlamaCoin</model>
<active>1</active>
<cctypes>AE,VI,MC,DI</cctypes>
</classyllama_llamacoin>
</payment>
</default>
</config>
Payment Model
Since the config.xml
file tells Magento where to find our payment model class, we want to make sure this class is created: ModelLlamaCoin
. This class is where the main logic that processes the payment information is found. The core Magento class defines several methods that we will want to implement in our class: authorize
and capture
. (There are several other methods defined, like void
, but we won’t be implementing them in this example.) Your payment model must extend MagentoPaymentModelMethodAbstractMethod
; if you’re creating a credit card method, though, you’ll probably want to extend MagentoPaymentModelMethodCc
, which provides helpful logic in the validate
method. We’ll come back to this model and fill it in later.
<?php
namespace ClassyLlamaLlamaCoinModel;
class LlamaCoin extends MagentoPaymentModelMethodCc
{
public function capture(MagentoPaymentModelInfoInterface $payment, $amount)
{
//todo add functionality later
}
public function authorize(MagentoPaymentModelInfoInterface $payment, $amount)
{
//todo add functionality later
}
}
After creating the system.xml
and config.xml
files and defining the base class for our payment method, we can now see our configuration settings in the admin. (Found at Stores > Configuration > Sales > Payment Methods
)
Accessing the Configuration Data
On the frontend, we’ll need to give the checkout interface access to our payment method’s configuration fields. To provide this information, we must define a PHP provider class. Magento will use this provider class to store all the checkout-related configuration in the window.checkoutConfig
Javascript object. The provider we create will need to implement the MagentoCheckoutModelConfigProviderInterface
interface and use the getConfig
method to return the data we configured. However, Magento has built a generic class, MagentoPaymentModelCcGenericConfigProvider
, that implements this required interface and additionally goes through all the available payment methods and gathers all the configuration fields we’ll need. This means we can simply inject our payment method into this CcGenericConfigProvider
and it will load our data to the window.checkoutConfig
object. Create a new file etc/frontend/di.xml
and add our payment method code as an argument.
<?xml version="1.0"?>
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="MagentoPaymentModelCcGenericConfigProvider">
<arguments>
<argument name="methodCodes" xsi_type="array">
<item name="classyllama_llamacoin" xsi_type="const">ClassyLlamaLlamaCoinModelLlamaCoin::CODE</item>
</argument>
</arguments>
</type>
</config>
We have the foundation of our custom payment method in place, and the frontend will be able to access our data, so now we can display the payment method on the frontend.
Displaying the New Payment During Checkout
We’re adding a payment method that will utilize credit cards to checkout. Since Magento natively has several payment methods with credit card templates, we can utilize several core templates to render our payment method.
Javascript
There are two important JS files to add: the payment method renderer and the component that registers the renderer. The contents of the component file, view/frontend/web/js/view/payment/llamacoin.js
, are rather simple and only serve to add the renderer:
define([
'uiComponent',
'Magento_Checkout/js/model/payment/renderer-list'
],
function (Component, rendererList) {
'use strict';
rendererList.push(
{
type: 'classyllama_llamacoin',
component: 'ClassyLlama_LlamaCoin/js/view/payment/method-renderer/llamacoin'
}
);
/** Add view logic here if needed */
return Component.extend({});
});
The file path of the renderer file is view/frontend/web/js/view/payment/method-renderer/
. This file handles the frontend logic specific to the new payment method: validation on the form fields and accessor methods for the Knockout template data. In its simplest form, the renderer only needs to include the path to the Knockout template, and this renderer can depend on Magento_Checkout/js/view/payment/default
. Since our payment method uses credit card fields, we can extend Magento_Payment/js/view/payment/cc-form
. This base JavaScript file (and its associated Knockout template, cc-form.html
) provide a form that contains the basic fields and validation for the credit card fields (number, date, type, year, and csv). By depending on these forms, you can leverage a great deal of basic functionality and then customize it to your needs.
define([
'jquery',
'Magento_Payment/js/view/payment/cc-form'
],
function ($, Component) {
'use strict';
return Component.extend({
defaults: {
template: 'ClassyLlama_LlamaCoin/payment/llamacoin'
},
context: function() {
return this;
},
getCode: function() {
return 'classyllama_llamacoin';
},
isActive: function() {
return true;
}
});
}
);
Templates
The JS renderer uses Knockout to render the uiComponent. The native templates used in this rendering process are found in the Magento_Payment/view/frontend/web/template/payment/
folder. But we’ll need to add one template, effectively a wrapper around our form, to contain a radio button, title, and billing address for our payment method. Create the new template at /view/frontend/web/template/payment/llamacoin.html
. In this template, we’re using Knockout to render the native credit card form and a place to display the address. (This new template is heavily based off the native free.html
template in the location referenced above.)
<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>
<!-- Render the native credit card form. -->
<form class="form" data-bind="attr: {'id': getCode() + '-form'}">
<!-- ko template: 'Magento_Payment/payment/cc-form' --><!-- /ko -->
</form>
<div class="checkout-agreements-block">
<!-- ko foreach: $parent.getRegion('before-place-order') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
</div>
<div class="actions-toolbar">
<div class="primary">
<button data-role="review-save"
type="submit"
data-bind="
attr: {title: $t('Place Order')},
enable: (getCode() == isChecked()),
click: placeOrder,
css: {disabled: !isPlaceOrderActionAllowed()}
"
class="action primary checkout"
disabled>
<span data-bind="i18n: 'Place Order'"></span>
</button>
</div>
</div>
</div>
</div>
If you need to customize the fields on the credit card form, simply copy forward the native cc-form.html
file to your module, update the reference in your JavaScript renderer file, and make your changes to the new template.
Layout
Finally, we need to tell Magento where to include these JavaScript files. Create a new layout file view/frontend/layout/checkout_index_index.xml
. (Sections of the file below have been left out. For the entire file, view checkout_index_index.xml
in the GitHub example I’ve created.) In this layout file, the new payment method is registered in layout so that it will be included on the checkout page. This XML file contains a reference to the JavaScript component we created.
<?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>
<!-- ... several nodes left out for readability ... -->
<!-- merge payment method renders here -->
<item name="children" xsi_type="array">
<item name="llamacoin-payments" xsi_type="array">
<item name="component" xsi_type="string">ClassyLlama_LlamaCoin/js/view/payment/llamacoin</item>
<item name="methods" xsi_type="array">
<item name="classyllama_llamacoin" xsi_type="array">
<item name="isBillingAddressRequired" xsi_type="boolean">true</item>
</item>
</item>
</item>
</item>
<!-- ... several nodes left out for readability ... -->
</arguments>
</referenceBlock>
</body>
</page>
Now our payment method will be an option during checkout.
Now in order to make our payment method work, we’ll need to update our payment class with logic on how to handle the submitted data.
Payment Method Model
Before our payment model LlamaCoin
can be used to process an order, we need to update a few properties: $_canCapture
and $_canAuthorize
. The payment abstract method class (MagentoPaymentModelMethodAbstractMethod
) checks these properties before calling the associated methods on our payment model instance. Since we’ll be implementing a capture
and authorize
method, we will want to change these properties to be true
. If your payment method implements methods for voids, refunds, or partial captures, you’ll want to set the corresponding property on your payment method instance. Look through the AbstractMethod
class to be familiar with all the available properties.
We’ll need one more property: $_code
. The value of this property will be the payment method code that we’re using: “classyllama_llamacoin”. The AbstractMethod
class uses this property in the getCode
method which is called any time Magento needs to get the payment model code. This means it’s important that the $_code
property matches the section name we defined in system.xml
. This is used several times, one of which is to set and save the payment method code on the order object.
Now that these properties are in place, we can look at the logic around how an order is processed. When an order is placed, the place
method in the MagentoSalesModelOrderPayment
class decides how to handle the payment. Several important steps happen during this method:
- The
place
method finds an instance of our payment method model. - The payment is validated. This validation happens in the
validate
method on the payment model. Since we extended theMagentoPaymentModelMethodCc
class, this parent class is used to validate our payment. To customize the validation logic, simply define avalidate
method in your payment class. - Payment Action. This method determines how the payment will be handled. By default, there are three actions defined as constants on
MagentoPaymentModelMethodAbstractMethod
: order, authorize, and authorize_capture. We’ll need to implement thegetConfigPaymentAction
method in our class and return the action we want to use. (This option can be made configurable. The Authorize.Net module adds a source model for payment action to make it a configurable field.) - The
authorize
andcapture
methods are called. (See full payment model class below.) - We will use two API calls. One to authorize the amount, and one to capture the amount.
- Two parameters are passed to the authorize and capture methods. An amount (the total price of the order) and a
$payment
object which implements theMagentoPaymentModelInfoInterface
interface. This$payment
object will have all data we need to reference set on it. (Credit card data, address info, and order data.) We can now build an array of data to pass to the credit card processor. makeCaptureRequest
andmakeAuthRequest
have been implemented as placeholder methods that return test data. In reality, these methods would be where you implement specific logic to reach out to the API of a credit card processor.- The
capture
method first checks to make sure anauthorize
request has been successful. Once authorization and capture happen, the status of the payment is updated by settingsetIsTransationClosed
totrue
. - After the
authorize
andcapture
methods, Magento updates the order status and saves the payment data to the database. The order process is now complete!
Here is the final state of the payment method model:
<?php
namespace ClassyLlamaLlamaCoinModel;
class LlamaCoin extends MagentoPaymentModelMethodCc
{
const CODE = 'classyllama_llamacoin';
protected $_code = self::CODE;
protected $_canAuthorize = true;
protected $_canCapture = true;
/**
* Capture Payment.
*
* @param MagentoPaymentModelInfoInterface $payment
* @param float $amount
* @return $this
*/
public function capture(MagentoPaymentModelInfoInterface $payment, $amount)
{
try {
//check if payment has been authorized
if(is_null($payment->getParentTransactionId())) {
$this->authorize($payment, $amount);
}
//build array of payment data for API request.
$request = [
'capture_amount' => $amount,
//any other fields, api key, etc.
];
//make API request to credit card processor.
$response = $this->makeCaptureRequest($request);
//todo handle response
//transaction is done.
$payment->setIsTransactionClosed(1);
} catch (Exception $e) {
$this->debug($payment->getData(), $e->getMessage());
}
return $this;
}
/**
* Authorize a payment.
*
* @param MagentoPaymentModelInfoInterface $payment
* @param float $amount
* @return $this
*/
public function authorize(MagentoPaymentModelInfoInterface $payment, $amount)
{
try {
///build array of payment data for API request.
$request = [
'cc_type' => $payment->getCcType(),
'cc_exp_month' => $payment->getCcExpMonth(),
'cc_exp_year' => $payment->getCcExpYear(),
'cc_number' => $payment->getCcNumberEnc(),
'amount' => $amount
];
//check if payment has been authorized
$response = $this->makeAuthRequest($request);
} catch (Exception $e) {
$this->debug($payment->getData(), $e->getMessage());
}
if(isset($response['transactionID'])) {
// Successful auth request.
// Set the transaction id on the payment so the capture request knows auth has happened.
$payment->setTransactionId($response['transactionID']);
$payment->setParentTransactionId($response['transactionID']);
}
//processing is not done yet.
$payment->setIsTransactionClosed(0);
return $this;
}
/**
* Set the payment action to authorize_and_capture
*
* @return string
*/
public function getConfigPaymentAction()
{
return self::ACTION_AUTHORIZE_CAPTURE;
}
/**
* Test method to handle an API call for authorization request.
*
* @param $request
* @return array
* @throws MagentoFrameworkExceptionLocalizedException
*/
public function makeAuthRequest($request)
{
$response = ['transactionId' => 123]; //todo implement API call for auth request.
if(!$response) {
throw new MagentoFrameworkExceptionLocalizedException(__('Failed auth request.'));
}
return $response;
}
/**
* Test method to handle an API call for capture request.
*
* @param $request
* @return array
* @throws MagentoFrameworkExceptionLocalizedException
*/
public function makeCaptureRequest($request)
{
$response = ['success']; //todo implement API call for capture request.
if(!$response) {
throw new MagentoFrameworkExceptionLocalizedException(__('Failed capture request.'));
}
return $response;
}
}
Conclusion
We’ve completed building a custom payment method for Magento. We built the foundational backend configuration settings and model, then we added the frontend changes necessary to display the payment method to the customer, and then added logic to have the model process the payment data. This tutorial can be used as a basis for adding payment methods to Magento. There are many ways to built upon what we’ve done to provide the best experience for the merchant and their customers.
I’ve added all the code for this example to this GitHub. (This example was built and tested on Magento 2.2.2.) Additionally, the Braintree and Authorize.Net modules in the core Magento code provide great examples of how several of these files interact.