Custom payment adapters
PayBridge is designed to interface with a variety of third-party payment service providers (PSP). Each PSP is unique, using different protocols for requests and returning different parameters after payment. PayBridge handles this by use of adapters. Adapters are a PHP class with responsibilities for building up a valid request and interpreting the response.
When developing an adapter you will need access to the documentation and integration guides for the payment provider you are integrating with. This will typically be available on request from the PSP.
Creating a custom payment adapter
The custom adapter class file should reside in /jadu/custom/PSP/Adapter/
and should extend Jadu_PayBridge_PSP_Adapter
. The Jadu_PayBridge_PSP_Adapter
class requires the custom adapter class to implement a number of abstract methods, which are intended to make development easier.
Some of these methods may be superfluous depending on the payment provider, and can therefore be implemented to just return an empty string or empty array, depending on the method.
Step One: Adding the custom adapter to the database
The first stage of development is to register your adapter with PayBridge. This is done by adding a row to the JaduPayBridgePSPs
table. PayBridge can support multiple adapters with each adapter having its own row in JaduPayBridgePSPs
.
The table below describes the fields the row will need to provide, and their purpose.
| Field | Data type | Purpose |
|-:-|-:-|-:-|
| id | integer | This is an auto number, and will be populated by the database when the row is inserted. |
| name | string | The name of the payment provider, as it should appear in the Control Center. Spaces can be used. |
| classFile | string | The path to the class file of the custom adapter. This should be of the form PayBridge/PSP/Adapter/MyAdapter.php
, where MyAdapter
is the name of the your adapter class file. |
| basketEnabled | integer | This determines whether multiple items are allowed in an order. Can be 1 (on) or 0 (off). |
| payeeDetails | integer | This determines whether details of the person making the order should be collected. Can be 1 (on) or 0 (off). Jadu recommend setting this to 0. |
| motoFull | integer | This determines if full MOTO payments are supported by the PSP. This can be enabled (1) or disabled (0). |
| motoRedirect | integer | If the PSP does not provide MOTO payments the complete page can contain a link into the PSP system back-end, where the payment should be recorded manually. This can be enabled (1) or disabled (0). |
| configPrefix | string | This can be set to to any value, but is usually the payment provider name with no spaces. This is used to retrieve any provider-specific settings added to the JaduPayBridgeConfiguration
table. For instance, if an instance of PayBridge contains two payment providers, each with a setting 'timeout', these would be 'provider1_timeout' and 'provider2_timeout' in JaduPayBridgeConfiguration
. |
| currency | string | This should represent the currency of the payment provider, in the form of an ISO 4217 Currency Code e.g. GBP or USD. |
| mappingSchemaFile | string | If required, this can be the path to a schema file (.xsd) form the root of the Jadu install. When provided, this schema is used to allow questions and variables of a form to be mapped into the payment request. If not required, this field should be set to NULL. |
Step Two: Setting endpoints
The first two abstract method signatures of the Jadu_PayBridge_PSP_Adapter
class are:
/**
* Abstract method that when implemented will return the test payment portal endpoint.
*
* @return string Payment portal test endpoint
*/
abstract protected function getTestEndpoint();
/**
* Abstract method that when implemented will return the production payment portal endpoint.
*
* @return string Payment portal production endpoint
*/
abstract protected function getProductionEndpoint();
These methods should return a string containing the location of the endpoints. If the payment provider uses HTTP POST or GET, this would be a URL. Alternatively, it may be a web service or SOAP endpoint.
The getTestEndpoint()
method should return the endpoint for the test payment system, which will be used when PayBridge is in test mode.
When PayBridge is not in test mode the request will instead be sent to the value returned from getProductionEndpoint()
.
Step Three: Building the request
PayBridge supports two distinct approaches to integration with payment providers, Client side redirect
and Server side request
. All adapters should contain an attribute integrationMethod
, as shown below:
public $integrationMethod = self::CLIENT_SIDE_REDIRECT // can be self::CLIENT_SIDE_REDIRECT or self::SERVER_SIDE_REQUEST
This determines how PayBridge attempts to make requests to the payment provider, and so it is important that is attribute is set correctly. If self::CLIENT_SIDE_REDIRECT
is used, PayBridge will render a HTML form containing hidden inputs with the relevant data. This form will contain a button for the user to click in order to proceed to the payment provider. This is suitable for payment providers who expect a request via HTTP/HTTPS.
Alternatively, if self::SERVER_SIDE_REQUEST
is used, PayBridge will output the same button asking the user to make a payment, but this will postback to PayBridge, which will then automatically make the request to the payment provider. This is suitable for web service based providers, e.g. via SOAP.
The next sections cover each integration method in more detail.
Client Side Redirect Integration
PayBridge's adapter interface provides a helpful framework for adapters which use client side redirection integration. The request is split into logical parts, each represented by one method, rather than providing one single method for the entire request. The diagram below shows how these smaller methods are put together to build the entire request.
Figure 1: Method calls to build the request
This diagram shows abstract methods that should be implemented on the custom adapter, but also shows how these methods are called from the front-end scripts, payment_provider.php
and payments_basket.php
. Whilst these scripts are specific to each implementation, and could therefore be customisable to interface with a payment provider in an entirely different way, this is not recommended. The best approach for adapter development is to make use of the abstract methods wherever possible, as this should ensure compatibility with future updates to PayBridge.
Each of the methods called by getAllParams()
should return an array, even if this array is empty. getAllParams()
is then responsible for merging these arrays into one single array. getFormFields
takes this single array, and translates each key/value pair in it to a hidden input of the form:
<input type="hidden" name="{{ key }}" value="{{ value }}" />
These hidden inputs are then returned by renderPortalRequest()
, and wrapped in a HTML form tag. This form has the appropriate payment provider endpoint as its action, and includes a submit button which takes the user to the payment provider portal.
Server Side Request Integration
The server side request integration is more complex than client side redirection because it is required to deviate from the default behaviour of outputting an HTML form for the request. As illustrated by the below diagram, the best practice for developing an adapter using a server side request is to override getFormFields()
in Jadu_PayBridge_PSP_Adapter
with a method of identical signature in the custom adapter. This gives the flexibility to change how the request is made, without modifying core PayBridge code or needlessly customising the front-end scripts.
Figure 2: Manipulating method calls to allow flexibility in development
The standard implementation of getFormFields()
in Jadu_PayBridge_PSP_Adapter
is below.
public function getFormFields()
{
$output = '';
foreach ($this->getAllParams() as $name => $value) {
$output .= $this->getHiddenFieldMarkup($name, $value) . PHP_EOL;
}
$output .= $this->getSecurityDigest();
return $output;
}
Your adapter may change the implementation in a number of ways. For example, a payment provider may expect a SOAP request, and respond with a URL to redirect the user to. In such a scenario, one approach could be:
public function getFormFields()
{
$envelope = $this->openSoapEnvelope();
foreach ($this->getAllParams() as $name => $value) {
$envelope .= $this->getXMLForParameter($name, $value) . PHP_EOL;
}
$envelope .= $this->closeSoapEnvelope();
$returnedData = $this->sendRequest($envelope);
header('Location: ' . $returnedData['paymentURL']);
exit;
}
In the above code snippet, the methods openSoapEnvelope()
, closeSoapEnvelope()
, getXMLForParameter()
and sendRequest()
would all need implementing in the custom adapter. This snippet still calls getAllParams()
on Jadu_PayBridge_PSP_Adapter
, which will use the abstract method to build up the request in smaller parts. Of course, the overridden version of getFormFields()
could build up the request in any way, and does not necessarily need to call getAllParams()
.
When using the server side request integration, PayBridge's call to renderPortalRequest()
is wrapped in a try-catch block. If the request fails, the adapter can throw an exception, which will be caught by PayBridge. This would result in the user being redirected to the payment maintenance page, where the 'Form configuration error' message set in PayBridge's settings page would be displayed.
Step Four: Populating the request
Once the adapter has been added to the database, the integration method decided and endpoints set, the next step is to build up the request to the payment provider. If using client side redirect or calling getAllParams()
in an overridden getFormFields()
method, this should be done by implementing the abstract methods described below.
Each of these functions should return an associative array, where the key is the parameter name and the value is as it should be in the request. Each method can return an empty array if no applicable parameters for the payment provider exist. Note that the comments below are intended as a guide - parameters can be set in any of the abstract methods.
Return URLs
Most payment systems require at least one URL to be passed in the request. This is usually the URL that the user is returned to after payment. Some providers accept multiple URLs - for example a URL to return the user to if they press 'Back' or 'Cancel' at the payment provider.
The abstract method getRequestURLs() should be implemented by the custom adapter, and is the best place to put any such URLs. The signature for this can be seen below.
/**
* Returns an array of payment portal request URLs for success, cancelled (as required by the PSP) actions
* IMPORTANT: Should at least call $this->goToCompletionPage() for main return URL
*
* @return array[string]string Array of URLs with the key the parameter name required by the payment portal.
*/
abstract protected function getRequestURLs();
This method should almost always set the post-payment URL (e.g. on success, failure, completion etc.) to be PayBridge's completion page. An example showing an implementation of the abstract method is shown below, where the parameter returnURL
is set to return to PayBridge's completion page.
protected function getRequestURLs()
{
return array(
"returnURL" => $this->goToCompletionPage()
);
}
This function calls $this->goToCompletionPage()
, a helper function which returns the URL to PayBridge's completion page with the correct parameters appended. PayBridge also provides similar helper functions:
$this->goToPaymentsHomepage()
- Returns the URL to the main list of payment services, which may be useful if an error occurs or the user cancels payment.$this->goToOrderPage()
- Returns the URL to the page before payment, so they can review/amend their order. This may be useful for a 'Back' button at the payment provider.
Account details
Any account details, such as portal IDs, fund codes or account numbers should be set here.
/**
* Returns an array of payment portal parameters to identify the site with the portal
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestAccount();
Order Reference
Reference numbers specific to the order should be set in this method.
/**
* Returns an array of payment portal parameters to identify the payment order e.g. reference number etc...
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestOrderReference();
Amount
This method should set the amount the order is for, and any surcharges e.g. for credit cards.
/**
* Returns an array for the amount to pay parameter required by the payment portal
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestAmount();
Description
A description of the order being paid for should go here if needed. This could be text to appear on a bank statement, for example.
/**
* Returns an array of payment portal parameters that describe the order
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestDescription();
User Interface
Any styling parameters, such as logos, fonts etc. that the payment provider offers can be set here.
/**
* Returns an array of payment portal parameters that configure the portal user interface
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestUserInterface();
Payee
Any details about the person making the purchase the payment provider requires can be set here.
/**
* Returns an array of payment portal parameters containing payee details
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestPayee();
Miscellaneous
Any parameters which do not fit in other methods can be added here. This method can also be used for setting individual item information, if the basketEnabled
flag is set to 1 and the payment provider supports it.
/**
* Returns an array of other payment portal parameters that differ from a standard implementation
*
* @return array[string]string Array of parameters with the key the parameter name
*/
abstract protected function getRequestMisc();
Security Digest
This is the only abstract method in building the request which does not return an array by default. This function should be used to build and return a hash of parts of the request if required by the payment provider. It may be necessary to wrap this in a hidden input, for which $this->getHiddenFieldMarkup($name,$value)
can be used.
/**
* Returns a security digest parameter as required by the payment portal
*
* @return string
*/
abstract protected function getSecurityDigest();
Using configuration values in the request
Some parameters required by the payment provider may be best stored in the database. This is especially true of environment-specific settings, such as a payment portal ID, or endpoints. These values can be stored either encrypted (if sensitive) or as plain text, and can be accessed and edited via the PayBridge settings page in the Control Center.
Each value to be stored in the database should have its own row in JaduPayBridgeConfiguration
. The table below describes the fields the row will need to provide, and their purpose.
| Field | Data type | Purpose |
|-:-|-:-|-:-|
| id | integer | This is an auto number, and will be populated by the database when the row is inserted. |
| name | string | The name of the setting. Should not contain spaces, and should start with the configPrefix
value of the corresponding Adapter's record in JaduPayBridgePSPs |
| value | string | The value of the setting. Should be left blank if isEncrypted
is set to 1. |
| editable | integer | This should be set to 1 if the value should be editable in the Control Center, and must be 1 if isEncrypted
is 1. |
| isEncrypted | integer | This determines whether the value should be stored encrypted, Can be 1 (yes) or 0 (no). |
| encryptedValue | string | The encrypted value of the setting. Should be always be left blank. If isEncrypted
is set to 1, the setting should be populated through the PayBridge settings page in the Control Center. |
| isStandard | integer | Should always be 0. |
| friendlyName | string | The name of the setting to be displayed in PayBridge's setting page. |
After this, the new configuration item should be shown at the bottom of the PayBroidge settings page (Open > PayBridge > Settings). If the configuration item is encrypted, its value should now be entered in this interface and saved.
Figure 3: A new custom configuration
To use this setting in an adapter, make a call to the following method:
$this->getConfigMapper()->getByName('settingName');
In the above method call, settingName
should be replaced with the name
field (not friendlyName
) of the row added to JaduPayBridgeConfiguration
. To help build the settingName, the configPrefix
attribute can be used, e.g.
$this->getConfigMapper()->getByName($this->configPrefix . 'my_setting');
Using schema mappings in the request
Some parameters for the request may need to be configured on a per-form or per-route basis. PayBridge allows this with the use of schemas.
To take advantage of this, an XSD schema will need to be created which contains the fields that need to be configurable. An example schema is below:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="My Payment Provider">
<xs:complexType>
<xs:sequence>
<xs:element ref="fieldOne" minOccurs="1"/>
<xs:element ref="fieldTwo" minOccurs="1"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="fieldOne" type="xs:string"/>
<xs:element name="fieldTwo" type="xs:string"/>
</xs:schema>
Once this schema is placed on the server, the value of the schemaMappingFile
field in the JaduPayBridgePSPs
table should be set to point to this schema for the adapter, relative to the Jadu install root.
With the schema in place, it should be possible to access values that are mapped to it by using:
$this->getSchemaMappingValue([Order Item ID], [Field Name]);
Here, 'Order Item ID' should be replaced with the id of the order item in question, and 'Field Name' should be set to the name of the mapping to retrieve as it appears in the schema.
Step Five: Logging the request
It is important to log the outgoing request to the payment provider, so that any problems or queries can be traced at a later date. To enforce this, PayBridges expects custom adapters to implement an abstract function, logTransaction
. The signature of this is given below.
/**
* Logs the transaction
*/
abstract public function logTransaction();
This should be done by using Monolog. Adapters already have a logger object instantiated, so this method should call
$this->logger->log(Monolog\Logger::INFO, $logData);
where $logData
is the request data to be logged. The format of $logData
is described by the table below:
| Field name | Data | Description | Encrypted |
|-:-|-:-|-:-|-:-|
| Order ID | integer | The PayBridge order ID, usually $this->getOrder()->id
| No |
| Response Code | string | The response code returned by the PSP. Should be set to 'n/a' for requests | Yes |
| Jadu Order Reference | string | The order reference generated by Jadu. | Yes |
| PSP Order Reference | string | The reference returned by the PSP. Should be set to 'n/a' for requests | Yes |
| Amount | float | The order amount | No |
| PSP Name | string | The name of the payment provider used | No |
| Additional Info | string | Anything additional to be logged | Yes |
$logData should be a string, with the fields above in the table above, each separated by four spaces. Any encrypted fields should be wrapped with Jadu_XForms2_Encryption::getInstance()->encrypt()
.
An example of an implementation of logTransaction()
might be:
/**
* Log the payment request
*/
public function logTransaction()
{
$amount = 0;
foreach($this->orderItems as $item) {
$amount += $item->grossAmount;
}
$logData = $this->getOrder()->id . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('n/a') . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('JADU-'.$this->order->id)) . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('n/a') . ' ' .
number_format($amount,2) . ' ' .
'Payment Provider Name' . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('Some additional info');
$this->logger->log(Monolog\Logger::INFO, $logData);
}
Step Six: Handling the response
Once payment has been made at the payment provider, the user should be sent back to the return URL specified in the request, with some data in $_GET or $_POST detailing the transaction.
PayBridge requires this response data to be parsed by the adapter, so the user can be informed of the success or failure of their order, and any XForms can be submitted. To facilitate this, PayBridge expects the adapter to implement four abstract methods.
The first of these is getResponse()
. The signature for this is shown below.
/**
* Returns the response from the payment portal as an array. This would typically just be either the _POST or
* _GET array, but each payment portal differs in the method.
*
* @return array[string]string Response from portal
*/
abstract public function getResponse();
This method should return an array containing the data sent back by the payment provider, which could be as simple as returning $_GET or $_POST.
The response should also be logged here, using $this->logger->log(Monolog\Logger::INFO, $logData);
. For information on how create $logData
, see 'Step Five: Logging the request'.
The returned array from getResponse()
is then passed into verifyDataReceived($receivedData)
, which is the second abstract method that must be implemented.
/**
* Checks that the data received from the payment portal is valid and can be trusted.
*
* @param array[string]string $receivedData Array of data received from the portal (typically _POST)
* @return boolean True if data is verified, False if it isn't
*/
abstract public function verifyDataReceived($receivedData);
This method should be used to execute checks to see if the data is as expected. For instance, the payment provider may send back a hash to be verified. If this is not the case, a more basic check to see if all expected fields are present is still worthwhile. The method should return true
if $receivedData
is valid or false otherwise. If valid data is not received, the user is taken to the payments homepage by default, for security reasons.
Once the received data is verified, we can then parse the data, and instantiate some important objects. This is best done in parseResponse($response)
.
/**
* Takes the raw response from the payment portal (typically _POST data) and parses it into a usable format
* by the adapter
*/
abstract public function parseResponse($response);
Especially important to set up are $this->order
and $this->orderItems
, as these are used from this point on. To help with this, PayBridge provides some helpful data mappers:
$this->orderMapper
$this->orderItemMapper
The simplest way to use these is, assuming the PayBridge orderID is returned by the payment provider, is to do:
$this->order = $this->getOrderMapper()->getByID($response['orderID']);
$this->orderItems = $this->getOrderItemMapper()->getByOrderID($this->order->id);
It may also be necessary to set the card type on the order, as this will not have been known when the request was made. This could be done in the following way:
$this->order->cardType = $response['paymentMethod'];
$this->getOrderMapper()->save($this->order);
parseResponse($response)
should also assign $response
to $this->response
, so that response data can be accessed later in execution.
Once the response data is parsed, the remaining abstract method to implement is wasPaymentSuccessful()
. This should check the response code sent back from the payment provider to see if the payment was successful. If it was, this function should return true
, or false
otherwise.
/**
* Checks the response received from the payment portal (called after parseResponse) to see if the payment
* was accepted by the portal and no errors occurred.
*
* @return boolean True if payment was successful and false if any error occurred
*/
abstract public function wasPaymentSuccessful();
Example Adapter
Below is a complete adapter for JaduPay, Jadu's mock payment portal.
<?php
require_once 'PayBridge/DataMapper/ProductMapper.php';
require_once 'CacheManager.php';
class Jadu_PayBridge_PSP_Adapter_JaduPay extends Jadu_PayBridge_PSP_Adapter
{
public $friendlyName = "JaduPay";
public $integrationMethod = self::CLIENT_SITE_REDIRECT;
private $orderRef = '';
public function __construct()
{
$this->initialise();
parent::__construct();
}
public function initialise()
{
global $db;
$this->productMapper = new Jadu_PayBridge_DataMapper_ProductMapper($db, new Jadu_CacheManager());
$this->orderMapper = new Jadu_PayBridge_DataMapper_OrderMapper($db, new Jadu_CacheManager());
$this->orderItemMapper = new Jadu_PayBridge_DataMapper_OrderItemMapper($db, new Jadu_CacheManager());
$this->orderPayeeMapper = new Jadu_PayBridge_DataMapper_OrderPayeeMapper($db, new Jadu_CacheManager());
$this->configMapper = new Jadu_PayBridge_DataMapper_ConfigurationMapper($db, new Jadu_CacheManager());
}
protected function getTestEndpoint()
{
return getSecureSiteRootURL() . '/' . $this->configMapper->getByName($this->configPrefix . 'endpoint_test');
}
protected function getProductionEndpoint()
{
return $this->getTestEndpoint();
}
protected function getRequestURLs()
{
if ($this->isMotoOrder()) {
return array(
"returnURL" => $this->goToMOTOCompletionPage()
);
}
else {
return array(
"returnURL" => $this->goToCompletionPage()
);
}
}
protected function getRequestAccount()
{
return array();
}
protected function getRequestOrderReference()
{
return array(
"orderID" => $this->order->id,
"orderRef" => $this->getJaduOrderRef($this->order->id)
);
}
protected function getRequestAmount()
{
return array(
"total" => $this->getOrderTotal()
);
}
protected function getRequestDescription()
{
return array();
}
protected function getRequestUserInterface()
{
return array();
}
protected function getRequestPayee()
{
return array();
}
protected function getRequestLocalisation()
{
return array();
}
protected function getRequestMisc()
{
$items = $this->getOrderItems();
$basket = array();
foreach($items as $id => $item) {
$basket['items_'.$id] = $item->id . '|' . $item->grossAmount . '|test';
}
return $basket;
}
protected function getSecurityDigest()
{
return '';
}
public function verifyDataReceived($receivedData)
{
return isset($receivedData['responseCode']) && $receivedData['responseCode'] != '';
}
public function parseResponse($response)
{
$this->response = $response;
}
public function wasPaymentSuccessful()
{
if ($this->response['responseCode'] == '000') {
return true;
}
return false;
}
public function getResponse()
{
$this->order = $this->orderMapper->getByID($_GET['orderID']);
$this->payee = $this->orderPayeeMapper->getByOrderID($_GET['orderID']);
$this->orderItems = $this->orderItemMapper->getByOrderID($_GET['orderID']);
$logData = $this->getOrder()->id . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt($_GET['responseCode']) . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt($_GET['orderRef']) . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt($_GET['responseCode']) . ' ' .
number_format($this->getOrderTotal(),2) . ' ' .
$this->friendlyName . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('');
$this->logger->log(Monolog\Logger::INFO, $logData);
return $_GET;
}
public function getJaduOrderRef($orderID)
{
if (empty($this->orderRef)) {
$this->orderRef = 'JaduPay-'.$this->getOrderID().'-'.(isset($_SESSION['attemptCount']) ? $_SESSION['attemptCount'] : '0');
}
return $this->orderRef;
}
public function logTransaction()
{
$logData = $this->getOrder()->id . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('n/a') . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt($this->getJaduOrderRef($this->order->id)) . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('n/a') . ' ' .
number_format($this->getOrderTotal(),2) . ' ' .
$this->friendlyName . ' ' .
Jadu_XForms2_Encryption::getInstance()->encrypt('');
$this->logger->log(Monolog\Logger::INFO, $logData);
}
public function getMotoRedirectURL()
{
return getSecureSiteRootURL() . '/site/scripts/jadupay.php';
}
public function isEmbeddable()
{
return true;
}
}