diff --git a/anti-same-day-dupe.js b/anti-same-day-dupe.js new file mode 100644 index 0000000..426c940 --- /dev/null +++ b/anti-same-day-dupe.js @@ -0,0 +1,150 @@ +var redis = require('redis'); +var LRU = require('lru-cache'); +var moment = require('moment'); + +var config; +var logger; +var redisClient; + +var taskCache = LRU({max: 100, maxAge: 1000 * 3600 * 2}); + +function createRedisClient(host, port) { + try { + redisClient = redis.createClient(port, host); + logger.verbose(__filename + ': Redis client for task history created'); + } catch(err) { + logger.warn(__filename + ": Error creating redis client to " + host + ':' + port); + process.exit(1); + } +} + +function init(options) { + if (!options) { + console.log('Undefined options, terminating....'); + process.exit(1); + } + + if (options.config) { + config = options.config; + } else { + console.log('Undefined options.config, terminating....') + process.exit(1); + } + + if (options && options.logger) { + logger = options.logger; + } else { + console.log('Undefined options.logger, terminating....') + process.exit(1); + } + + createRedisClient(config.globals.redis_host, config.globals.redis_port); +} + +function getKey(task, chipInfo) { + var today = moment(task.timestamp, 'YYYYMMDDHHmmss').format('YYYYMMDD'); + return chipInfo + '.antiSameDayDupe.trx.date:' + today + '.rProd:' + task.remoteProduct.toUpperCase() + '.dest:' + task.destination ; +} + +function register(task, cb) { + var key = getKey(task, config.globals.gateway_name); + + taskCache.set(key, task); + saveToRedis(task,cb); +} + +function saveToRedis(task, cb) { + var key = getKey(task, config.globals.gateway_name); + logger.verbose('Saving task', {key: key, task: task}); + + redisClient.set(key, JSON.stringify(task), function() { + redisClient.expire(key, 3600*24); + if (cb) { + cb(); + } + }); +} + +function createDummyTask(remoteProduct, destination) { + return { + remoteProduct: remoteProduct, + destination: destination, + timestamp: moment().format('YYYYMMDD'), + } +} + +function get(remoteProduct, destination, cb) { + var dummyTask = createDummyTask(remoteProduct, destination); + + var key = getKey(dummyTask, config.globals.gateway_name); + var task = taskCache.get(key); + + if (task) { + cb(null, task); + } + else { + getFromRedis(remoteProduct, destination, cb); + } +} + +function getFromRedis(remoteProduct, destination, cb) { + var dummyTask = createDummyTask(remoteProduct, destination); + + var key = getKey(dummyTask, config.globals.gateway_name, moment().format('YYYYMMDD')); + redisClient.get(key, function(err, result) { + if (err) { + logger.warn('antiDupe.get: error getting task from redis', {key: key, params: dummyTask}); + + cb(err, null); + return; + } + + var task = {}; + + try { + task = JSON.parse(result); + } + catch(e) { + logger.warn('antiDupe.get: Can not parse result', {key: key, params: dummyTask, data: result}); + err = "Can not parse task"; + cb(err, null); + return; + } + + cb(err, task); + }); +} + +function check(task, cbNoDupe, cbDupe, cbDupeWithSameReqId) { + if (Number(config.globals.no_same_day_dupe_check)) { + logger.verbose('Skipping same day dupe check because of config.globals.no_same_day_dupe_check'); + cbNoDupe(task); + } + + get(task.remoteProduct, task.destination, function(err, archivedTask) { + if (err) { + logger.warn('Error on checking same day duplicate', {task: task}); + cbNoDupe(task); + return; + } + + if (archivedTask && archivedTask.requestId) { + if (task.requestId == archivedTask.requestId) { + logger.verbose('Duplicate trx on same day with same requestId', {task: task}); + cbDupeWithSameReqId(task); + return; + } + + logger.verbose('Duplicate trx on same day', {task: task, archivedTask: archivedTask}); + cbDupe(task); + return; + } + + register(task, function() { + cbNoDupe(task); + }); + }); +} + +exports.init = init; +exports.check = check; diff --git a/package.json b/package.json index 124e74c..3523aae 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,12 @@ "author": "Adhidarma Hadiwinoto <me@adhisimon.org>", "license": "ISC", "dependencies": { + "lru-cache": "^4.0.1", + "moment": "^2.14.1", + "redis": "^2.6.2", "request": "^2.74.0", "sate24": "git+http://gitlab.kodesumber.com/reload97/node-sate24.git", - "sate24-expresso": "git+http://gitlab.kodesumber.com/reload97/sate24-expresso.git" + "sate24-expresso": "git+http://gitlab.kodesumber.com/reload97/sate24-expresso.git", + "xml2js": "^0.4.17" } } diff --git a/partner-hpay.js b/partner-hpay.js new file mode 100644 index 0000000..7fc9cb8 --- /dev/null +++ b/partner-hpay.js @@ -0,0 +1,223 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +var request = require('request'); +var xml2js = require('xml2js'); + +var resendDelay = require('sate24/resend-delay.js'); +var taskHistory = require('./task-history.js'); +var antiSameDayDupe = require('./anti-same-day-dupe.js'); + +var config; +var aaa; +var _callbackReport; +var logger; + +var xmlBuilder = new xml2js.Builder({rootName: 'xml'}); + +function start(options) { + if (!options) { + console.log('Undefined options, terminating....'); + process.exit(1); + } + + if (options.config) { + config = options.config; + } else { + console.log('Undefined options.config, terminating....') + process.exit(1); + } + + if (options.aaa) { + aaa = options.aaa; + _callbackReport = options.aaa.callbackReportWithPushToMongoDb; + } else { + console.log('Undefined options.aaa, terminating....') + process.exit(1); + } + + if (options && options.logger) { + logger = options.logger; + } else { + console.log('Undefined options.logger, terminating....') + process.exit(1); + } + + resendDelay.init({ + config: config, + topupRequest: _topupStatus, + logger: logger + }); + + taskHistory.init(options); + antiSameDayDupe.init(options); +} + +function callbackReport(requestId, responseCode, msg) { + if (responseCode != '68') { + resendDelay.cancel(requestId); + + } else { + taskHistory.get(requestId, function(err, task) { + if (task) { + resendDelay.register(task); + } + }); + } + + _callbackReport(requestId, responseCode, msg); +} + +function parseResponse(task, response) { + var xmlParser = xml2js.parseString; + + xmlParser(response, function(err, result) { + if (err) { + var msg = 'Error parsing response. ' + err; + logger.warn(msg, {task: task, response: response, err: err}); + callbackReport(task.requestId, '68', msg); + return; + } + + logger.verbose('Response parsed', {result: result, task: task}); + + var rc = '68'; + + var partnerResponseCode; + try { + partnerResponseCode = result.hpay.responseCode[0]; + } catch(e) {} + + var partnerMessage = ''; + try { + partnerMessage = result.hpay.responseMessage[0]; + } catch(e) {} + + if (partnerResponseCode == '00') { + rc = '00'; + } + else if ((partnerResponseCode == '99') && (partnerMessage == 'Member Not Found')) { + rc = '40'; + } + else if (partnerResponseCode == '99') { + rc = '68'; + } + else { + rc = '40'; + } + + var msg = ''; + if (partnerResponseCode) { + msg = partnerResponseCode; + } + + if (partnerMessage) { + msg = msg + ' ' + partnerMessage; + } + msg = msg.trim(); + + callbackReport(task.requestId, rc, msg); + }); +} + +function requestToPartner(methodName, task) { + var payload = createPayload(methodName, task, config.h2h_out.userid, config.h2h_out.noid, config.h2h_out.password); + if (!payload) { + callbackReport(task.requestId, '40', 'ERROR: Undefined payload'); + return; + } + + var requestOpts = { + url: config.h2h_out.partner, + method: "POST", + body: payload, + headers: { + 'Content-Type': 'text/xml', + } + } + + logger.verbose('Requesting to partner', {methodName: methodName, task: task, requestOpts: requestOpts}); + request(requestOpts, function (err, response, responseBody) { + if (err) { + var rc = '68'; + if (methodName === 'pay') { + rc = '40'; + } + + var msg = 'Error requesting pay method. ' + err; + callbackReport(task.requestId, rc, msg); + return; + } + + logger.info('Got direct response from partner', {task: task, responseBody: responseBody}); + parseResponse(task, responseBody); + }); +} + + +function topupRequest(task) { + if (!aaa.isTodayTrx(task)) { + logger.warn('Maaf, transaksi beda hari tidak dapat dilakukan'); + callbackReport(task.requestId, '68', 'Maaf, transaksi beda hari tidak dapat dilakukan'); + resendDelay.cancel(task); + return; + } + + antiSameDayDupe.check(task, _topupRequest, onSameDayDupe, _topupStatus); +} + +function _topupRequest(task) { + taskHistory.put(task, function() { + requestToPartner('pay', task);; + }); +} + +function _topupStatus(task) { + requestToPartner('checking', task); +} + +function onSameDayDupe(task) { + callbackReport(task.requestId, '55', 'Transaksi duplikat dalam satu hari yang sama'); +} + +function extractProductDetail(remoteProduct) { + var product; + + try { + product = remoteProduct.split(','); + } + catch(e) { + logger.warn('extractProductDetail: exception on split'); + return null; + } + + if (product.length !== 3) { + logger.warn('extractProductDetail: product.length <> 3'); + return null; + } + + return {product: product[0], productType: product[1], nominal: product[2]}; +} + +function createPayload(transactionType, task, user, noid, pin) { + var product = extractProductDetail(task.remoteProduct); + if (!product) { + logger.warn('createPayload: undefined product'); + return; + } + + var payload = { + transactionType: transactionType, + product: product.product, + productType: product.productType, + idpel: task.destination, + pin: pin, + noid: noid, + user: user, + nominal: product.nominal + }; + + return xmlBuilder.buildObject(payload); +} + +exports.start = start; +exports.topupRequest = topupRequest; diff --git a/task-history.js b/task-history.js new file mode 100644 index 0000000..12d1185 --- /dev/null +++ b/task-history.js @@ -0,0 +1,131 @@ +var redis = require('redis'); +var LRU = require('lru-cache'); + +var config; +var logger; +var redisClient; + +var taskHistory = LRU({max: 100, maxAge: 1000 * 3600 * 2}); + +function createRedisClient(host, port) { + try { + redisClient = redis.createClient(port, host); + logger.verbose(__filename + ': Redis client for task history created'); + } catch(err) { + logger.warn(__filename + ": Error creating redis client to " + host + ':' + port); + process.exit(1); + } +} + +function init(options) { + if (!options) { + console.log('Undefined options, terminating....'); + process.exit(1); + } + + if (options.config) { + config = options.config; + } else { + console.log('Undefined options.config, terminating....') + process.exit(1); + } + + if (options && options.logger) { + logger = options.logger; + } else { + console.log('Undefined options.logger, terminating....') + process.exit(1); + } + + createRedisClient(config.globals.redis_host, config.globals.redis_port); +} + +function getKey(task) { + var requestId; + + if (typeof task === 'string') { + requestId = task; + } else { + try { + requestId = task.requestId; + } + catch(e) { + return; + } + } + + return config.globals.gateway_name + '.smithsonian.hist.rid:' + requestId; +} + +function put(task, cb) { + var key = getKey(task, config.globals.gateway_name); + logger.verbose('Saving task to history LRU', {key: key, task: task}); + + try { + taskHistory.set(key, JSON.parse(JSON.stringify(task))); + } catch (e) { } + + putToRedis(task, cb); +} + +function putToRedis(task, cb) { + if (!redisClient) { + logger.verbose('Not saving to redis because of undefined redisClient') + if (cb) { cb(); } + return; + } + + var key = getKey(task, config.globals.gateway_name); + logger.verbose('Saving task to redis', {key: key, task: task}); + + redisClient.set(key, JSON.stringify(task), function() { + redisClient.expire(key, 3600*24*30); + if (cb) { + cb(); + } + }); +} + +function get(task, cb) { + logger.verbose('Getting task from history', {task: task}); + var key = getKey(task, config.globals.gateway_name); + var archive = taskHistory.get(key); + + if (archive) { + if (cb) { cb(null, archive); } + } + else { + getFromRedis(task, cb); + } +} + +function getFromRedis(task, cb) { + if (!redisClient) { + if (cb) { cb(null, null); } + return; + } + + var key = getKey(task, config.globals.gateway_name); + redisClient.get(key, function(err, result) { + if (err) { + logger.warn('Error retrieving task from redis', {err: err}); + cb(err, null); + return; + } + + var task; + try { + task = JSON.parse(result); + } + catch(e) { + logger.warn('Exception on parsing redis result as a json', {err: e}); + cb(e, null); + } + + cb(null, task); + }) +} + +exports.init = init; +exports.get = get; +exports.put = put;