When developing e-commerce extensions, such as an online store, paid services or consultations, etc., you need to arrange payment acceptance, which will be convenient to use.
This can be implemented using the "Payments" module.
This module can be integrated with any online payment system: PayPal, UKassa, bank payment systems, etc.
To create a payment, add the following code in the controller of your extension after the order is generated:
$amount = 500; // Payment amount. Pennies are transferred as a fractional part. $area = 'orderType'; // type of payment (order or service code) $options = [ // Optional. When paying for periodic services, if the period is not stored // in your extension, the expiration date of the paid service can be stored // in this field. 'time' => $months * 30 * 24 * 60 * 60, // Optional. Description or purpose of the payment. 'desc' => 'Order description', // Optional. The order ID on the side of the extension being developed or the user ID. // Can be used to identify the payment by your extension. 'code' => $orderId, // Optional. Where to redirect the user after successful payment. // If not specified, the user will be redirected to their payment history. // https://domain.tld/payments?m=balance&n=history 'redirect' => cot_url('extension-code', ['m' => 'orders', 'id' => $orderId], '', true) ] cot_payments_create_order($area, $amount, $options);
A pair of values $area
and $options['code']
should uniquely identify the payment so that your extension can correctly automatically activate the paid service after payment.
The cot_payments_create_order()
function will create a payment and redirect the user to the payment page, where the user will be able to select one of the installed payment system plugins and make a payment.
After making the payment, the user will be redirected to a page with his payments history and there will be shown a message about a successful payment or an error.
If the $options['redirect']
, parameter was passed when creating the payment, then in case of successful payment, user will be redirected to the specified url.
At the moment when the payment status changes to "Paid" the payments.payment.success hook is triggered.
Your extension should handle this hook to automatically activate the paid service. An array with payment data is available to the hook handler. This is an array with data from the cot_payments table containing the payment record being processed..
Please note that this hook is usually called at the moment when the payment system calls the web-hook with payment notification. The request is made from the payment system's server and the user making the payment has nothing to do with this request and the session of this user is unavailable at this moment. Therefore, the handler of this hook should not output any data or headers and should not make any redirects.
To display any information to the user, you need to use the URL passed through $options['redirect']
, if the standard one is not enough. Usually, when a user goes on this URL, the payment has already been processed and the result can be displayed to the user.
To test your extension integration with the payment module, you can use the Nullbilling plugin. It is a plugin with a test payment system that does not make any payments, but simulates a successful payment.
You can add a new payment system by installing the appropriate plugin. If there is no ready-made one, then you can write your own.
Let's consider creating such a plugin.
The plugin must register itself as a payment system plugin. To do this, it must process the payments.billing.register hook and add its data to the $cot_billings
array. For example, like this:
if ( Cot::$cfg['plugin']['myPaymentSystem']['on'] && !empty(Cot::$cfg['plugin']['myPaymentSystem']['apiKey']) && !empty(Cot::$cfg['plugin']['myPaymentSystem']['secret']) ) { $cot_billings['myPaymentSystem'] = [ 'plug' => 'myPaymentSystem', 'title' => Cot::$L['tbank_title'], 'icon' => Cot::$cfg['plugins_dir'] . '/myPaymentSystem/images/logo.png', ]; }
If the payment system sends payment notification (web-hook) using the POST method, it is necessary to disable xss protection for the web-hook handler, otherwise Cotonti will not accept the request from the payment system. To do this, you need to make the input hook handler with the following code:
if ( isset($_GET['e']) && $_GET['e'] === 'myPaymentSystem' && isset($_GET['a']) && $_GET['a'] === 'notify' && $_SERVER['REQUEST_METHOD'] === 'POST' ) { define('COT_NO_ANTIXSS', 1) ; Cot::$cfg['referercheck'] = false; }
Add $_GET parameters for which the payment notification (web-hook) handler is available to the if condition.
The plugin has a standalone hook handler, but it does not output any data to the user, but uses it for other purposes. The "Open" button for this plugin is not needed in the admin panel in the list of extensions. Let's put it away. To do this, add the code to the admin.extensions.plug.list.loop hook handler:
if ($type === COT_EXT_TYPE_PLUGIN && $code === 'myPaymentSystem') { $t->assign([ 'ADMIN_EXTENSIONS_JUMPTO_URL' => null, ]); }
And to admin.extensions.details hook:
if ($type === COT_EXT_TYPE_PLUGIN && $code === 'myPaymentSystem') { $standalone = null; $t->assign([ 'ADMIN_EXTENSIONS_JUMPTO_URL' => $standalone, ]); }
If you need to display any reference information for the site administrator on the plugin settings page in the admin panel, for example, the settings that need to be made in the personal account of the payment system itself, use the admin.config.edit.tags hook
if ($o === COT_EXT_TYPE_PLUGIN && $p === 'myPaymentSystem') { $adminHelp = '... Справка по настройке платежной системы ...'; }
And will make the standalone hook handler. It solves 2 tasks, depending on the passed parameters:
1) On the payment method selection page, when clicking on the payment system link, the user is redirecting to this page. The GET parameter pid contains the local payment ID in the cot_payments table.
Get the payment data:
// Get payment ID
$paymentId = cot_import('pid', 'G', 'INT');
if (!$paymentId) {
cot_die_message(404);
}
// Get pyment data from DB
$payment = PaymentRepository::getById($paymentId);
if (!$payment) {
cot_die_message(404);
}
// Check if the payment status allows to make a payment
cot_block(in_array($payment['pay_status'], PaymentDictionary::ALLOW_PAYMENT_STATUSES, true));
The further scenario depends on the API of the payment system. It usually consists of the following: send a request to the payment system to initialize the payment on its side. In response, the payment system returns the URL to which the user must be redirected to make the payment.
Many payment systems allow to transfer URLs in the payment initialization parameters, where needed to redirect the user after payment. You can get them using the following methods: successful payment url - PaymentService::getSuccessUrl($PaymentId)
and the fail payment URL - PaymentService::getFailUrl($paymentId)
.
Before redirecting the user to the payment system's website to make a payment, set the local payment status "In progress" and specify which payment system processes the payment.
PaymentService::setStatus( $paymentId, PaymentDictionary::STATUS_PROCESS, 'myPaymentSystem', PaymentDictionary::METHOD_CARD, $psPaymentId );
The last 2 parameters are optional. The penultimate one indicates that the payment will be made with bank card, and the last one is the payment identifier passed to the payment system.
Some payment systems require you to pass a unique payment ID each time. And in the case when the payment failed and the user tries to pay again, the old payment ID is no longer accepted. For such payment systems, your plugin must generate a unique payment ID. For example, like this:
$psPaymentId = $paymentId . '-' . time();
We save it in the cot_payments table, passing the 5th parameter to PaymentService::setStatus()
.
2) When a user makes a payment on the payment system's website, before returning the user back to your site, the payment system sends a payment notification (web hook) with the payment status. It needs to be processed. There is no need to do this in the standalone hook, you can also do it in the ajax hook.
When the notification from the payment system has been processed and the checks have been completed in accordance with the documentation for the payment system, it is necessary to set the local payment status to the "Paid"
PaymentService::setStatus( $paymentId, PaymentDictionary::STATUS_PAID, 'myPaymentSystem', null, null, $transactionId )
here we skip the parameters $paymentMethod
, $paymentSystemPaymentId
- they were set earlier. But we pass $transactionId
(optional) - the identifier of the transaction (or payment) on the payment system side. Most payment systems pass it with payment notification (web-hook).