📜 ⬆️ ⬇️

Qiwi IPN on Node.js

To realize the possibility of payment through the Qiwi payment gateway, it’s enough to read the developer’s guide, which, by the way, is in Russian. But for those who have deadlines and do not want to spend a lot of time on development, I will try to facilitate the development process with my own calculations with the code.

The initial data - Node.js - 0.12.4, Sails - v0.12.11.

To start development, you need to register and wait for confirmation of your account at https://ishop.qiwi.com . After confirming the account, you need to go to Settings → Protocols → REST-protocol and in the plate “Authentication data” you can see the project ID - store ID (SHOP_ID) to check the REST responses. Additionally, you need to click "Generate a new ID" - and generate an API_ID for REST requests to the Qiwi API. I want to note that you need to write a password (API_PWD), then there will be no place to see it.

I would like to first grieve the programmers and notify that Qiwi does not have a sandbox, such as Paypal, all work will initially be done on live servers with real money and cards.
')
To begin, learn to send a request for invoicing. In short: the entire payment process may consist of issuing an invoice, getting a link for payment, going to the site where the client pays for the service and waiting for the server to answer from Qiwi IPN server.

// AccountController.js module.exports = { // action for payment request qw_activate: function (req, res) { var user_id = user.id; var bill_id = user_id +'_'+ Date.now(), order_lifetime_days = 1, successUrl = req.param('success_return_url'), // redirect URL in case of success payment failUrl = req.param('fail_return_url'); // redirect URL in case of payment is failed var url = sails.config.custom_config.QIWI.API_URL+sails.config.custom_config.QIWI.SHOP_ID+'/bills/'+bill_id, request = require('request'), querystring = require('querystring'); var request_data = {headers: { "Accept": "text/json", "Authorization": 'Basic '+new Buffer( sails.config.custom_config.QIWI.API_ID +':'+ sails.config.custom_config.QIWI.API_PWD ).toString('base64'), "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" }}; request_data.url = url; var lifetime = new Date(); lifetime.setHours(lifetime.getHours() + 24 * order_lifetime_days); request_data.body = querystring.stringify({ user: 'tel:'+req.param('phone').replace(/[\(\)]/g, ""), amount: sails.config.custom_config.QIWI.member_pro_membership_cost, ccy: sails.config.custom_config.QIWI.CURRENCY, // RUB || USD comment: "Payment for service by "+user.email, lifetime: lifetime.toISOString(), pay_source: 'qw', // 'mobile' prv_name: 'email@mail.ru' }); request.put(request_data, function (err, data) { if(err) return res.badRequest(err); // q if(JSON.parse(data.body).response.result_code == 0) { return res.ok({ url: 'https://qiwi.com/order/external/main.action?shop='+sails.config.custom_config.QIWI.SHOP_ID+'&transaction='+bill_id+'&successUrl='+successUrl+'&failUrl='+failUrl+'&iframe=false' }); } res.badRequest({ message: JSON.parse(data.body).response.description }); }); } } 

Next, send a request to create an account - get a link to pay for it, send the client to the Qiwi website. After the client has paid on the Qiwi website, depending on the outcome of the payment, the client sends the page indicated in the link to the payment to the successUrl or failUrl. Regardless of the outcome of the payment (cancellation, success, delay, error, etc.) - our server is open to receive responses from the Qiwi IPN server. Answers can be both on https and http. If you can transfer your API server to https - I advise you to use this protocol - it is safer.

