Creating Custom Online Payment Method in Magento 2

In this blog, we will learn to create online payment method. We will implement Stripe payment as a reference. You may change it as per your requirement. It will depend upon the library that the gateway provider gives.

Stripe Payment Gateway has several features, but we will be implementing the most basic ones. We will use the official PHP library of stripe i.e. https://github.com/stripe/stripe-php

It should be noticed that the library of Stripe is managed via Composer so it can be installed via that, alternatively, it can be downloaded and the files should be put in vendor/stripe. If we manage the whole process via composer, then it can be automatically included in the code through autoloader. But if we don’t, then we will have to include it in our code.

We will be creating several files and most of the files will be same as the previous blog. So, we will be dicussing about the important files only. Now, let’s get started:

The structure of the extension will look like this:

1. System.xml

This file is used to create the store configuration part for the extension so that the configuration for setting up the payment method can be done from admin panel easily. We will define several fields like API Key, Supported Credit Card Types etc.

<?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="excellence_stripe" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Stripe</label>
                <comment>
                    <![CDATA[<a href="https://stripe.com/" target="_blank">Click here to sign up for Stripe account</a>]]>
                </comment>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="api_key" translate="label" type="obscure" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Api Key</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
                <field id="debug" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Debug</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="cctypes" translate="label" type="multiselect" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Credit Card Types</label>
                    <source_model>Excellence\Stripe\Model\Source\Cctype</source_model>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                </field>
                <field id="allowspecific" translate="label" type="allowspecific" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>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>Payment from Specific Countries</label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                </field>
                <field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Minimum Order Total</label>
                </field>
                <field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Maximum Order Total</label>
                    <comment>Leave empty to disable limit</comment>
                </field>
            </group>
        </section>
    </system>
</config>

2. Model

We will create a Model for handling the processing of payment and will define this model is the config.xml as explained in the previous blog:

<?php

namespace Excellence\Stripe\Model;

class Payment extends \Magento\Payment\Model\Method\Cc
{
    const CODE = 'excellence_stripe';

    protected $_code = self::CODE;

    protected $_isGateway                   = true;
    protected $_canCapture                  = true;
    protected $_canCapturePartial           = true;
    protected $_canRefund                   = true;
    protected $_canRefundInvoicePartial     = true;

    protected $_stripeApi = false;

    protected $_countryFactory;

    protected $_minAmount = null;
    protected $_maxAmount = null;
    protected $_supportedCurrencyCodes = array('USD');

    protected $_debugReplacePrivateDataKeys = ['number', 'exp_month', 'exp_year', 'cvc'];

    public function __construct(
        \Magento\Framework\Model\Context $context,
        \Magento\Framework\Registry $registry,
        \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
        \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory,
        \Magento\Payment\Helper\Data $paymentData,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Payment\Model\Method\Logger $logger,
        \Magento\Framework\Module\ModuleListInterface $moduleList,
        \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
        \Magento\Directory\Model\CountryFactory $countryFactory,
        \Stripe\Stripe $stripe,
        array $data = array()
    ) {
        parent::__construct(
            $context,
            $registry,
            $extensionFactory,
            $customAttributeFactory,
            $paymentData,
            $scopeConfig,
            $logger,
            $moduleList,
            $localeDate,
            null,
            null,
            $data
        );

        $this->_countryFactory = $countryFactory;

        $this->_stripeApi = $stripe;
        $this->_stripeApi->setApiKey(
            $this->getConfigData('api_key')
        );

        $this->_minAmount = $this->getConfigData('min_order_total');
        $this->_maxAmount = $this->getConfigData('max_order_total');
    }

