How to create an off-site payment extension in Opencart 3? Part II of III

In this Opencart tutorial, we go through how to create an off-site payment extension in Opencart 3.0.3.3, in our last tutorial, we show the eCommerce payment flow that happens in Opencart, here we will show how to create the PayPal Standard payment extension, as all 3rd party extensions are removed from the download files, saying it is taking too much time to maintain but these will be uploaded to opencart.com and will be provided in a JSON string to be downloaded on demand.

SEE Difference between Off-site payment extension and On-site payment extension

Here is the step by step guide to integrate PayPal Standard in your website and here are the HTML variables than can be used on form for PayPal Standard.

For OpenCart payment extension, here are the lists of files you need to create, the link is for GitHub page where you can get all the codes, below we are just describing what is important:

Here files are folders in the admin section is to create the setting fields for the payment module, for PayPal Standard seeing the HTML form:

https://developer.paypal.com/docs/paypal-payments-standard/integration-guide/formbasics/#sample-html-code-for-overriding-addresses-stored-with-paypal and https://developer.paypal.com/docs/paypal-payments-standard/ht-test-pps-buttons/#create-test-buttons-in-the-sandbox

We see the form action URL is different so that field setting is needed in the admin, merchant email or business email is need where the payment is made, transaction method whether it is an Authorization or Sale transactions, Total field, Geo Zone field, Status, Sort Order field and order status fields are needed. You can know which fields are needed by going through the documentation of the Payment method.

So, for PayPal Standard here is the output for the admin section:

Paypal Standard Settings
Paypal Order Status

Let’s start with the admin controller file of Payment extension:

Open admin >> controller >> extension >> payment folder and create the paypal_standard.php, for yours you can name what your payment name is. In the file name _ is not needed, we add it just to differentiate it. Now open the paypal_standard.php and start creating the Controller Class.

As our file name is paypal_standard.php, the class name is ControllerExtensionPaymentPaypalStandard which extends the Controller base class. Although the filename includes _ (underscore), it is not needed for the Class name.

class ControllerExtensionPaymentPaypalStandard extends Controller {

Declaration of the private property ‘error’ so that we can get check if any error occurs in the whole class.

private $error = array();

Create an index method. The index method is called automatically if no parameters are passed, check this video tutorial for details https://www.youtube.com/watch?v=X6bsMmReT-4

public function index() {

Loads the language file by which the variables of language file are accessible in twig files

$this->load->language('extension/payment/paypal_standard');

Set the page or document title with following code:

$this->document->setTitle($this->language->get('heading_title'));

Loads the model admin/model/setting/setting.php so that we can use the methods defined there.

$this->load->model('setting/setting');

This is how we check if the form is submitted. When we submit the form then this block of code also runs. Then it also validates the modified permission and other validation.

if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) {

Look at this line of codes, this is the code section which distinguishes from Single Instance to Multi-Instance. If it is a payment extension then it acts as a Single instance module, which will have editSetting function, see the first parameters “payment_paypal_standard”, which need to be unique and it should start with “payment_”, all $_POST values will be saved on the setting database table. Then, it sets the success message in session and then redirects to the payment listing page.

if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) {
    $this->model_setting_setting->editSetting('payment_paypal_standard', $this->request->post);
    $this->session->data['success'] = $this->language->get('text_success');
    $this->response->redirect($this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true));
}

In the database setting table, the code is “payment_paypal_standard” and have the key which is the field name and value is the field value.

Opencart setting database payment paypal standard

The editSetting saves the data to oc_setting database table, see payment_ is important else it will not be saved.
The following code sets the success message in the session.

$this->session->data['success'] = $this->language->get('text_success');

This code is to redirect to the payment extensions listing page.

$this->response->redirect($this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true));

This is to check if there are any warnings

if (isset($this->error['warning'])) {
$data['error_warning'] = $this->error['warning'];
} else {
$data['error_warning'] = '';
}

Similarly, other fields value error are checked, like for email error it is checked with the following code:

if (isset($this->error['email'])) {
$data['error_email'] = $this->error['email'];
} else {
$data['error_email'] = '';
}

Following are for breadcrumbs

$data['breadcrumbs'] = array();
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/module/login', 'user_token=' . $this->session->data['user_token'], true)
);

Form action URL

$data['action'] = $this->url->link('extension/payment/paypal_standard', 'user_token=' . $this->session->data['user_token'], true);

Form cancel URL

$data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true);

This is to check what we fill out in the form, whether the email is added. If it is the loading time then it gets the value from the database config value which was stored in the oc_setting database table.

if (isset($this->request->post['payment_paypal_standard_email'])) {
$data['payment_paypal_standard_email'] = $this->request->post['payment_paypal_standard_email'];
} else {
$data['payment_paypal_standard_email'] = $this->config->get('payment_paypal_standard_email');
}

