Open Journal Systems, write a new payment module

Paul Marriott
7 min readMar 26, 2022

--

OJS — Open Journal Systems is a open source software to managing and publishing scholarly articles. One of the features is payment processing with modules for Paypal and Manual payment processing. My use case was to integrate a payment gateway called Bluesnap rather than Paypal. So here is how I did it.

The first step was to signup to Bluesnap https://sandbox.bluesnap.com/jsp/onboarding/ and get a user account that would let me access developer content and the sandbox. Sign-up required an email that looked like a business email. Sorry no GMAIL :-(

On Bluesnap I then setup two routes for payment, Hosted CheckOut and Hosted Payment. The difference is subtle but important, Hosted Payment would allow me to take a payment directly and Hosted CheckOut would allow me to show a product and take payment for that product. For my scenario the Hosted CheckOut was a better fit.

The product setup is as above, the link generated shows page to take payment. The quick and dirty method here from the journal would be to put the link on a static page and ask the customer to pay. This is not integrated and is a not a clean way to get paid.

The docs from Bluesnap https://support.bluesnap.com/ are useful but a little confusing when you start. It is possible to add parameters to the URL that calls the Hosted Checkout and get the payment gateway to make a call back to your own site.

https://sandbox.bluesnap.com/buynow/checkout?sku2770827=1&storeid=314287&sellerorderid=XXXXX&browsertitle=Checkout&currency=GBP&backtosellervisible=Y&sealvisible=Y&storecardvisible=N&thankyou.backtosellerurl=http%3A%2F%2F127.0.0.1%2Fojs%2Findex.php%2FIOJS%2Fpayment%2Fplugin%2FPaypalPayment%2Freturn%3FqueuedPaymentId%3D%21%7Bseller.order.id%7D%26status%3D%21%7Binvoice.status%7D%26amount%21%7Bamount%7D%26trancurrency%21%7Binvoice.currency%7D

sku2770827=1 This is the product id and the number of items. This came from the URL generated in Bluesnap

storeid=314287 This is the number of the store also from the Bluesnap URL

sellerorderid=XXXXX This is a placeholder for the order number to be sent from the OJS site

browsertitle=Checkout Set the title to show the word Checkout

currency=GBP Set the currency to GBP

thankyou.backtosellerurl=http%3A%2F%2F127.0.0.1%2Fojs%2Findex.php%2FIOJS%2Fpayment%2Fplugin%2FPaypalPayment%2Freturn%3FqueuedPaymentId%3D%21%7Bseller.order.id%7D%26status%3D%21%7Binvoice.status%7D%26amount%21%7Bamount%7D%26trancurrency%21%7Binvoice.currency%7D

This is the URL for the site to call after payment processing and must be encoded. Decoded the URL looks like this, this is the clever part :-)

http://127.0.0.1/ojs/index.php/IOJS/payment/plugin/PaypalPayment/return?queuedPaymentId=!{seller.order.id}&status=!{invoice.status}&amount!{amount}&trancurrency!{invoice.currency}

To create a new module in OJS for payment the best way is to take an existing module, copy and modify it. I took the Paypal module and modified the code.

First copy the paypal directory and rename to bluesnap. Then rename PaypalPaymentForm.inc.php to BluesnapPaymentForm.inc.php and PaypalPaymentPlugin.inc.php to BluesnapPaymentPlugin.inc.php. The file and directory structure is as above.

Modify the files as below. Files not shown remain unchanged, also note that the composer.phar file is not required.

version.xml

<?xml version=”1.0" encoding=”UTF-8"?>

<!DOCTYPE version SYSTEM “../../../lib/pkp/dtd/pluginVersion.dtd”>

<! —

* plugins/paymethod/bluesnap/version.xml

*

* Copy from Paypal to Bluesnap

*

* Modified to handle processing using bluesnap, extended from omnipay code

*

* Plugin version information.

<version>

<application>bluesnap</application>

<type>plugins.paymethod</type>

<release>1.1.0.0</release>

<date>2022–03–19</date>

</version>

index.php

<?php

/**

* @defgroup plugins_paymethod_paypal PayPal Payment Processing Plugin

*/

/**

* @file plugins/paymethod/paypal/index.php

*

* Copy from Paypal to Bluesnap

*

* @ingroup plugins_paymethod_paypal

* @brief Wrapper for PayPal payment plugin.

*/

require_once(‘BluesnapPaymentPlugin.inc.php’);

return new BluesnapPaymentPlugin();

BluesnapPaymentPlugin.inc.php

<?php

/**

* @file plugins/paymethod/paypal/BluesnapPaymentPlugin.inc.php

*

* Copy from Paypal to Bluesnap

*

* @class BluesnapPaymentPlugin

* @ingroup plugins_paymethod_paypal

*

* @brief Paypal payment plugin class

*/

import(‘lib.pkp.classes.plugins.PaymethodPlugin’);