In the code, I have a piece of code that is responsible for checking the answers on https, but it is not verified, this code can be taken as a basis. To receive answers about the status of payment via http or https to your server, you must configure the section "Pull Settings (REST) ​​Protocol" in the settings of your Qiwi personal account. You must enable notifications and specify the URL for notifications. The port for http is only 80, for https it is 443. You will not be able to specify other ports. You need to generate a password for alerts by clicking on "Change password alert". After that you can start writing the code:

 // AccountController.js module.exports = { // ipn action qw_ipn: function (req, res) { var Q = require('q'), THIS = this; (function (req, res) { var deferred = Q.defer(); var reqParams = req.allParams(); var UID = reqParams.bill_id ? reqParams.bill_id.split('_')[0] : null, payment_date = reqParams.bill_id ? new Date(parseInt(reqParams.bill_id.split('_')[1])) : null, txn_id = "txn_" + reqParams.bill_id, txn_status = reqParams.status; (function() { var deferred2 = Q.defer(); if (typeof req.headers.authorization !== 'undefined') { // Basic authorization if (req.headers.authorization == 'Basic ' + new Buffer(sails.config.custom_config.QIWI.SHOP_ID + ':' + sails.config.custom_config.QIWI.NOTIFICATION_PWD).toString('base64')) { deferred2.resolve(); } else { deferred2.reject(150); // Error in password verification } } else if (typeof req.headers['x-api-signature'] !== 'undefined') { // digital sign // TODO: code not verified var crypto = require('crypto'), hexHash, signature = req.headers['x-api-signature'], encoded_signature, reqString = ""; var sortedIndexes = Object.keys(reqParams).sort(); // sort keys // generate string from values of sorted request for (var i in sortedIndexes) { reqString += "|" + reqParams[sortedIndexes[i]]; } reqString = THIS._convertUTF16ToUTF8ToByteStr(reqString.substring(1)); // convert UTF16 string to UTF8 and then to string of bytes hexHash = crypto.createHmac('sha1', THIS._convertUTF16ToUTF8ToByteStr(sails.config.custom_config.QIWI.SHOP_ID)).update(reqString).digest('hex'); // hashed string hexadecimal encoded_signature = new Buffer(THIS._convertUTF16ToUTF8ToByteStr(hexHash)).toString('base64'); // base64 encoded if (encoded_signature == signature) { // compare encoded signature with signature from header deferred2.resolve(); } else { deferred2.reject(151); // Error in sign verification } } return deferred2.promise; })().then(function() { if(parseFloat(reqParams.amount) !== sails.config.custom_config.QIWI.member_pro_membership_cost) return deferred.resolve(0); // ignore creating transactions for commission Transaction.findOne({txn_id: txn_id, payment_status: txn_status}).exec(function (err, found) { if (err) return deferred.reject('Invalid updating payment status. Error: ' + err); (function() { var deferred3 = Q.defer(); if (!found) { var params = { txn_id: txn_id, txn_type: reqParams.command, // "bill" mc_gross: reqParams.amount, mc_currency: reqParams.ccy, payment_date: payment_date, payment_status: reqParams.status, business: reqParams.prv_name, receiver_email: reqParams.prv_name, payer_id: UID, payer_email: reqParams.user, custom: JSON.stringify({error: reqParams.error}), gateway_type: Transaction.attributes.gateway_type.in[1] // qiwi gateway }; // first payment Transaction.create(params).then(function (created) { if (created) { deferred3.resolve(); } }).catch(function (err) { if (err) deferred3.reject('Invalid transaction creation. Error: ' + err); }); } else { // already exists deferred.resolve(0); } return deferred3.promise; })().then(function() { if (parseFloat(reqParams.amount) == sails.config.custom_config.QIWI.member_pro_membership_cost && reqParams.ccy == sails.config.custom_config.QIWI.CURRENCY) { Model.findOne({id: UID}).then(function (found_user) { if(found_user) { switch(reqParams.status) { case 'paid': // mark user as paid ... break; case 'rejected': // mark user as unpaid if he was rejected payment ... break; } } else { if (err) return deferred.reject('User not found. Error: ' + err); } }).catch(function (err) { if (err) return deferred.reject('Error while searching user. Error: ' + err); }); } else { deferred.reject('Not valid currency or payment amount.'); } }, function(err) { deferred.reject('Error while transaction creation. Error: ' + err); }); }); }, function(err) { deferred.reject(err); }); return deferred.promise; })(req, res).then(function (result_code) { res.setHeader("Content-type", "text/xml"); var xml = '<?xml version="1.0"?>\ <result>\ <result_code>' + result_code + '</result_code>\ </result>'; return res.send(xml); }, function (error) { console.log(error); res.setHeader("Content-type", "text/xml"); var errNum = typeof error == 'number' ? error : 13; var xml = '<?xml version="1.0"?>\ <result>\ <result_code>' + errNum + '</result_code>\ </result>'; return res.send(xml); }); }, /** * Convert UTF16 string to UTF8 and then to bytes * @param str * @returns {string} * @private */ _convertUTF16ToUTF8ToByteStr: function (str) { var utf8 = unescape(encodeURIComponent(str)); var byteString = ""; for (var i = 0; i < utf8.length; i++) { byteString += utf8.charCodeAt(i); } return byteString; } } 

The code is not complicated, but there may be some questions that I will try to predict and give answers to them below.

The Qiwi IPN server repeats the request at an incrementing interval during the day (50 attempts in total) before receiving the result code 0 and the HTTP status code 200 in the response. To avoid duplicate payment, I, at the first notification, create a transaction with the account number, later, if the transaction With this account there is - I reject requests. I am also interested in payment and refund, that is, “paid” and “rejected” payment statuses.

To understand the types of the request I post routes to the action.

 // routes.js module.exports.routes = { 'POST /qw_ipn': 'AccountController.qw_ipn', 'POST /qw_activate': 'AccountController.qw_activate' } 

This concludes my short and first post. It is better to read the code with the Qiwi API documentation, all the error numbers are there, the business logic is listed, and others. Those who have read thanks for reading. I would welcome any comments. I love criticism.

Source: https://habr.com/ru/post/323384/


All Articles