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, $success_url, $cancel_url) {
try {
// What's inside the order data?
//var_dump($order->data); exit;
// Create a request for contacting a remote machine
$request = [
...
];
// Contact the payment service provider
$client = new wrap_http();
$response = $client->call('POST', 'https://paymentservice.tld/api/transaction/', $request);
// Make sure $result is fine
if (!$result = json_decode($response, true)) {
throw new Exception('Bad stuff');
}
// Save some important details used for later
session::$data['vendorname']['sessionid'] = $result['aReturnedTransactionId'];
return [
'action' => 'GET',
'method' => $result[''],
'fields' => '',
];
} catch (Exception $e) {
return ['error' => $e->getMessage()];
}
}
// 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, $success_url, $cancel_url) {
...
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, $success_url, $cancel_url) {
...
return [
'method' => 'get',
'action' => 'https://www.paymentgateway.com/token/0123456789abcdef',
];
}
C. Using HTML
public function transfer($order, $success_url, $cancel_url) {
...
// 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, $success_url, $cancel_url) {
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.)
$response = $client->call('POST', 'https://www.vendor.com/api/...', json_encode($request), $headers);
if (!$response) {
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, $success_url, $cancel_url) {
try {
//$order->save(); // Save session order to database before transaction creates an $order->data['id']. Not recommended
$fields = [
...
'cancel_url' => $cancel_url,
'success_url' => $success_url,
'callback_url' => document::link(WS_DIR_APP . 'ext/payment_service_provider/my_external_callback_file.php', ['order_id' => $order->data['id']]),
];
...
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'])) {
trigger_error('Unauthorized access by '. $_SERVER['REMOTE_ADDR'], E_USER_WARNING);
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;
}
If you would like to store the callbacks you can set up a custom table for this. That can be handy if don't want to verify both the transaction and the callback separately but instead save the callback for the verify() process upon customer return.