Similarly, other fields are checked, like for fields payment_paypal_standard_test, payment_paypal_standard_transaction, payment_paypal_standard_debug, payment_paypal_standard_total, payment_paypal_standard_canceled_reversal_status_id, payment_paypal_standard_completed_status_id, payment_paypal_standard_denied_status_id, payment_paypal_standard_expired_status_id, payment_paypal_standard_failed_status_id, payment_paypal_standard_pending_status_id, payment_paypal_standard_processed_status_id, payment_paypal_standard_refunded_status_id, payment_paypal_standard_reversed_status_id, payment_paypal_standard_voided_status_id, payment_paypal_standard_status, and payment_paypal_standard_sort_order. You can check those all opencart codes here.

Mostly, we use geo zones for payment extensions so that we can control as per the country or zones. So to pull the geo zones in opencart we use the following code:

$this->load->model('localisation/geo_zone');
$data['geo_zones'] = $this->model_localisation_geo_zone->getGeoZones();

This is how we load the header, column left, and footer and pass to the template file.

$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');

This is to set output data variables to the view or twig files and twig file is loaded and HTML rendering is done with it.

$this->response->setOutput($this->load->view('extension/payment/paypal_standard', $data));

In this way the index method is closed.

Now, we see the validate method, this is how validation is done, we check whether the user has permission to modify or not. If you want to validate the form data then you can check it here as well. If there is an error then $this->error[‘warning’] is set and a warning is shown. We check whether the Paypal email is added or not. Similarly, you can check other validation here.

private function validate()
{
if (!$this->user->hasPermission('modify', 'extension/payment/paypal_standard')) {
$this->error['warning'] = $this->language->get('error_permission');
}
if (!$this->request->post['payment_paypal_standard_email']) {
$this->error['email'] = $this->language->get('error_email');
}
return !$this->error;
}

In this way, we write the code in the controller for the payment extension module at admin section.

Now, admin language file of Payment extension:

Let’s work on Language file admin/language/en-gb/extension/payment/paypal_standard.php, let’s define some variables which are needed for the Paypal standard payment extension:
https://github.com/rupaknepali/Opencart-free-modules/blob/master/paypal-standard-opencart-extension/upload/admin/language/en-gb/extension/payment/paypal_standard.php

<?php
// Heading
$_['heading_title']	= 'PayPal Payments Standard';

// Text
$_['text_extension']	= 'Extensions';
$_['text_success']	= 'Success: You have modified PayPal account details!';
$_['text_edit']            = 'Edit PayPal Payments Standard';
$_['text_paypal_standard']= '<a target="_BLANK" href="https://www.paypal.com/uk/mrb/pal=V4T754QB63XXL"><img src="view/image/payment/paypal.png" alt="PayPal Payments Standard" title="PayPal Payments Standard" style="border: 1px solid #EEEEEE;" /></a>';
$_['text_authorization']= 'Authorization';
$_['text_sale']		   = 'Sale'; 

// Entry
$_['entry_email']   = 'E-Mail';
$_['entry_test']	    = 'Sandbox Mode';
$_['entry_transaction'] = 'Transaction Method';
$_['entry_debug']  = 'Debug Mode';
$_['entry_total'].    = 'Total';
$_['entry_canceled_reversal_status'] = 'Canceled Reversal Status';
$_['entry_completed_status']		 = 'Completed Status';
$_['entry_denied_status']			 = 'Denied Status';
$_['entry_expired_status']			 = 'Expired Status';
$_['entry_failed_status']			 = 'Failed Status';
$_['entry_pending_status']			 = 'Pending Status';
$_['entry_processed_status']		 = 'Processed Status';
$_['entry_refunded_status']		 = 'Refunded Status';
$_['entry_reversed_status']		 = 'Reversed Status';
$_['entry_voided_status']			 = 'Voided Status';
$_['entry_geo_zone']				 = 'Geo Zone';
$_['entry_status']				 = 'Status';
$_['entry_sort_order']			 = 'Sort Order';

// Tab
$_['tab_general']			= 'General';
$_['tab_order_status']       		= 'Order Status';

// Help
$_['help_test']	= 'Use the live or testing (sandbox) gateway server to process transactions?';
$_['help_debug'] = 'Logs additional information to the system log';
$_['help_total']	= 'The checkout total the order must reach before this payment method becomes active';

// Error
$_['error_permission']= 'Warning: You do not have permission to modify payment PayPal Payments Standard!';
$_['error_email']  = 'E-Mail required!';

In the above code, you can find the following code:

$_['text_paypal_standard']= '<a target="_BLANK" href="https://www.paypal.com/uk/mrb/pal=V4T754QB63XXL"><img src="view/image/payment/paypal.png" alt="PayPal Payments Standard" title="PayPal Payments Standard" style="border: 1px solid #EEEEEE;" /></a>';

This is the code which shows the payment logo at the payment extension listings.