    /**
     * Payment capturing
     *
     * @param \Magento\Payment\Model\InfoInterface $payment
     * @param float $amount
     * @return $this
     * @throws \Magento\Framework\Validator\Exception
     */
    public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount)
    {
        //throw new \Magento\Framework\Validator\Exception(__('Inside Stripe, throwing donuts :]'));

        /** @var \Magento\Sales\Model\Order $order */
        $order = $payment->getOrder();

        /** @var \Magento\Sales\Model\Order\Address $billing */
        $billing = $order->getBillingAddress();

        try {
            $requestData = [
                'amount'        => $amount * 100,
                'currency'      => strtolower($order->getBaseCurrencyCode()),
                'description'   => sprintf('#%s, %s', $order->getIncrementId(), $order->getCustomerEmail()),
                'card'          => [
                    'number'            => $payment->getCcNumber(),
                    'exp_month'         => sprintf('%02d',$payment->getCcExpMonth()),
                    'exp_year'          => $payment->getCcExpYear(),
                    'cvc'               => $payment->getCcCid(),
                    'name'              => $billing->getName(),
                    'address_line1'     => $billing->getStreetLine(1),
                    'address_line2'     => $billing->getStreetLine(2),
                    'address_city'      => $billing->getCity(),
                    'address_zip'       => $billing->getPostcode(),
                    'address_state'     => $billing->getRegion(),
                    'address_country'   => $billing->getCountryId(),
                    // To get full localized country name, use this instead:
                    // 'address_country'   => $this->_countryFactory->create()->loadByCode($billing->getCountryId())->getName(),
                ]
            ];

            $charge = \Stripe\Charge::create($requestData);
            $payment
                ->setTransactionId($charge->id)
                ->setIsTransactionClosed(0);

        } catch (\Exception $e) {
            $this->debugData(['request' => $requestData, 'exception' => $e->getMessage()]);
            $this->_logger->error(__('Payment capturing error.'));
            throw new \Magento\Framework\Validator\Exception(__('Payment capturing error.'));
        }

        return $this;
    }

    /**
     * Payment refund
     *
     * @param \Magento\Payment\Model\InfoInterface $payment
     * @param float $amount
     * @return $this
     * @throws \Magento\Framework\Validator\Exception
     */
    public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount)
    {
        $transactionId = $payment->getParentTransactionId();

        try {
            \Stripe\Charge::retrieve($transactionId)->refund(['amount' => $amount * 100]);
        } catch (\Exception $e) {
            $this->debugData(['transaction_id' => $transactionId, 'exception' => $e->getMessage()]);
            $this->_logger->error(__('Payment refunding error.'));
            throw new \Magento\Framework\Validator\Exception(__('Payment refunding error.'));
        }

        $payment
            ->setTransactionId($transactionId . '-' . \Magento\Sales\Model\Order\Payment\Transaction::TYPE_REFUND)
            ->setParentTransactionId($transactionId)
            ->setIsTransactionClosed(1)
            ->setShouldCloseParentTransaction(1);

        return $this;
    }

    /**
     * Determine method availability based on quote amount and config data
     *
     * @param \Magento\Quote\Api\Data\CartInterface|null $quote
     * @return bool
     */
    public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null)
    {
        if ($quote && (
            $quote->getBaseGrandTotal() < $this->_minAmount
            || ($this->_maxAmount && $quote->getBaseGrandTotal() > $this->_maxAmount))
        ) {
            return false;
        }

        if (!$this->getConfigData('api_key')) {
            return false;
        }

        return parent::isAvailable($quote);
    }

    /**
     * Availability for currency
     *
     * @param string $currencyCode
     * @return bool
     */
    public function canUseForCurrency($currencyCode)
    {
        if (!in_array($currencyCode, $this->_supportedCurrencyCodes)) {
            return false;
        }
        return true;
    }
}

In this file, there are two main methods:

  1. capture(): This method is handling the process of charging the credit card and saving the transaction. We are just fetching the billing address details from payment object, and the passing it to stripe API along with card details and order amount. The API will process this data and will return a charge object. We are just saving its id as transaction id for the order.
  2. refund(): This method will be used to process the refund. The previously saved transaction id will be sent to the API and then refund will be processed.

You can see the exact files and directory structure over here.

Creating Sandbox Account and Getting API Key:

To Create a sandbox account, go to https://dashboard.stripe.com/register and enter the required details and Sign Up. After creating the account, you will need to access the API key which needs to be entered in Payment Configuration (in Magento Admin Panel). Navigate to Developers -> API Keys. Here you will get two keys, Publishable key and Secret key.

You need to use the Secret key only. Copy it and save somewhere.

Now, we will see where to put the API key and how to do other configurations for the payment method, we have created. For this, navigate to Store -> Configuration -> Payment Methods -> Stripe

Here you can enter the API key and do other settings:

Now let’s see the use of all of the fields one by one:

Enabled: This is used to enable/disable the payment method

Title: The payment method title can be set here. It will be shown on frontend while checkout process on payment step.

API Key: The API key that we had got previously needs to be entered here

Debug: It is an optional field, this needs to be used if we want to make some entries in debug log whenever this payment method is used to complete the checkout process

Credit Card Types: Here we can select the  credit card types which needs to be supported

Payment from Applicable Countries/Payment from Specific Countries: Here we can select for which countries, the payment method should be used and shown on frontend while checkout.

Minimum Order Total/Maximum Order Total: Here we can define the rules based on order total. If the order total is less/more than the defined, then the payment method should be hidden/shown according to this only.

Sort Order: This field need to be manage the orde in which the payment method will be shown in respect of other available payment methods on frontend.