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.