Paypal Payments standard image

Now, admin template file of Payment extension:

Third, open admin/view/template/extension/payment and create paypal_standard.twig. Here we create the form and other layouts. You can see the code here: https://github.com/rupaknepali/Opencart-free-modules/blob/master/paypal-standard-opencart-extension/upload/admin/view/template/extension/payment/paypal_standard.twig

Be sure to start the name with “payment_” for every fields name.

{{ header }}{{ column_left }}
<div id="content">
  <div class="page-header">
    <div class="container-fluid">
      <div class="pull-right">
        <button type="submit" form="form-payment" data-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary"><i class="fa fa-save"></i></button>
        <a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i class="fa fa-reply"></i></a></div>
      <h1>{{ heading_title }}</h1>
      <ul class="breadcrumb">
        {% for breadcrumb in breadcrumbs %}
        <li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
        {% endfor %}
      </ul>
    </div>
  </div>
  <div class="container-fluid">
    {% if error_warning %}
    <div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> {{ error_warning }}
      <button type="button" class="close" data-dismiss="alert">×</button>
    </div>
    {% endif %}
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title"><i class="fa fa-pencil"></i> {{ text_edit }}</h3>
      </div>
      <div class="panel-body">
        <form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-payment" class="form-horizontal">
          <ul class="nav nav-tabs">
            <li class="active"><a href="#tab-general" data-toggle="tab">{{ tab_general }}</a></li>
            <li><a href="#tab-status" data-toggle="tab">{{ tab_order_status }}</a></li>
          </ul>
          <div class="tab-content">
            <div class="tab-pane active" id="tab-general">
              <div class="form-group required">
                <label class="col-sm-2 control-label" for="entry-email">{{ entry_email }}</label>
                <div class="col-sm-10">
                  <input type="text" name="payment_paypal_standard_email" value="{{ payment_paypal_standard_email }}" placeholder="{{ entry_email }}" id="entry-email" class="form-control"/>
                  {% if error_email %}
                  <div class="text-danger">{{ error_email }}</div>
                  {% endif %}
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-live-demo"><span data-toggle="tooltip" title="{{ help_test }}">{{ entry_test }}</span></label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_test" id="input-live-demo" class="form-control">
                    {% if payment_paypal_standard_test %}
                    <option value="1" selected="selected">{{ text_yes }}</option>
                    <option value="0">{{ text_no }}</option>
                    {% else %}
                    <option value="1">{{ text_yes }}</option>
                    <option value="0" selected="selected">{{ text_no }}</option>
                    {% endif %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-debug"><span data-toggle="tooltip" title="{{ help_debug }}">{{ entry_debug }}</span></label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_debug" id="input-debug" class="form-control">
                    {% if payment_paypal_standard_debug %}
                    <option value="1" selected="selected">{{ text_enabled }}</option>
                    <option value="0">{{ text_disabled }}</option>
                    {% else %}
                    <option value="1">{{ text_enabled }}</option>
                    <option value="0" selected="selected">{{ text_disabled }}</option>
                    {% endif %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-transaction">{{ entry_transaction }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_transaction" id="input-transaction" class="form-control">
                    {% if not payment_paypal_standard_transaction %}
                    <option value="0" selected="selected">{{ text_authorization }}</option>
                    {% else %}
                    <option value="0">{{ text_authorization }}</option>
                    {% endif %}
                    {% if payment_paypal_standard_transaction %}
                    <option value="1" selected="selected">{{ text_sale }}</option>
                    {% else %}
                    <option value="1">{{ text_sale }}</option>
                    {% endif %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-total"><span data-toggle="tooltip" title="{{ help_total }}">{{ entry_total }}</span></label>
                <div class="col-sm-10">
                  <input type="text" name="payment_paypal_standard_total" value="{{ payment_paypal_standard_total }}" placeholder="{{ entry_total }}" id="input-total" class="form-control"/>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-geo-zone">{{ entry_geo_zone }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_geo_zone_id" id="input-geo-zone" class="form-control">
                    <option value="0">{{ text_all_zones }}</option>
                    {% for geo_zone in geo_zones %}
                    {% if geo_zone.geo_zone_id == payment_paypal_standard_geo_zone_id %}
                    <option value="{{ geo_zone.geo_zone_id }}" selected="selected">{{ geo_zone.name }}</option>
                    {% else %}
                    <option value="{{ geo_zone.geo_zone_id }}">{{ geo_zone.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-status">{{ entry_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_status" id="input-status" class="form-control">
                    {% if payment_paypal_standard_status %}
                    <option value="1" selected="selected">{{ text_enabled }}</option>
                    <option value="0">{{ text_disabled }}</option>
                    {% else %}
                    <option value="1">{{ text_enabled }}</option>
                    <option value="0" selected="selected">{{ text_disabled }}</option>
                    {% endif %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-sort-order">{{ entry_sort_order }}</label>
                <div class="col-sm-10">
                  <input type="text" name="payment_paypal_standard_sort_order" value="{{ payment_paypal_standard_sort_order }}" placeholder="{{ entry_sort_order }}" id="input-sort-order" class="form-control"/>
                </div>
              </div>
            </div>
            <div class="tab-pane" id="tab-status">
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-canceled-reversal-status">{{ entry_canceled_reversal_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_canceled_reversal_status_id" id="input-canceled-reversal-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_canceled_reversal_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-completed-status">{{ entry_completed_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_completed_status_id" id="input-completed-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_completed_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-denied-status">{{ entry_denied_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_denied_status_id" id="input-denied-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_denied_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-expired-status">{{ entry_expired_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_expired_status_id" id="input-expired-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_expired_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-failed-status">{{ entry_failed_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_failed_status_id" id="input-failed-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_failed_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-pending-status">{{ entry_pending_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_pending_status_id" id="input-pending-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_pending_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-processed-status">{{ entry_processed_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_processed_status_id" id="input-processed-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_processed_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-refunded-status">{{ entry_refunded_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_refunded_status_id" id="input-refunded-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_refunded_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-reversed-status">{{ entry_reversed_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_reversed_status_id" id="input-reversed-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_reversed_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-void-status">{{ entry_voided_status }}</label>
                <div class="col-sm-10">
                  <select name="payment_paypal_standard_voided_status_id" id="input-void-status" class="form-control">
                    {% for order_status in order_statuses %}
                    {% if order_status.order_status_id == payment_paypal_standard_voided_status_id %}
                    <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
                    {% else %}
                    <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
                    {% endif %}
                    {% endfor %}
                  </select>
                </div>
              </div>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
{{ footer }}

We describe some important code only as mostly they are same.

Let’s start with the submit or save button code:

<button type="submit" form="form-payment" data-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary"><i class="fa fa-save"></i></button>

The button type is submitted, and form equals value needs to be the form id. The form id is form-payment that is why it is, form=”form-payment”, the form code is like below:

<form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-payment" class="form-horizontal">

Following is the code to create a tab in the Opencart admin section, as they use the bootstrap v3.3.7 so it is the same as bootstrap.

<ul class="nav nav-tabs">
  <li class="active"><a href="#tab-general" data-toggle="tab">{{ tab_general }}</a></li>
  <li><a href="#tab-status" data-toggle="tab">{{ tab_order_status }}</a></li>
</ul>

We create the form email field like below, similarly we create other fields.

<div class="form-group required">
  <label class="col-sm-2 control-label" for="entry-email">{{ entry_email }}</label>
  <div class="col-sm-10">
    <input type="text" name="payment_paypal_standard_email" value="{{ payment_paypal_standard_email }}" placeholder="{{ entry_email }}" id="entry-email" class="form-control"/>
  </div>
</div>

If some error need to show then here is the code that we set in the controller for the email.

{% if error_email %}
  <div class="text-danger">{{ error_email }}</div>
{% endif %}

Another code to see is the geo zones code, here the geo_zones is for loop to show lists of geo zones available.

 <select name="payment_paypal_standard_geo_zone_id" id="input-geo-zone" class="form-control">
    <option value="0">{{ text_all_zones }}</option>
    {% for geo_zone in geo_zones %}
        {% if geo_zone.geo_zone_id == payment_paypal_standard_geo_zone_id %}
            <option value="{{ geo_zone.geo_zone_id }}" selected="selected">{{ geo_zone.name }}</option>
        {% else %}
            <option value="{{ geo_zone.geo_zone_id }}">{{ geo_zone.name }}</option>
        {% endif %}
    {% endfor %}
</select>

Likewise, look at Order status tab, the code is similar for all the statuses, the select field name starts with payment_ and it loops order_statuses and the selected order status is saved in the database setting table.

<select name="payment_paypal_standard_canceled_reversal_status_id" id="input-canceled-reversal-status" class="form-control">
    {% for order_status in order_statuses %}
        {% if order_status.order_status_id == payment_paypal_standard_canceled_reversal_status_id %}
            <option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
        {% else %}
            <option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
        {% endif %}
    {% endfor %}
</select>

This way you can check other codes and let us know in a comment if you need any code explanation.

With above four files created, our admin side coding is completed.

Now, catalog language file of Payment extension:

Let’s start with the language file catalog/language/en-gb/extension/payment and create paypal_standard.php, let’s define some variables which are useful for module

<?php
// Text
$_['text_title']	= 'PayPal';
$_['text_testmode']	= 'Warning: The payment gateway is in \'Sandbox Mode\'. Your account will not be charged.';
$_['text_total']	= 'Shipping, Handling, Discounts & Taxes';

Now, catalog controller file of Payment extension:

The following code is of front end controller file, go to catalog/controller/extension/payment and create paypal_standard.php and paste following code, here there are two methods index method and callback method.

<?php
class ControllerExtensionPaymentPaypalStandard extends Controller
{
    public function index()
    {
        $this->load->language('extension/payment/paypal_standard');

        $data['text_testmode'] = $this->language->get('text_testmode');
        $data['button_confirm'] = $this->language->get('button_confirm');

        $data['testmode'] = $this->config->get('payment_paypal_standard_test');

        if (!$this->config->get('payment_paypal_standard_test')) {
            $data['action'] = 'https://www.paypal.com/cgi-bin/webscr';
        } else {
            $data['action'] = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
        }

        $this->load->model('checkout/order');

        $order_info = $this->model_checkout_order->getOrder($this->session->data['order_id']);

        if ($order_info) {
            $data['business'] = $this->config->get('payment_paypal_standard_email');
            $data['item_name'] = html_entity_decode($this->config->get('config_name'), ENT_QUOTES, 'UTF-8');

            $data['products'] = array();

            foreach ($this->cart->getProducts() as $product) {
                $option_data = array();

                foreach ($product['option'] as $option) {
                    if ($option['type'] != 'file') {
                        $value = $option['value'];
                    } else {
                        $upload_info = $this->model_tool_upload->getUploadByCode($option['value']);

                        if ($upload_info) {
                            $value = $upload_info['name'];
                        } else {
                            $value = '';
                        }
                    }

                    $option_data[] = array(
                        'name' => $option['name'],
                        'value' => (utf8_strlen($value) > 20 ? utf8_substr($value, 0, 20) . '..' : $value),
                    );
                }

                $data['products'][] = array(
                    'name' => htmlspecialchars($product['name']),
                    'model' => htmlspecialchars($product['model']),
                    'price' => $this->currency->format($product['price'], $order_info['currency_code'], false, false),
                    'quantity' => $product['quantity'],
                    'option' => $option_data,
                    'weight' => $product['weight'],
                );
            }

            $data['discount_amount_cart'] = 0;

            $total = $this->currency->format($order_info['total'] - $this->cart->getSubTotal(), $order_info['currency_code'], false, false);

            if ($total > 0) {
                $data['products'][] = array(
                    'name' => $this->language->get('text_total'),
                    'model' => '',
                    'price' => $total,
                    'quantity' => 1,
                    'option' => array(),
                    'weight' => 0,
                );
            } else {
                $data['discount_amount_cart'] -= $total;
            }

            $data['currency_code'] = $order_info['currency_code'];
            $data['first_name'] = $order_info['payment_firstname'];
            $data['last_name'] = $order_info['payment_lastname'];
            $data['address1'] = $order_info['payment_address_1'];
            $data['address2'] = $order_info['payment_address_2'];
            $data['city'] = $order_info['payment_city'];
            $data['zip'] = $order_info['payment_postcode'];
            $data['country'] = $order_info['payment_iso_code_2'];
            $data['email'] = $order_info['email'];
            $data['invoice'] = $this->session->data['order_id'] . ' - ' . $order_info['payment_firstname'] . ' ' . $order_info['payment_lastname'];
            $data['lc'] = $this->session->data['language'];
            $data['return'] = $this->url->link('checkout/success');
            $data['notify_url'] = $this->url->link('extension/payment/paypal_standard/callback', '', true);
            $data['cancel_return'] = $this->url->link('checkout/checkout', '', true);

            if (!$this->config->get('payment_paypal_standard_transaction')) {
                $data['paymentaction'] = 'authorization';
            } else {
                $data['paymentaction'] = 'sale';
            }

            $data['custom'] = $this->session->data['order_id'];

            return $this->load->view('extension/payment/paypal_standard', $data);
        }
    }

    public function callback()
    {
        if (isset($this->request->post['custom'])) {
            $order_id = $this->request->post['custom'];
        } else {
            $order_id = 0;
        }

        $this->load->model('checkout/order');

        $order_info = $this->model_checkout_order->getOrder($order_id);

        if ($order_info) {
            $request = 'cmd=_notify-validate';

            foreach ($this->request->post as $key => $value) {
                $request .= '&' . $key . '=' . urlencode(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
            }

            if (!$this->config->get('payment_paypal_standard_test')) {
                $curl = curl_init('https://www.paypal.com/cgi-bin/webscr');
            } else {
                $curl = curl_init('https://www.sandbox.paypal.com/cgi-bin/webscr');
            }

            curl_setopt($curl, CURLOPT_POST, true);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, false);
            curl_setopt($curl, CURLOPT_TIMEOUT, 30);
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);

            $response = curl_exec($curl);

            if (!$response) {
                $this->log->write('paypal_standard :: CURL failed ' . curl_error($curl) . '(' . curl_errno($curl) . ')');
            }

            if ($this->config->get('payment_paypal_standard_debug')) {
                $this->log->write('paypal_standard :: IPN REQUEST: ' . $request);
                $this->log->write('paypal_standard :: IPN RESPONSE: ' . $response);
            }

            if ((strcmp($response, 'VERIFIED') == 0 || strcmp($response, 'UNVERIFIED') == 0) && isset($this->request->post['payment_status'])) {
                $order_status_id = $this->config->get('config_order_status_id');

                switch ($this->request->post['payment_status']) {
                    case 'Canceled_Reversal':
                        $order_status_id = $this->config->get('payment_paypal_standard_canceled_reversal_status_id');
                        break;
                    case 'Completed':
                        $receiver_match = (strtolower($this->request->post['receiver_email']) == strtolower($this->config->get('payment_paypal_standard_email')));

                        $total_paid_match = ((float) $this->request->post['mc_gross'] == $this->currency->format($order_info['total'], $order_info['currency_code'], $order_info['currency_value'], false));

                        if ($receiver_match && $total_paid_match) {
                            $order_status_id = $this->config->get('payment_paypal_standard_completed_status_id');
                        }

                        if (!$receiver_match) {
                            $this->log->write('paypal_standard :: RECEIVER EMAIL MISMATCH! ' . strtolower($this->request->post['receiver_email']));
                        }

                        if (!$total_paid_match) {
                            $this->log->write('paypal_standard :: TOTAL PAID MISMATCH! ' . $this->request->post['mc_gross']);
                        }
                        break;
                    case 'Denied':
                        $order_status_id = $this->config->get('payment_paypal_standard_denied_status_id');
                        break;
                    case 'Expired':
                        $order_status_id = $this->config->get('payment_paypal_standard_expired_status_id');
                        break;
                    case 'Failed':
                        $order_status_id = $this->config->get('payment_paypal_standard_failed_status_id');
                        break;
                    case 'Pending':
                        $order_status_id = $this->config->get('payment_paypal_standard_pending_status_id');
                        break;
                    case 'Processed':
                        $order_status_id = $this->config->get('payment_paypal_standard_processed_status_id');
                        break;
                    case 'Refunded':
                        $order_status_id = $this->config->get('payment_paypal_standard_refunded_status_id');
                        break;
                    case 'Reversed':
                        $order_status_id = $this->config->get('payment_paypal_standard_reversed_status_id');
                        break;
                    case 'Voided':
                        $order_status_id = $this->config->get('payment_paypal_standard_voided_status_id');
                        break;
                }

                $this->model_checkout_order->addOrderHistory($order_id, $order_status_id);
            } else {
                $this->model_checkout_order->addOrderHistory($order_id, $this->config->get('config_order_status_id'));
            }

            curl_close($curl);
        }
    }
}

As our file is paypal_standard.php so Class name is ControllerExtensionPaymentPaypalStandard which extends the Controller base class

class ControllerExtensionPaymentPaypalStandard extends Controller{ 

Create index method. Index method is called automatically if no parameters are passed, check this video tutorial for details https://www.youtube.com/watch?v=X6bsMmReT-4. In payment extension you don’t need to pass any parameter in index() method.

public function index() { 

Loads the language file by which the varaibles of language file are accessible in twig files

$this->load->language('extension/payment/paypal_standard'); 

Text to show when it is in test mode.

$data['text_testmode'] = $this->language->get('text_testmode'); 

Text to show for the button.

$data['button_confirm'] = $this->language->get('button_confirm'); 

Get the configured value, and find when it is on test mode or not.

$data['testmode'] = $this->config->get('payment_paypal_standard_test'); 

If it is on test mode then set the form action URL to sandbox paypal URL else set the form action URL to live paypal URL

if (!$this->config->get('payment_paypal_standard_test')) { 
$data['action'] = 'https://www.paypal.com/cgi-bin/webscr'; 
} else { 
$data['action'] = 'https://www.sandbox.paypal.com/cgi-bin/webscr'; 
}

Notify URL is important to get the callback once the payment gateway processes the payment.

$data['notify_url'] = $this->url->link('extension/payment/paypal_standard/callback', '', true);

This is the custom data that we are passing to the PayPal and it will return us back to the website server, in our case we are sending the Order Id.

$data['custom'] = $this->session->data['order_id'];

Now, let’s check the callback() method. Some Instant Update variables set up the Cart Upload to use your callback server. Include the following required variables in the Cart Upload command to have PayPal send Instant Update requests to your callback server. It means the Paypal will call this URL and update the order status in the database of the website.

public function callback() {

PayPal sends back the order id so that we know it it is the right order that we are processing back.

if (isset($this->request->post['custom'])) {
$order_id = $this->request->post['custom'];
} else {
$order_id = 0;
}

Once the Paypal sends us back data, we need to verify again with the returned data to finalized if the data are not tampered. The image shows the flow:

Paypal IPN
https://developer.paypal.com/docs/api-basics/notifications/ipn/IPNIntro/#ipn-overview

In PHP we use CURL to call the Paypal server to verify it.

$request = 'cmd=_notify-validate';

foreach ($this->request->post as $key => $value) {
$request .= '&' . $key . '=' . urlencode(html_entity_decode($value, ENT_QUOTES, 'UTF-8'));
}
if (!$this->config->get('payment_paypal_standard_test')) {
$curl = curl_init('https://www.paypal.com/cgi-bin/webscr');
} else {
$curl = curl_init('https://www.sandbox.paypal.com/cgi-bin/webscr');
}
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($curl);
if (!$response) {
$this->log->write('paypal_standard :: CURL failed ' . curl_error($curl) . '(' . curl_errno($curl) . ')');
}

if ($this->config->get('payment_paypal_standard_debug')) {
$this->log->write('paypal_standard :: IPN REQUEST: ' . $request);
$this->log->write('paypal_standard :: IPN RESPONSE: ' . $response);
}

Once we call the CURL, it returns back the responses whether it is verified or not. Then, we can check and update our order status, for checking the code is like below:

if ((strcmp($response, 'VERIFIED') == 0 || strcmp($response, 'UNVERIFIED') == 0) && isset($this->request->post['payment_status'])) {

In the switch statement, check Canceled_Reversal case, the code is

case 'Canceled_Reversal':
  $order_status_id = $this->config->get('payment_paypal_standard_canceled_reversal_status_id');
  break;

It sets the $order_status_id to Canceled Reversal value or as per the setting configuration we do in the Paypal standard extension. Other order statuses are similar except Completed. See the Completed switch case code:

case 'Completed':
$receiver_match = (strtolower($this->request->post['receiver_email']) == strtolower($this->config->get('payment_paypal_standard_email')));

$total_paid_match = ((float) $this->request->post['mc_gross'] == $this->currency->format($order_info['total'], $order_info['currency_code'], $order_info['currency_value'], false));

if ($receiver_match && $total_paid_match) {
$order_status_id = $this->config->get('payment_paypal_standard_completed_status_id');
}

if (!$receiver_match) {
$this->log->write('paypal_standard :: RECEIVER EMAIL MISMATCH! ' . strtolower($this->request->post['receiver_email']));
}

if (!$total_paid_match) {
$this->log->write('paypal_standard :: TOTAL PAID MISMATCH! ' . $this->request->post['mc_gross']);
}
break;

For the completed status, we check whether the received amount and paid amount are matched or not. If it matched then only order status is set to completed. If the amount is not matched then a log is written on the log file.

Finally the order status is added on the history with the following lines of code:

$this->model_checkout_order->addOrderHistory($order_id, $order_status_id);

In this way, we write the code at the catalog controller of Paypal Standard, similarly way we can write for other payment extensions.

Now, catalog model file of Payment extension

We need to create the model file, although we don’t call in the controller file. It is automatically called for payment methods. For that go to catalog/model/extension/payment and create paypal_standard.php and paste following code.

<?php
class ModelExtensionPaymentPaypalStandard extends Model {
	public function getMethod($address, $total) {
		$this->load->language('extension/payment/paypal_standard');

		$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "zone_to_geo_zone WHERE geo_zone_id = '" . (int)$this->config->get('payment_paypal_standard_geo_zone_id') . "' AND country_id = '" . (int)$address['country_id'] . "' AND (zone_id = '" . (int)$address['zone_id'] . "' OR zone_id = '0')");

		if ($this->config->get('payment_paypal_standard_total') > $total) {
			$status = false;
		} elseif (!$this->config->get('payment_paypal_standard_geo_zone_id')) {
			$status = true;
		} elseif ($query->num_rows) {
			$status = true;
		} else {
			$status = false;
		}

		$currencies = array(
			'AUD',
			'CAD',
			'EUR',
			'GBP',
			'JPY',
			'USD',
			'NZD',
			'CHF',
			'HKD',
			'SGD',
			'SEK',
			'DKK',
			'PLN',
			'NOK',
			'HUF',
			'CZK',
			'ILS',
			'MXN',
			'MYR',
			'BRL',
			'PHP',
			'TWD',
			'THB',
			'TRY',
			'RUB'
		);

		if (!in_array(strtoupper($this->session->data['currency']), $currencies)) {
			$status = false;
		}

		$method_data = array();

		if ($status) {
			$method_data = array(
				'code'       => 'paypal_standard',
				'title'      => $this->language->get('text_title'),
				'terms'      => '',
				'sort_order' => $this->config->get('payment_paypal_standard_sort_order')
			);
		}

		return $method_data;
	}
}

The payment class is created and we need the getMethod() method whose parameters are address and total. In getMethod() method we return the value when the currencies is supported, when geo zones are supported.

$method_data = array(
'code' => 'paypal_standard',
'title' => $this->language->get('text_title'),
'terms' => '',
'sort_order' => $this->config->get('payment_paypal_standard_sort_order')
);

Once this is returned then only it is shown in the payment methods.

Now, catalog template file of Payment extension:

Finally we create the frontend template file, go to catalog/view/theme/default/extension/payment/ and create paypal_standard.twig.

the form should match the provided form from payment gateway.

For paypal, the form is provided here: https://developer.paypal.com/docs/paypal-payments-standard/integration-guide/formbasics/#sample-html-code-for-auto-fill-forms

Paste the following code in paypal_standard.twig

{% if testmode %}
<div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> {{ text_testmode }}</div>
{% endif %}
<form action="{{ action }}" method="post">
  <input type="hidden" name="cmd" value="_cart" />
  <input type="hidden" name="upload" value="1" />
  <input type="hidden" name="business" value="{{ business }}" />
  {% set i = 1 %}
  {% for product in products %}
  <input type="hidden" name="item_name_{{ i }}" value="{{ product.name }}" />
  <input type="hidden" name="item_number_{{ i }}" value="{{ product.model }}" />
  <input type="hidden" name="amount_{{ i }}" value="{{ product.price }}" />
  <input type="hidden" name="quantity_{{ i }}" value="{{ product.quantity }}" />
  <input type="hidden" name="weight_{{ i }}" value="{{ product.weight }}" />
  {% set j = 0 %}
  {% for option in product.option %}
  <input type="hidden" name="on{{ j }}_{{ i }}" value="{{ option.name }}" />
  <input type="hidden" name="os{{ j }}_{{ i }}" value="{{ option.value }}" />
  {% set j = j + 1 %}
  {% endfor %}
  {% set i = i + 1 %}
  {% endfor %}
  {% if discount_amount_cart %}
  <input type="hidden" name="discount_amount_cart" value="{{ discount_amount_cart }}" />
  {% endif %}
  <input type="hidden" name="currency_code" value="{{ currency_code }}" />
  <input type="hidden" name="first_name" value="{{ first_name }}" />
  <input type="hidden" name="last_name" value="{{ last_name }}" />
  <input type="hidden" name="address1" value="{{ address1 }}" />
  <input type="hidden" name="address2" value="{{ address2 }}" />
  <input type="hidden" name="city" value="{{ city }}" />
  <input type="hidden" name="zip" value="{{ zip }}" />
  <input type="hidden" name="country" value="{{ country }}" />
  <input type="hidden" name="address_override" value="0" />
  <input type="hidden" name="email" value="{{ email }}" />
  <input type="hidden" name="invoice" value="{{ invoice }}" />
  <input type="hidden" name="lc" value="{{ lc }}" />
  <input type="hidden" name="rm" value="2" />
  <input type="hidden" name="no_note" value="1" />
  <input type="hidden" name="no_shipping" value="1" />
  <input type="hidden" name="charset" value="utf-8" />
  <input type="hidden" name="return" value="{{ return }}" />
  <input type="hidden" name="notify_url" value="{{ notify_url }}" />
  <input type="hidden" name="cancel_return" value="{{ cancel_return }}" />
  <input type="hidden" name="paymentaction" value="{{ paymentaction }}" />
  <input type="hidden" name="custom" value="{{ custom }}" />
  <input type="hidden" name="bn" value="OpenCart_2.0_WPS" />
  <div class="buttons">
    <div class="pull-right">
      <input type="submit" value="{{ button_confirm }}" class="btn btn-primary" />
    </div>
  </div>
</form>

No header, no footer, no left or right column are needed on the template, with this, only the submit button is shown. Check the following code, this is to show the text saying it is only for test.

{% if testmode %}
<div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> {{ text_testmode }}</div>
{% endif %}

In this way, we write the code at the catalog template of PayPal Standard, similarly way we can write for other payment extensions. Please let us know in the comment if we need to define any of the code above.

Now once the Paypal Standard is activated then it will show like below:

Paypal Standard Opencart

In this way, you can create the form in the admin section, validate the form data, validate the permission, and save the data to the database in the setting database table for the Payment extension and show it in the front. Hope you liked this article, please subscribe to our YouTube Channel for Opencart video tutorials. You can also find us on Twitter and Facebook.

Previous articleHow to create a payment module or extension in the Opencart 3? Developer guide Part I of III
Next articleOpencart 3 development tutorial, multi-instance slider testimonials module

4 COMMENTS

  1. Thanks for the extremely helpful tutorial. There’s one thing I had to fix though:
    As of OpenCart version 3.0.3.6 I had to change the following template file path
    catalog/view/theme/default/extension/payment/
    to
    catalog/view/theme/extension/payment/

  2. I have created the payment extension as per this article, but it does display in checkout page.
    How can I fix this?

  3. I have created the payment extension as per this article, but it does display in checkout page.
    How can I fix this?

LEAVE A REPLY

Please enter your comment!
Please enter your name here