require_once(dirname(__FILE__) . ‘/vendor/autoload.php’);

class BluesnapPaymentPlugin extends PaymethodPlugin {

/**

* @see Plugin::getName

*/

function getName() {

return ‘PaypalPayment’;

}

/**

* @see Plugin::getDisplayName

*/

function getDisplayName() {

return __(‘plugins.paymethod.paypal.displayName’);

}

/**

* @see Plugin::getDescription

*/

function getDescription() {

return __(‘plugins.paymethod.paypal.description’);

}

/**

* @copydoc Plugin::register()

*/

function register($category, $path, $mainContextId = null) {

if (parent::register($category, $path, $mainContextId)) {

$this->addLocaleData();

\HookRegistry::register(‘Form::config::before’, array($this, ‘addSettings’));

return true;

}

return false;

}

/**

* Add settings to the payments form

*

* @param $hookName string

* @param $form FormComponent

*/

public function addSettings($hookName, $form) {

import(‘lib.pkp.classes.components.forms.context.PKPPaymentSettingsForm’); // Load constant

if ($form->id !== FORM_PAYMENT_SETTINGS) {

return;

}

$context = Application::get()->getRequest()->getContext();

if (!$context) {

return;

}

$form->addGroup([

‘id’ => ‘paypalpayment’,

‘label’ => __(‘plugins.paymethod.paypal.displayName’),

‘showWhen’ => ‘paymentsEnabled’,

])

->addField(new \PKP\components\forms\FieldOptions(‘testMode’, [

‘label’ => __(‘plugins.paymethod.paypal.settings.testMode’),

‘options’ => [

[‘value’ => true, ‘label’ => __(‘common.enable’)]

],

‘value’ => (bool) $this->getSetting($context->getId(), ‘testMode’),

‘groupId’ => ‘paypalpayment’,

]))

->addField(new \PKP\components\forms\FieldText(‘merchantid’, [

‘label’ => __(‘plugins.paymethod.paypal.settings.accountName’),

‘value’ => $this->getSetting($context->getId(), ‘merchantid’),

‘groupId’ => ‘paypalpayment’,

]))

->addField(new \PKP\components\forms\FieldText(‘enc’, [

‘label’ => __(‘plugins.paymethod.paypal.settings.secret’),

‘value’ => $this->getSetting($context->getId(), ‘enc’),

‘groupId’ => ‘paypalpayment’,

]))

->addField(new \PKP\components\forms\FieldText(‘backtosellerurl’, [

‘label’ => __(‘plugins.paymethod.paypal.settings.backtosellerurl’),

‘value’ => $this->getSetting($context->getId(), ‘backtosellerurl’),

‘groupId’ => ‘paypalpayment’,

]))

->addField(new \PKP\components\forms\FieldText(‘sku’, [

‘label’ => __(‘plugins.paymethod.paypal.settings.sku’),

‘value’ => $this->getSetting($context->getId(), ‘sku’),

‘groupId’ => ‘paypalpayment’,

]))

->addField(new \PKP\components\forms\FieldText(‘storeid’, [

‘label’ => __(‘plugins.paymethod.paypal.settings.storeid’),

‘value’ => $this->getSetting($context->getId(), ‘storeid’),

‘groupId’ => ‘paypalpayment’,

]));

return;

}

/**

* @copydoc PaymethodPlugin::saveSettings()

*/

public function saveSettings($params, $slimRequest, $request) {

$allParams = $slimRequest->getParsedBody();

$saveParams = [];

foreach ($allParams as $param => $val) {

switch ($param) {

case ‘accountName’:

case ‘clientId’:

case ‘secret’:

case ‘username’:

case ‘password’:

case ‘merchantid’:

case ‘enc’:

case ‘backtosellerurl’:

case ‘sku’:

case ‘storeid’:

$saveParams[$param] = (string) $val;

break;

case ‘testMode’:

$saveParams[$param] = $val === ‘true’;

break;

}

}

$contextId = $request->getContext()->getId();

foreach ($saveParams as $param => $val) {

$this->updateSetting($contextId, $param, $val);

}

return [];

}

/**

* @copydoc PaymethodPlugin::getPaymentForm()

*/

function getPaymentForm($context, $queuedPayment) {

// $this->import(‘PaypalPaymentForm’);

// return new PaypalPaymentForm($this, $queuedPayment);

$this->import(‘BluesnapPaymentForm’);

return new BluesnapPaymentForm($this, $queuedPayment);

}

/**

* @copydoc PaymethodPlugin::isConfigured

*/

function isConfigured($context) {

if (!$context) return false;

if ($this->getSetting($context->getId(), ‘accountName’) == ‘’) return false;

return true;

}

/**

* Handle a handshake with the Bluesnap service

*/

function handle($args, $request) {

$journal = $request->getJournal();

$queuedPaymentDao = DAORegistry::getDAO(‘QueuedPaymentDAO’); /* @var $queuedPaymentDao QueuedPaymentDAO */

import(‘classes.payment.ojs.OJSPaymentManager’); // Class definition required for unserializing

try {

$queuedPayment = $queuedPaymentDao->getById($queuedPaymentId = $request->getUserVar(‘queuedPaymentId’));

if (!$queuedPayment) throw new \Exception(“Invalid queued payment ID $queuedPaymentId!”);

$status = $request->getUserVar(‘status’);

$amount = $request->getUserVar(‘amount’);

$trancurrency = $request->getUserVar(‘trancurrency’);

if (!$status) throw new \Exception(“Payment status not found”);

if ($status != ‘APPROVED’) throw new \Exception(“Payment was not approved”);

//Set the journal item as paid and bounce back to admin screen

$paymentManager = Application::getPaymentManager($journal);

$paymentManager->fulfillQueuedPayment($request, $queuedPayment, $this->getName());

$request->redirectUrl($queuedPayment->getRequestUrl());

} catch (\Exception $e) {

error_log(‘Bluesnap transaction exception: ‘ . $e->getMessage());

$templateMgr = TemplateManager::getManager($request);

$templateMgr->assign(‘message’, ‘plugins.paymethod.paypal.error’);

$templateMgr->display(‘frontend/pages/message.tpl’);

}

}

/**

* @see Plugin::getInstallEmailTemplatesFile

*/

function getInstallEmailTemplatesFile() {

return ($this->getPluginPath() . DIRECTORY_SEPARATOR . ‘emailTemplates.xml’);

}

}

