partner-simplepay.js 8.86 KB
"use strict";

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

const request = require('request');
const crypto = require('crypto');
const moment = require('moment');

const http = require('http');
http.globalAgent.maxSockets = Infinity;

const seconds_to_wait_before_resend_on_pending = 30;

var config;
var aaa;
var logger;

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;
    } 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);
    }

    createReverseHttpServer();
}

function callbackReport(requestId, rc, message, options) {
    aaa.callbackReportWithPushToMongoDb(requestId, rc, message);

    // resend trx as status check if rc 68 and task is defined
    if (options && options.task && (rc == '68')) {
        logger.verbose('Got pending trx, requesting in ' + seconds_to_wait_before_resend_on_pending + ' secs');
        setTimeout(function() {
            checkStatus(options.task);
        }, seconds_to_wait_before_resend_on_pending * 1000)
    }
}

function calculateSign(dt, trx_ref_id, cust_num, username, password) {
    const cryptoHashPassword = crypto.createHash('sha1');
    const cryptoHashUsernamePassword = crypto.createHash('sha1');
    const cryptoHashOutest = crypto.createHash('sha1');

    cryptoHashPassword.update(password);
    const hashPassword = cryptoHashPassword.digest('hex');

    cryptoHashUsernamePassword.update(username + hashPassword);
    const hashUsernamePassword = cryptoHashUsernamePassword.digest('hex');

    cryptoHashOutest.update(dt + trx_ref_id + cust_num + hashUsernamePassword);
    return cryptoHashOutest.digest('hex');
}

function _decodeResponseBody(responseBody) {
    let response;

    try {
        response = JSON.parse(responseBody)
    }
    catch(e) {
        logger.warn('Error parsing response body');
    }

    return response;
}

function _composeMessageFromResponseData(responseDataObj) {
    const diag = _getPropertyFromObjectSafe(responseDataObj, 'diag');
    const msg = _getPropertyFromObjectSafe(responseDataObj, 'message');
    const balance = _getPropertyFromObjectSafe(responseDataObj, 'balance');
    const timestamp = _getPropertyFromObjectSafe(responseDataObj, 'timestamp');

    let messages = [];

    if (timestamp) {
        messages.push(timestamp);
    }

    if (diag) {
        messages.push(diag);
    }

    if (msg) {
        messages.push(msg);
    }

    if (balance) {
        messages.push('Balance: ' + balance);
    }

    return messages.join('. ') + '.';
}

function _composeCompleteSn(responseDataObj) {
    const serial = _getPropertyFromObjectSafe(responseDataObj, 'serial');
    const info = _getPropertyFromObjectSafe(responseDataObj, 'info');

    if (!info) {
        //logger.warn('Undefined data.info on _composeCompleteSn');
        return serial;
    }

    const cleanedData = {
        token: serial,
        cust_name: _getPropertyFromObjectSafe(info, 'cust_name'),
        tariff: _getPropertyFromObjectSafe(info, 'kelas') + 'VA',
        total_kwh: _getPropertyFromObjectSafe(info, 'size')
    }

    if (cleanedData.cust_name) {
        cleanedData.cust_name = cleanedData.cust_name.replace(/\W+/g, ' ').trim().replace(/\W+/g, '-').toUpperCase();
    }
    logger.verbose('Detail token info extracted', {originalResponseInfo: info, cleanedData: cleanedData});

    return [
        cleanedData.token, cleanedData.cust_name, cleanedData.tariff, cleanedData.total_kwh
    ].join('/');
}

