· 6 years ago · Oct 10, 2019, 11:02 AM
1/* OPS PAYMENT TERMINAL API FOR GOKASA.CZ PROTOCOL PAX COMGATE ECR
2===============================================================================
3===============================================================================
4===============================================================================
5===============================================================================
6===============================================================================
7===============================================================================
8===============================================================================
9===============================================================================
10===============================================================================
11===============================================================================
12===============================================================================
13===============================================================================
14===============================================================================
15===============================================================================
16===============================================================================
17===============================================================================
18===============================================================================
19===============================================================================
20===============================================================================
21===============================================================================
22===============================================================================
23===============================================================================
24===============================================================================
25===============================================================================
26===============================================================================
27===============================================================================
28===============================================================================
29===============================================================================
30===============================================================================
31===============================================================================
32===============================================================================
33===============================================================================
34===============================================================================
35===============================================================================
36===============================================================================
37===============================================================================
38===============================================================================
39===============================================================================
40===============================================================================
41===============================================================================
42===============================================================================
43===============================================================================
44===============================================================================
45===============================================================================
46===============================================================================
47===============================================================================
48===============================================================================
49===============================================================================
50===============================================================================
51*/
52
53
54/* examples: to call this api
55 var ptRequest = {
56 transactionType: 'sale',
57 ip: '10.0.0.102',
58 port: '2050',
59 amount: '20.00',
60 currency: 'CZK',
61 customerLanguage: 'cs',
62 referenceNumber: '17010000007',
63 password: 'sJ8niYXknkLAdlM3s8WnFLNR2GdCMGaM8G8JxC7SizwIbu7QztAzY44y4A8Z1rMcwS9kvBH11QsA7LLP'
64 };
65 $.ajax({
66 url: "https://localhost:3443/pt",
67 dataType: "jsonp",
68 jsonp: "callback",
69 data: ptRequest
70 }).done(function (resp) {
71 console.log(resp);
72 });
73 $.post({
74 url: "https://localhost:3443/pt",
75 data: ptRequest,
76 dataType: 'json',
77 }).done(function (resp) {
78 console.log(resp);
79 });
80
81 $.get("https://localhost:3443/transaction?referenceNumber=19010000003").done(function (resp) {
82 console.log(resp);
83 });
84*/
85
86
87var cors = require('cors');
88var path = require('path');
89var net = require('net');
90var fs = require('fs');
91var api_password = 'sJ8niYXknkLAdlM3s8WnFLNR2GdCMGaM8G8JxC7SizwIbu7QztAzY44y4A8Z1rMcwS9kvBH11QsA7LLP';
92if (fs.existsSync('./password.txt')) {
93 api_password = fs.readFileSync(path.join(__dirname, 'password.txt'), 'utf8').trim();
94}
95var date = require('./date');
96var header = require('./header');
97var body = require('./body');
98var ASCII = require('./ascii');
99var utils = require('./utils');
100var iconv = require('iconv-lite');
101
102String.prototype.showUnprintable = function () {
103 var res = "";
104 for (var i = 0; i < this.length; i++) {
105 var translated = this[i];
106
107 var charCode = this[i].charCodeAt(0);
108 if (charCode >= 0 && charCode <= 32) {
109 translated = utils.getUnprintableChar(charCode);
110 }
111 res += translated;
112 }
113 return res;
114};
115
116String.prototype.toHexString = function () {
117 var res = "";
118 for (var i = 0; i < this.length; i++) {
119 var hex = utils.decimalToHex(this[i].charCodeAt(0), 2);
120 if (hex.length === 1) {
121 hex.length = "0" + hex;
122 }
123 res += hex;
124 }
125 return res;
126};
127
128var db = require('./database');
129var config = require('./config');
130var express = require('express');
131var app = express();
132var bodyParser = require('body-parser');
133var moment = require('moment');
134
135var homepage = require('./homepage');
136var transactionspage = require('./transactionspage');
137
138app.use(bodyParser.urlencoded({extended: false}));
139app.use(bodyParser.json());
140app.use(cors());
141
142app.use('/public', express.static(path.join(__dirname, 'public')));
143
144app.get('/', function (req, res) {
145 res.send(homepage());
146});
147
148app.get('/version', function (req, res) {
149 res.json({ version: config.version });
150});
151
152app.get('/transactions', function (req, res) {
153 if (req.query.auth !== api_password) {
154 return res.sendStatus(401);
155 }
156 res.send(transactionspage());
157});
158
159app.get('/transactions.json', function (req, res) {
160 if (req.query.auth !== api_password) {
161 return res.sendStatus(401);
162 }
163 var query = {};
164 if (req.query.month) {
165 if(!/^[12]\d{3}(0[1-9]|1[0-2])$/.test(req.query.month)) {
166 return res.sendStatus(400);
167 }
168 query = { month: parseInt(req.query.month) };
169 }
170 var results = db.getCollection('transactions').find(query);
171 var transactions = JSON.parse(JSON.stringify(results)).map(function (t, index) {
172 var responseWithoutText = JSON.parse(JSON.stringify(t.response.dataFields || {}));
173 delete responseWithoutText.t;
174 delete responseWithoutText.R;
175 var rc = t.response.responseCode;
176 var rcStyle = /^R00\d|R010$/.test(rc) ? 'style="color: green" ' : 'style="color: red" ';
177 var responseText = t.response.dataFields && t.response.dataFields.t ? t.response.dataFields.t.split(/[\r\n]+/).map(function (line) {
178 var replaced = line;
179 if(/^\[?\d{2} /.test(line)){
180 replaced = line.replace(/[ \[\]]/g, '');
181 }
182 if(/^Refer/.test(line)){
183 replaced = line.replace(/ +/g, ' ');
184 }
185 return replaced;
186 }).join('\r\n') : '';
187 return [
188 moment(t.date).format('YYYY-MM-DD HH:mm:ss'),
189 t.transactionType,
190 t.referenceNumber ? ('<a class="transaction" href="/transaction?referenceNumber=' + t.referenceNumber + '&auth=' + api_password + '">' + t.referenceNumber + '</a>') : '',
191 t.response.tags || '',
192 rc ? '<span ' + rcStyle + 'title="' + utils.translateResponseCode(rc) + '">' + rc + '</span>' : '',
193 '<pre>' + (JSON.stringify(responseWithoutText, null, 2) || '') + '</pre>',
194 t.response.dataFields ? (responseText ? ('<pre>' + responseText + '</pre>') : '') : '',
195 ];
196 });
197 res.json({ data: transactions });
198});
199
200app.get('/transaction', function (req, res) {
201 // return res.json({success: true, msg: [{
202 // "tags": "0000",
203 // "responseCode": "R000",
204 // "responseMessage": "Transaction declined",
205 // response: {"dataFields": {
206 // "T": "00",
207 // "R": "082",
208 // "E": "2105",
209 // "f": "ISO_8859-2",
210 // "t": "21/04/19 19:22:00 0019\nDÁVKA:016 ÚČTENKA:0019\nID.ZAŘÍZENÍ: 460710 \n-------------------------\nA0000000043060 \nMaestro (S)\n *** **** **** **** 0049\n PRODEJ \nČástka CZK: 173,80\n TRANSAKCE ZRUŠENA \nAutorizační kód: A80608\n[08 04 00 00 01 00 00 00 00 00 01 00 ]\n-------------------------\n Doklad uchovejte pro \n pozdější kontrolu \n\n"
211 // }}
212 // }]
213 // })
214 if (req.query.auth !== api_password) {
215 return res.sendStatus(401);
216 }
217 if (!req.query.referenceNumber) {
218 return res.status(400).json({ success: false, msg: 'srv_invalid_format' });
219 }
220 var query = { referenceNumber: req.query.referenceNumber };
221 if (req.query.transactionType) {
222 query.transactionType = req.query.transactionType;
223 }
224 var results = db.getCollection('transactions').find(query);
225 res.json({ success: true, msg: results.map(function (r) {
226 return {
227 date: r.date,
228 referenceNumber: r.referenceNumber,
229 request: r.request,
230 response: r.response,
231 transactionType: r.transactionType,
232 }
233 })});
234});
235
236var sessionIdFilePath = path.join(__dirname, 'session.txt');
237var sessionId = parseInt(fs.readFileSync(sessionIdFilePath, 'utf8'));
238// reset sessionId sequence - the sessionId must be unique within each day, we assume that within one day there will be no more than 9900 requests
239if (sessionId >= 9900) {
240 fs.writeFile(sessionIdFilePath, 0);
241}
242
243app.all('/pt', function (req, res) {
244 var payload = req.query;
245 var respMethod = 'jsonp';
246 if (!req.query.callback) {
247 payload = req.body;
248 respMethod = 'json';
249 }
250 var response = {
251 tags: '0000',
252 responseCode: 'R000',
253 responseMessage: 'Transaction OK',
254 dataFields: {}
255 };
256 // fake response
257 // return setTimeout(function () {
258 // return res.jsonp({ "success": true,
259 // "msg": {
260 // "tags": "0000",
261 // "responseCode": "R000",
262 // "responseMessage": "Transaction OK",
263 // "dataFields": {
264 // "t": " \n COMGATE TEST \n ulica \n mesto 00000 \n \n07.09.2019 13:07:35\n-------------------------------\n STVRZENKA PRO OBCHODNÍKA \n-------------------------------\n Uzávěrka \n Součty souhlasí \n \nTerminal: CGTEST06 \n-------------------------------\nTERMINAL \nPřijato 0000 0.00Vráceno 0000 0.00\n\n RC: 007, Prijata admin \n-------------------------------\n Děkujeme Vám. Uschovejte pro\n kontrolu. \n Verze: KS 01.02 (043) ",
265 // "r": "9",
266 // "m": "Nesprávny MAC",
267 // "S": "19010000224"
268 // }
269 // }
270 // });
271 // }, 2000);
272 var password = payload.password;
273 if (!password || password !== api_password) {
274 return res.status(400)[respMethod]({success: false, msg: 'Incorrect API Password'});
275 }
276 var ipValidator = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
277 var ptIpAddress = payload.ip;
278 if (!ptIpAddress || !ipValidator.test(ptIpAddress)) {
279 return res.status(400)[respMethod]({success: false, msg: 'Incorrect IP Address for Payment Terminal'});
280 }
281 var ptPort = payload.port;
282 if (!ptPort || !/\d{1,5}/.test(ptPort)) {
283 return res.status(400)[respMethod]({success: false, msg: 'Incorrect Port for Payment Terminal'});
284 }
285 ptPort = parseInt(ptPort)
286 if (ptPort > 65536 || ptPort < 1025) {
287 return res.status(400)[respMethod]({success: false, msg: 'Incorrect Port for Payment Terminal'});
288 }
289
290 utils.log('');
291 var client = new net.Socket();
292 utils.log('Connecting to ' + ptIpAddress + ':' + ptPort);
293
294 var socketClosed = false;
295 var activityTimeoutExtension = 120000;
296 var timeoutAction = function() {
297 utils.log('[ERROR] Activity timed out - No activity in ' + activityTimeoutExtension + 'ms');
298 if (!socketClosed) {
299 client.destroy();
300 }
301 if (!res.headersSent) {
302 res[respMethod]({ success: false, msg: 'pt_timeout' });
303 }
304 };
305 var activityTimeout = 0;
306
307 client.connect(ptPort, ptIpAddress);
308 var enqInterval;
309 var maxTry = 5;
310 var enqWaitTime = 3000;
311 var tryCount = 0;
312 var lastSentCommand;
313 client.on('connect', function () {
314 activityTimeout = setTimeout(timeoutAction, activityTimeoutExtension);
315 utils.log('Connected');
316 utils.log('Sending : ' + ASCII.ENQ.showUnprintable());
317 client.write(ASCII.ENQ);
318 enqInterval = setInterval(function () {
319 if (!socketClosed) {
320 utils.log('Sending : ' + ASCII.ENQ.showUnprintable());
321 client.write(ASCII.ENQ);
322 tryCount++;
323 if (tryCount >= maxTry) {
324 clearInterval(enqInterval);
325 client.destroy();
326 if (!res.headersSent) {
327 res[respMethod]({ success: false, msg: 'pt_timeout' });
328 }
329 }
330 }
331 }, enqWaitTime);
332 lastSentCommand = ASCII.ENQ;
333 });
334
335 client.on('error', function(err) {
336 clearTimeout(activityTimeout);
337 if (err.code === 'ENOTFOUND') {
338 utils.log('[ERROR] No device found at this address!');
339 client.destroy();
340 return;
341 }
342
343 if (err.code === 'ECONNREFUSED') {
344 utils.log('[ERROR] Connection refused! Please check the IP.');
345 client.destroy();
346 return;
347 }
348 utils.log('[CONNECTION] Unexpected error! ' + err.message);
349 });
350
351 var packetId = -1;
352 client.on('data', function (data) {
353 clearTimeout(activityTimeout);
354 activityTimeout = setTimeout(timeoutAction, activityTimeoutExtension);
355 var stringData = iconv.decode(data, 'utf8');
356 utils.log('Received : ' + stringData.showUnprintable().replace(/\\n/g, '\n'));
357 var ptResp = utils.parseResponse(stringData);
358 if (ptResp.command === ASCII.ACK && lastSentCommand === ASCII.ENQ) {
359 clearInterval(enqInterval);
360 sessionId++;
361 fs.writeFile(sessionIdFilePath, sessionId);
362 packetId++;
363 var message = utils.createMessage({ command: 'START_RQ', sessionId, packetId, payload });
364 utils.log('Sending : ' + message.showUnprintable());
365 client.write(message);
366 lastSentCommand = 'START_RQ';
367 return true;
368 }
369 if (ptResp.command === 'START_RSP') {
370 if (ptResp.error) {
371 utils.log('Error in response: ' + ptResp.error);
372 client.destroy();
373 return false;
374 }
375 utils.log('Sending : ' + ASCII.ACK.showUnprintable());
376 client.write(ASCII.ACK);
377 packetId++;
378 var message = utils.createMessage({ command: 'RQ_SRV', sessionId, packetId, payload });
379 utils.log('Sending : ' + message.showUnprintable());
380 client.write(message);
381 lastSentCommand = 'RQ_SRV';
382 return true;
383 }
384 if (ptResp.command === 'INFO') {
385 utils.log('Sending : ' + ASCII.ACK.showUnprintable());
386 client.write(ASCII.ACK);
387 lastSentCommand = ASCII.ACK;
388 var printDataField;
389 if (ptResp.p) {
390 printDataField = 'p';
391 } else if (ptResp.P) {
392 printDataField = 'P';
393 }
394 if (printDataField) {
395 if (response.dataFields.t) {
396 response.dataFields.t += ptResp[printDataField];
397 } else {
398 response.dataFields.t = ptResp[printDataField];
399 }
400 }
401 return true;
402 }
403 if (ptResp.command === 'RSP_SRV') {
404 if (ptResp.fields) {
405 response.responseCode = ptResp.responseCode;
406 response.responseMessage = ptResp.fields.m || 'N/A';
407 response.dataFields = Object.assign({}, response.dataFields, ptResp.fields);
408 }
409 utils.log('Sending : ' + ASCII.ACK.showUnprintable());
410 client.write(ASCII.ACK);
411 packetId++;
412 var message = utils.createMessage({ command: 'FINISH', sessionId, packetId, payload });
413 utils.log('Sending : ' + message.showUnprintable());
414 client.write(message);
415 lastSentCommand = 'FINISH';
416 return true;
417 }
418 if (ptResp.command === 'COMPLETE') {
419 utils.log('Sending : ' + ASCII.ACK.showUnprintable());
420 client.write(ASCII.ACK);
421 packetId++;
422 var message = utils.createMessage({ command: 'END', sessionId, packetId, payload });
423 utils.log('Sending : ' + message.showUnprintable());
424 client.write(message);
425 lastSentCommand = 'END';
426 return true;
427 }
428 if (ptResp.command === ASCII.ACK && lastSentCommand === 'END') {
429 if (!res.headersSent) {
430 res[respMethod]({ success: true, msg: response });
431 }
432 var now = new Date();
433 var month = now.getMonth() + 1;
434 var monthPrefix = parseInt(now.getFullYear() + '' + (month < 10 ? '0' + month : month));
435 var request = JSON.parse(JSON.stringify(payload));
436 delete request.callback;
437 delete request.ip;
438 delete request.port;
439 delete request.password;
440 delete request._;
441 var record = {
442 transactionType: payload.transactionType,
443 date: now,
444 month: monthPrefix,
445 referenceNumber: payload.referenceNumber,
446 request: request,
447 response: response,
448 };
449 db.getCollection('transactions').insert(record);
450 return true;
451 }
452 });
453
454 client.on('close', function () {
455 clearTimeout(activityTimeout);
456 socketClosed = true;
457 utils.log('Connection closed');
458 utils.log('');
459 client.destroy();
460 if (!res.headersSent) {
461 res[respMethod]({ success: false, msg: 'pt_fail' });
462 }
463 });
464});
465
466var httpsPort = 3443;
467var https = require('https');
468var httpsServer = https.createServer({
469 key: fs.readFileSync(path.join(__dirname, '..', 'ssl', 'vcap-me-key.pem')),
470 cert: fs.readFileSync(path.join(__dirname, '..', 'ssl', 'vcap-me-cert.pem'))
471}, app);
472httpsServer.listen(httpsPort, function () {
473 console.log('OPS Payment serminal server PAX Listening on https://127.0.0.1:' + httpsPort);
474});
475httpsServer.on('close', function () {
476 console.log('Payment terminal server PAX has closed');
477});
478
479module.exports = httpsServer;