Here's a brief explaination of what all the methods do in a payment module.
First let's see a simple module example:
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 = 'http://www.litecart.net'; public $priority = 1; public function options($items, $subtotal, $tax, $currency_code, $customer) { return [ 'title' => 'My Payment module', 'options' => [ [ 'id' => 'method1', 'icon' => 'images/payment/mymodule-method1.png', 'name' => 'Method 1', 'description' => 'Select this option for method 1.', 'fields' => '', 'cost' => 0, 'tax_class_id' => 0, 'confirm' => 'Confirm Order', ], ] ]; } public function pre_check($order) { } public function transfer($order) { } public function verify($order) { } public function after_process($order) { } public function receipt($order) { } 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' => 'input()', ], [ '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' => 'int()', ], ]; } public function install() { } public function uninstall() { } }
Now let's have a look at the process chain:
Called in checkout - used to display all avaliable payment options. A payment module may output several payment options if necessary, e.g. card, directbank, etc.
public function options($items, $subtotal, $tax, $currency_code, $customer) { 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 bankpayment.', 'fields' => '', 'cost' => 0, 'tax_class_id' => 0, 'confirm' => 'Pay Now', ], ] ]; }
If you need to collect some user data, here is an example. Inside the module you can access any submitted form fields through the array $this→userdata
.
[ 'id' => 'method', 'icon' => 'images/payment/icon.png', 'name' => 'Title', 'description' => 'This is a payment method.', 'fields' => '<input type="text" name="foo" value="'. (isset($this->userdata['foo']) ? htmlspecialchars($this->userdata['foo']) : '') .'" />' . PHP_EOL . '<input type="text" name="bar" value="'. (isset($this->userdata['bar']) ? htmlspecialchars($this->userdata['bar']) : '') .'" />', 'cost' => 0, 'tax_class_id' => 0, 'confirm' => 'Button Text', ],
If nothing is returned, no option will be returned.
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']
public function transfer($order) { ... return [ 'method' => 'post', 'action' => 'http://www.paymentgateway.com/form_process.ext', 'fields' => [ // Pass HTML string or associative array that is recognized as form fields 'foo' => 'bar', 'this' => 'that', ], ]; }
public function transfer($order) { ... return [ 'method' => 'get', 'action' => 'http://www.paymentgateway.com/token/0123456789abcdef', ]; }
public function transfer($order) { ... $myview = new ent_view(); $myview->snippets = [ userdata => @$this->userdata, ]; $html = $myview->stitch('views/myview'); return [ 'method' => 'html', 'content' => $html, ]; }
<h1>Additional Payment Details</h1> <?php echo functions::form_draw_form_begin('myform', 'post'); ?> <?php echo functions::form_draw_text_field('foo', @$userdata['foo']); <?php echo functions::form_draw_form_end(); ?>
public function transfer($order) { ... // Pass data to some machine $client = new wrap_http(); $response = $client->call('POST', 'https://www.vendor.com/api/...', $mydata); // Vaidate response if (empty($result)) { return ['error' => 'No response']; } if (!$result = json_decode($response, true)) { return ['error' => 'Invalid response']; } if (empty($result['error'])) { return ['error' => $result['error']]; } ... // Since we don't need to send the user anywhere we simply don't return anything }
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) { $fields['item_name_'.$item_no] = $item['name']; $fields['item_number_'.$item_no] = $item['product_id'] . (!empty($item['option_id']) ? ':'.$item['product_id'] : ''); $fields['quantity_'.$item_no] = $item['quantity']; $fields['amount_'.$item_no] = currency::format_raw($item['price'], $order->data['currency_code'], $order->data['currency_value']); $fields['tax_'.$item_no] = currency::format_raw($item['tax'], $order->data['currency_code'], $order->data['currency_value']); $item_no++; } $item_no = 0; foreach ($order->data['order_total'] as $row) { if (empty($row['calculate'])) continue; $fields['item_name_'.$item_no] = $row['title']; $fields['item_number_'.$item_no] = $row['module_id']; $fields['quantity_'.$item_no] = '1'; $fields['amount_'.$item_no] = currency::format_raw($row['value'], $order->data['currency_code'], $order->data['currency_value']); $fields['tax_'.$item_no] = currency::format_raw($row['tax'], $order->data['currency_code'], $order->data['currency_value']); $item_no++; }
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; }
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_uid' => $order->data['uid']]), // An order always has a uid, even though it is not saved. ]; ... if ($error) { throw new Exception('There was an error verifying your transaction'); } return [ 'action' => 'http://www.paymentgateway.com/form_process.ext', 'method' => 'post', 'fields' => $fields, ]; } catch (Exception $e) { return ['error' => $e->getMessage()]; } }
The verify() method is used to verify the transaction. There are a few security questions you may ask yourself:
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', ]; }
This method does not have a return. It is available for after order operations if necessary i.e. updating order reference with the order number.
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.
This method sets up the payment module with a settings structure. The return is an array of the structure.
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()', ], ]; }
Keep in mind status and priority are mandatory for all modules.
$this->settings['key_name']
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.
This method does not have a return. It is executed upon saving the module settings in the admin panel. This method is triggered when the module is already installed. It can be used for updating third party mysql tables etc. Note: update() doesn't run until the “Save” button is clicked.
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.
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')
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().
<?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.