function _responseBodyHandler(responseBody, task) {
    let rc = '68';
    let response = _decodeResponseBody(responseBody);

    logger.verbose('RESPONSE', {response: response});

    const responseStatus = _getPropertyFromObjectSafe(response, status);
    const responseInfo = _getPropertyFromObjectSafe(response, info);

    if (responseStatus == 'Error') {
        if (responseInfo == 'insufficient balance') {
            rc = '91';
        }
        callbackReport(task.requestId, '91', [responeStatus, responseInfo].join(': '), {task: task});
        return;
    }

    const requestId = _getPropertyFromObjectSafe(response.data, 'request_id');
    const trxStatus = _getPropertyFromObjectSafe(response.data, 'trx_status');
    const diag = _getPropertyFromObjectSafe(response.data, 'diag');
    let balance = _getPropertyFromObjectSafe(response.data, 'balance');

    if (balance && aaa.updateBalance) {
        balance = balance.replace(/\D/g, '');
        if (balance) {
            aaa.updateBalance(balance);
        }
    }

    let aaaMessage = _composeMessageFromResponseData(response.data);
    if (!aaaMessage) {
        aaaMessage = 'Transaksi sedang diproses';
    }

    if (trxStatus == 'P') {
        logger.verbose('Got pending trx response', {response: response.data});
        rc = '68';
    }
    else if (trxStatus == 'S') {
        logger.verbose('Got succcess trx response', {response: response.data});

        rc = '00';
        aaaMessage = 'SN=' + _composeCompleteSn(response.data) + '; ' + aaaMessage;
    }
    else if (trxStatus == 'R') {
        logger.verbose('Got rejected trx response', {response: response.data});

        const partnerRC = getPartnerRCFromDiagMessage(diag);
        if (partnerRC == '15') {
            rc = '14';
        }
        else {
            rc = '40';
        }
    }

    callbackReport(requestId, rc, aaaMessage, {task: task});
}

function getPartnerRCFromDiagMessage(diag) {
    let matches = diag.match(/^\s*\[(.*)\]/);
    if (matches.length < 2) {
        return;
    }

    return matches[1];
}

function _hitTopup(task, pendingOnErrorConnect) {

    const dt = moment().format('YYYY-MM-DD HH:mm:ss');
    const username = config.h2h_out.username || config.h2h_out.userid;
    const password = config.h2h_out.password || config.h2h_out.pin;
    const sign = calculateSign(dt, task.requestId, task.destination, username, password);

    logger.verbose('Sign for ' + dt + ', ' + task.requestId + ', ' + task.destination + ', ' + username + ', ' + password + ' is ' + sign);
    const requestOptions = {
        url: config.h2h_out.partner,
        form: {
            username: username,
            datetime: dt,
            code: task.remoteProduct,
            trx_ref_id: task.requestId,
            cust_num: task.destination,
            sign: sign
        }
    }

    logger.verbose('Requeting to partner', {requestOptions: requestOptions});

    request.post(requestOptions, function(error, response, body) {
        if (error) {
            let rc = '68';

            if (!pendingOnErrorConnect && (error.syscall == 'connect')) {
                rc = '91';
            }

            logger.warn('Error requesting to partner', {task: task, rc: rc, error: error});
            callbackReport(task.requestId, rc, 'Error requesting to partner. ' + error, {task: task});
            return;
        }

        if (response.statusCode != 200) {
            let rc = '68';

            logger.warn('HTTP status code is not 200', {task: task, http_status_code: response.statusCode});
            callbackReport(task.requestId, rc, 'HTTP status code ' + response.statusCode, {task: task});
            return;
        }

        logger.info('Transaksi sedang diproses', {task: task, response_body: body});

        _responseBodyHandler(body, task);

    })
}

function _getPropertyFromObjectSafe(obj, property) {
    let retval;

    if (!obj) {
        logger.warn('Invalid object')
        return;
    }

    try {
        retval = obj[property];
    }
    catch(e) {
        logger.warn('Error getting ' + property + ' from object');
    }

    return retval;
}

function topupRequest(task) {
    aaa.insertTaskToMongoDb(task);
    _hitTopup(task);
}

function checkStatus(task) {
    _hitTopup(task, true);
}


function reverseReportHandler(body) {
    logger.info('Got reverse report', {body: body});
}

function createReverseHttpServer() {
    var httpServer = http.createServer(function(request, response) {

        logger.info('Got request from partner');

        var body = "";
        request.on('data', function (chunk) {
            body += chunk;
        });

        request.on('end', function () {
            response.writeHead(200);
            response.end('OK');

            reverseReportHandler(body);
        });

    });

    httpServer.listen(config.h2h_out.listen_port, function() {
        logger.info('HTTP Reverse/Report server listen on port ' + config.h2h_out.listen_port);
    });
}


exports.calculateSign = calculateSign;
exports.start = start;
exports.topupRequest = topupRequest;
exports.checkStatus = checkStatus;