BluesnapPaymentForm.inc.php

<?php

/**

* @file BluesnapPaymentForm.inc.php

*

* Copy from Paypal to Bluesnap

*

* @class BluesnapPaymentForm

*

* Form for Paypal-based payments.

*

*/

import(‘lib.pkp.classes.form.Form’);

class BluesnapPaymentForm extends Form {

/** @var PaypalPaymentPlugin */

var $_paypalPaymentPlugin;

/** @var QueuedPayment */

var $_queuedPayment;

/**

* @param $paypalPaymentPlugin PaypalPaymentPlugin

* @param $queuedPayment QueuedPayment

*/

function __construct($paypalPaymentPlugin, $queuedPayment) {

$this->_paypalPaymentPlugin = $paypalPaymentPlugin;

$this->_queuedPayment = $queuedPayment;

parent::__construct(null);

}

/**

* @copydoc Form::display()

*/

function display($request = null, $template = null) {

try {

$journal = $request->getJournal();

$merchantid = $this->_paypalPaymentPlugin->getSetting($journal->getId(), ‘merchantid’);

$enc = $this->_paypalPaymentPlugin->getSetting($journal->getId(), ‘enc’);

$backtosellerurl = $this->_paypalPaymentPlugin->getSetting($journal->getId(), ‘backtosellerurl’);

$skuid = $this->_paypalPaymentPlugin->getSetting($journal->getId(), ‘sku’);

$storeid = $this->_paypalPaymentPlugin->getSetting($journal->getId(), ‘storeid’);

//bounce to the bluesnap site to make payment if using the HOSTED PAYMENT

// $request->redirectUrl(‘https://sandbox.bluesnap.com/buynow/checkout?merchantid=' . $merchantid . ‘&enc=’ . $enc . ‘merchanttransactionid=’ . $this->_queuedPayment->getId() );

//Use if using the the HOSTED CHECKOUT

$request->redirectUrl(‘https://sandbox.bluesnap.com/buynow/checkout?'. $skuid .’=1&storeid=’ . $storeid . ‘&sellerorderid=’ . $this->_queuedPayment->getId() . ‘&browsertitle=Checkout&currency=GBP&backtosellervisible=Y&sealvisible=Y&storecardvisible=N&thankyou.backtosellerurl=’. $backtosellerurl);

} catch (\Exception $e) {

error_log(‘Bluesnap transaction exception: ‘ . $e->getMessage());

$templateMgr = TemplateManager::getManager($request);

$templateMgr->assign(‘message’, ‘plugins.paymethod.paypal.error’);

$templateMgr->display(‘frontend/pages/message.tpl’);

}

}

}

Also change the locale file

These code changes will add parameters to the payment module and enable OJS to call Bluesnap and get the payment status back automatically. The same logic could be modified for other payment gateways that follow the same design for taking payments.

Screen shots below:

Settings page in OJS
https://developers.bluesnap.com/docs/test-credit-cards useful for getting card numbers for testing

The last screen shows Bluesnap calling back to OJS which causes OJS to mark the payment as completed and redirect the caller back to the article submission page.

This work was largely trial and error (mostly error to start with). It does show that creating a new payment module in OJS is possible and that it can be done with relative ease, if you do enough reading and work through each issue step by step.

Good luck!

--

--

No responses yet