How to Create a Payment Module

Here's a brief explaination of what all the methods do in a payment module.

First let's see a simple module example:

/includes/modules/payment/pm_mymodule.inc.php:


class pm_mymodule {

  public $id = __CLASS__;
  public $name = 'My Module';
  public $description = 'Lorem ipsum dolor';
  public $author = 'ACME Corp.';
  public $version = '1.0';

  public $website = 'https://www.litecart.net';
  public $priority = 1;

  // options() returns the available payment options during checkout.
  // It can output several payment options if necessary, e.g. card, directbank, etc.
  public function options($items, $subtotal, $tax, $currency_code, $customer) {

    if (!$this->settings['status']) {
      return; // Don't return any options
    }

    return [
      'title' => 'My Payment module',
      'options' => [
        [
          'id' => 'card',
          'icon' => 'images/payment/card.png',
          'name' => 'Card Payment',
          'description' => 'Select this option for card payment.',
          'fields' => '',
          'cost' => 0,
          'tax_class_id' => 0,
          'confirm' => 'Pay Now',
        ],
        [
          'id' => 'directbank',
          'icon' => 'images/payment/bank.png',
          'name' => 'Direct Bank Payment',
          'description' => 'Select this option for direct bank payment.',
          'fields' => '',
          'cost' => 0,
          'tax_class_id' => 0,
          'confirm' => 'Pay Now',
          'error' => 'This option has an error and cannot be selected.'
        ],

    // If you need to collect some user data, here is an example.
    // Note:  Collected data can later be accessed via $this->userdata['param'].

        [

          'id' => 'method',
          'icon' => 'images/payment/icon.png',
          'name' => 'Title',
          'description' => 'This is a payment method.',
          'fields' => implode(PHP_EOL, [
            '<input type="text" name="foo" value="'. (isset($this->userdata['foo']) ? htmlspecialchars($this->userdata['foo']) : '') .'" />',
            '<input type="text" name="bar" value="'. (isset($this->userdata['bar']) ? htmlspecialchars($this->userdata['bar']) : '') .'" />',
          ]),
          'cost' => 0,
          'tax_class_id' => 0,
          'confirm' => 'Button Text',
        ],
      ]
    ];
  }

  public function pre_check($order) {
    return 'There was an error';
  }

  // The transfer() method is used to send the user to a payment gateway. The return is an array of the destination and transaction data. If not declared, the transfer operation will be skipped and advance immediately to /order_process.
  public function transfer($order) {

      var_dump($order->data); exit;

    // Contact the payment service provider
      $client = new wrap_http();

      $result = $client->call('GET', 'https://paymentservice.tld/api/transaction/TX123456789');

    // Save some important details used for later

      session::$data['vendorname']['sessionid'] = 'TX123456789';

      return [
        'action' => 'GET',
        'method' => 'https://domain.tld/payment/window/TX123456789',
        'fields' => '',
      ];
  }

  // The verify() method is called during /order_process to verify the transaction.
  public function verify($order) {

  // Fetch the latest updates for transaction from provider
    $client = new wrap_http();

    $result = $client->call('GET', 'https://paymentservice.tld/api/transaction/'. session::$data['vendorname']['sessionid']);

  // Verify some data
    ...

    if ($error) {
      return ['error' => 'There was an error verifying your transaction'];
    }

    return [
      'order_status_id' => $this->settings['order_status_id'],
      'transaction_id' => '123456789',
      'payment_terms' => 'PWO',
      'comments' => 'This is an order comment',
    ];
  }

  // This method is available for after order operations if necessary i.e. updating order reference with the order number. It does not have a return.
  public function after_process($order) {
  }

  // This method returns html code that is output on the order success page. It was intended to display a payment receipt but your imagination sets the limit.
  public function receipt($order) {
    return 'Receipt';
  }

  // This method sets up the payment module with a settings structure. The return is an array of the structure.
  // Note: status and priority are mandatory
  function settings() {
    return [
      [
        'key' => 'status',

        'default_value' => '0',
        'title' => 'Status',
        'description' => 'Enables or disables the module.',
        'function' => 'toggle("e/d")',
      ],
      [
        'key' => 'icon',

        'default_value' => 'images/payment/paymentlogo.png',
        'title' => 'Icon',
        'description' => 'Web path of the icon to be displayed.',
        'function' => 'text()',
      ],
      [
        'key' => 'order_status_id',

        'default_value' => '0',
        'title' => 'Order Status:',
        'description' => 'Give successful orders made with this payment module the following order status.',
        'function' => 'order_status()',
      ],
      [
        'key' => 'priority',

        'default_value' => '0',
        'title' => 'Priority',
        'description' => 'Process this module in the given priority order.',
        'function' => 'number()',
      ],
    ];
  }

  // This method does not have a return. It is executed upon installing the module in the admin panel. It can be used for creating third party mysql tables etc. Note: install() doesn't run until the “Save” button is clicked.
  public function install() {
  }

  // This method does not have a return. It is executed upon uninstalling the module in the admin panel. It can be used for deleting orphan data.
  public function uninstall() {
  }
}

Different ways of using transfer()

The transfer() method is used to send the user to a payment gateway. The return is an array of the destination and transaction data.

If not declared the transfer operation will be skipped.

The total amount of the order that include all the fees (shipping fees, payment fees, VAT, etc.) can be accessed via : $order->data['payment_due']

A. Using HTTP POST

  public function transfer($order) {

    ...

    return [
      'method' => 'post',
      'action' => 'https://www.paymentgateway.com/form_process.ext',

      'fields' => [   // Pass HTML string or associative array that is recognized as form fields
        'foo' => 'bar',
        'this' => 'that',
      ],
    ];
  }

B. Using HTTP GET

  public function transfer($order) {

    ...

    return [
      'method' => 'get',
      'action' => 'https://www.paymentgateway.com/token/0123456789abcdef',
    ];
  }

C. Using HTML

  public function transfer($order) {

    ...

  // Initiate a view
    $myview = new ent_view();

  // Pass some useful parameters to view

    $myview->snippets = [
      'userdata' => $this->userdata ?? [],
    ];

  // Render view
    $html = $myview->stitch('views/myview');

    return [
      'method' => 'html',
      'content' => $html,
    ];
  }

~/includes/templates/mytemplate.catalog/views/myview.inc.php:


Additional Payment Details

D. Using Classic API Requests


  public function transfer($order) {

    try {

    // Initiate HTTP client
      $client = new wrap_http();

    // Set request headers

      $headers = [
        'Content-Type' => 'application/json; charset='. language::$selected['charset'],
      ];

    // Set request body

      $request = [
        'this' => 'that',
        // ...
      ];

    // Validate response (A request log and response is stored in the logs/ folder.)

      if (!$response = $client->call('POST', 'https://www.vendor.com/api/...', json_encode($request), $headers)) {
        throw new Exception('No response');
      }

    // Decode response
      if (!$result = json_decode($response, true)) {
        throw new Exception('Invalid response');
      }

    // Halt on error
      if (empty($result['error'])) {
        throw new Exception($result['error']);
      }

    // Other additional error checking?
      //...

    // Redirect to payment gateway
      header('Location: '. $result['redirect_url']);
      exit;

    } catch (Exception $e) {
      return ['error' => $e->getMessage()];
    }
  }

Order Object

The $order object is passed to the method as the first passed variable.

You may see what's inside by displaying it's content:

  var_dump($order->data);
  exit;

This is how we make use of the order object to build order lines. This is just an example as the structure is payment service specific:

  $item_no = 0;
  foreach ($order->data['items'] as $item) {

    $request['cart_contents'][] = [
      'name' => $item['name'],
      'sku' => $item['sku'],
      'quantity' => $item['product_id'],
      'amount' => currency::format_raw($item['price'], $order->data['currency_code'], $order->data['currency_value']),
      'tax' => currency::format_raw($item['tax'], $order->data['currency_code'], $order->data['currency_value']),
    ];
  }

  foreach ($order->data['order_total'] as $row) {
    if (empty($row['calculate'])) continue;

    $request['cart_contents'][] = [
      'name' => $row['title'],
      'sku' => $row['module_id'],
      'quantity' => 1,
      'amount' => currency::format_raw($row['value'], $order->data['currency_code'], $order->data['currency_value']),
      'tax' => currency::format_raw($row['tax'], $order->data['currency_code'], $order->data['currency_value']),
    ];
  }

Different behaviors depending on different choice of option:

    list($module_id, $option_id) = explode(':', $order->data['payment_option']['id']);

    switch($option_id) {

      case 'option1':
        // Do this ...
        break;

      case 'option2':
        // Do that ...
        break;
    }

Return URLs

Example of return URLs:

  public function transfer($order) {

    try {

      //$order->save(); // Save session order to database before transaction creates an $order->data['id']. Not recommended

      $fields = [
        ...
        'cancel_url' => document::ilink('checkout'),
        'success_url' => document::ilink('order_process'),
        'callback_url' => document::link(WS_DIR_APP . 'ext/payment_service_provider/my_external_callback_file.php', ['order_id' => $order->data['id']]), // Make sure the order was saved during transfer()
      ];

      ...

      if ($error) {
        throw new Exception('There was an error verifying your transaction');
      }

      return [
        'action' => 'https://www.paymentgateway.com/form_process.ext',
        'method' => 'post',
        'fields' => $fields,
      ];

    } catch (Exception $e) {
        return ['error' => $e->getMessage()];
    }
  }

verify()

The verify() method is used to verify the transaction. There are a few security questions you may ask yourself:

  • Does the transaction result come from a trusted source?
  • Is this a valid order ID or UID
  • Is the payment flagged as okay by the payment service provider?
  • Is the payment amount the same as the order amount? Be aware of rounding.
  • Is the payment currency same as the order currency?
  public function verify($order) {

  // Verify some data
    ...

    if ($error) {
      return ['error' => 'There was an error verifying your transaction'];
    }

    return [
      'order_status_id' => $this->settings['order_status_id'],
      'transaction_id' => '123456789',
      'payment_terms' => 'PWO',
      'comments' => 'This is an order comment',
    ];
  }

Get a Settings Value

  $this->settings['key_name']

Translations

It is not user friendly to hardcode text in a single language. LiteCart recognizes the following syntax for translating any translations for a module.

  language::translate(__CLASS__.':title_hello_world', 'Hello World')

Callbacks

Some payment service providers offers machine-to-machine data exchange during the transaction takes part. In such cases you will need a callback function. Here is an example of an external script that will call a method inside the module called callback().

/ext/provider/callback.php:


<?php
  require_once('../../includes/app_header.inc.php');

  try {

  // Make sure callback comes from a trusted IP address
    if (!in_array($_SERVER['REMOTE_ADDR'], ['123.456.789.0', '123.456.789.1', '123.456.789.2'])) {
      error_log('Unauthorized access by '. $_SERVER['REMOTE_ADDR'] .' to file '. __FILE__);
      throw new Exception(Access Denied, 403);
    }

  // Make sure we have an order ID
    if (empty($_GET['order_id'])) {
      throw new Exception('Bad Request', 400);
    }

  // Find the order in the database (The order must previously have been saved)
    $orders_query = database::query(
      "select id from ". DB_TABLE_PREFIX ."orders
      where id = ". (int)$_GET['order_id'] ."
      limit 1;"
    );

    if (!$order = database::fetch($orders_query)) {
      throw new Exception('Not Found', 404);
    }

  // Initiate $order as the order object
    $order = new ent_order($order['id']);

  // Get the order's payment option
    list($module_id, $option_id) = explode(':', $order->data['payment_option']['id']);

  // Pass the call to the payment module's method callback()
    $payment = new mod_payment();
    $result = $payment->run('callback', $module_id, $order);

    // The rest is handled inside the payment module
    // Define the funtion by: public function callback($order) {}

  } catch (Exception $e) {
    http_response_code($e->getCode());
    echo $e->getMessage();
    exit;
  }

We recommend storing the transaction details of the callback in the database. You can set up a custom table for this. Then verify them later using the method verify(). Otherwise you might have to verify the transaction for both the callback and the returning user evolving duplicate code.

Revisions

Top Editors
Recently Edited Articles