partner-mkios.js 10.6 KB
"use strict";

const fs = require('fs');
const moment = require('moment');

const Modem = require('./modem');

const pullgw = require('komodo-sdk/gateway/pull');

const config = require('komodo-sdk/config');
const logger = require('komodo-sdk/logger');
const matrix = require('komodo-sdk/matrix');

const modemDashboard = require('./modem-dashboard');

const chipsJsonFile = process.cwd() + '/chips.json';
const chips = fs.existsSync(chipsJsonFile) ? require(chipsJsonFile) : {};

if (config && config.debug_modem) {
    process.env.KOMODO_DEBUG_MODEM=1;
}

/*
if (!config || !config.partner || !config.partner.pin) {
    logger.warn('Undefined PIN');
    process.exit(1);
}
*/

matrix.modem = {};

matrix.not_ready = true;
matrix.not_ready_ts = null;
matrix.not_ready_ts_readable = null;
matrix.not_ready_max_age_secs = null;

matrix.stock = {};

const db = require('./local-db').getConnection();
const pendingArchive = require('./pending-archive');
const patternMatcher = require('./pattern-rule-matcher');
const smsHandler = require('./sms-handler');

const modem = new Modem(config.partner.modem.dev, {baudRate: 115200});

const resumeHandlers = {};

let last_trx_id = null;

function getPin() {
    return ( config && config.partner && config.partner.pin ) || ( matrix && matrix.modem && matrix.modem.pin_trx );
}

modem.on('open', function() {
    logger.info('Modem opened');

    const ussd_command = '*776*' + getPin() + '#';
    db.run("INSERT INTO ussd VALUES (?, ?, 'OUT', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, 'AT+CUSD=1,"' + ussd_command + '",15', function(err) {
        if (err) {
            logger.warn('Error inserting ussd command (stock check) to local database', {err: err});
        }
    });

    modem.sendUSSD(ussd_command, function() {});
})

modem.on('imsi', function(imsi) {
    logger.verbose('IMSI: ' + imsi);
    matrix.modem.imsi = imsi;
    matrix.modem.msisdn = chips && chips.by_imsi && chips.by_imsi[imsi] && chips.by_imsi[imsi].msisdn ? chips.by_imsi[imsi].msisdn : config.partner.msisdn;
    matrix.modem.pin_trx = chips && chips.by_imsi && chips.by_imsi[imsi] && chips.by_imsi[imsi].pin ? chips.by_imsi[imsi].pin : null;
})


function onIncomingSMS(sms) {
    logger.info('Incoming SMS', {sms: sms});
    db.run("INSERT INTO sms VALUES (?, ?, 'IN', ?, ?, ?)", sms.created, moment(sms.created).format('YYYY-MM-DD'), matrix.modem.imsi, sms.sender, sms.msg, function(err) {
        if (err) {
            logger.warn('Error inserting sms to local database', {err: err});
        }
    });

    if (!smsHandler.isAllowedSender(sms.sender)) {
        logger.verbose('Ignoring SMS from unknown sender', {sender: sms.sender});
        return;
    }

    const stocks = smsHandler.getMultiStockBalance(sms.msg);
    if (stocks && Array.isArray(stocks) && stocks.length) {
        stocks.forEach(function(stock) {
            const vals = stock.split('=');
            updateStock(vals[0], vals[1]);
        })
    }
    else {
        const stock = smsHandler.getStockBalance(sms.msg);
        if (stock.name && stock.balance) {
            updateStock(stock.name, stock.balance);
        }
    }

    const destination = smsHandler.getDestination(sms.msg);
    if (!destination) {
        logger.verbose('Ignoring SMS with unknown trx destination');
        return;
    }

    const product = smsHandler.getProduct(sms.msg);
    if (!product) {
        logger.verbose('Ignoring SMS with unknown trx product');
        return;
    }

    const trx_date = smsHandler.getTrxDate(sms.msg);
    if (!trx_date) {
        logger.verbose('Ignoring SMS with unknown trx date');
        return;
    }

    logger.verbose('SMS message parsed and extracted', {destination: destination, product: product, trx_date: trx_date});
    pendingArchive.get(destination, product, trx_date, function(err, trx_id) {
        if (!trx_id) {
            logger.verbose('No pending trx suits with SMS', {destination: destination, product: product, trx_date: trx_date});
            return;
        }

        deleteResumeHandler(trx_id);
        pendingArchive.remove(trx_id);

        report({
            trx_id: trx_id,
            rc: smsHandler.getRc(sms.msg) || '68',
            sn: smsHandler.getSn(sms.msg),
            message: 'SMS: ' + sms.msg
        })
    })

}
modem.on('incoming sms', onIncomingSMS);

modem.on('signal strength', function(signal_strength) {
    matrix.modem.signal_strength = signal_strength;
    logger.verbose('Signal strength: ' + signal_strength);
})

function onUSSDResponse(data) {
    logger.verbose('Got USSD response', {response: data});

    db.run("INSERT INTO ussd VALUES (?, ?, 'IN', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, data, function(err) {
        if (err) {
            logger.warn('Error inserting ussd response to local database', {err: err});
        }
    });


    if (!last_trx_id) return;


    const rc = getRcFromMessage(data, config.ussd_parser.rc) || '68';
    if (rc !== '68') {
        onTrxFinish(last_trx_id);
    }

    deleteResumeHandler(last_trx_id);;
    unsuspendPull();

    report({
        trx_id: last_trx_id,
        rc: rc,
        sn: getSnFromMessage(data),
        message: data
    });

    const stock_name = getStockProductFromMessage(data);
    const stock_balance = getStockBalanceFromMesssage(data);

    updateStock(stock_name, stock_balance);
    last_trx_id = null;
}

modem.on('ussd response', onUSSDResponse);


logger.info('Opening MODEM');
modem.open(function(err) {
    if (err) {
        logger.warn('Error opening modem port', {err: err});
        process.exit(1);
    }

    logger.info('Modem open successfully, going to ready in 30 secs');
    setTimeout( unsuspendPull, 30 * 1000 )

})

function updateStock(stock_name, stock_balance) {
    if (stock_name && (stock_balance !== undefined || stock_balance !== null)) {
        logger.verbose('Updating stock', {stock_name: stock_name, stock_balance: stock_balance});

        const new_stock_name = config && config.remote_product_alias && config.remote_product_alias[stock_name] ? config.remote_product_alias[stock_name] : stock_name;

        matrix.stock[new_stock_name] = Number(stock_balance);

        logger.verbose('Stock balance updated', {stock: matrix.stock});
    }
}

function getRcFromMessage(msg, rules) {
    if (!rules || !Array.isArray(rules)) {
        return '68';
    }

    for (let rule of rules) {
        if (!rule.pattern) return '68';

        const re = new RegExp(rule.pattern);
        if (msg.search(re) >= 0) {
            return rule.rc ? rule.rc : '68';
        }
    }

    return '68';
}

function getPatternMatchFromMessage(msg, rules) {
    if (!rules || !Array.isArray(rules)) {
        return;
    }

    for (let rule of rules) {
        if (!rule.pattern) return;

        const re = new RegExp(rule.pattern);
        const matches = msg.match(re);

        if (matches && matches.length >= 2) {
            return matches[1];
        }
    }
}

function getSnFromMessage(msg, rules) {
    return patternMatcher(msg, config.ussd_parser.sn);
}

function getStockProductFromMessage(msg, rules) {
    return patternMatcher(msg, config.ussd_parser.stock.product);
}

function getStockBalanceFromMesssage(msg, rules) {
    return patternMatcher(msg, config.ussd_parser.stock.balance);
}

function suspendPull(trx_id) {
    logger.verbose('Set modem to not ready so no other task can be entered and registering delayed resume');
    matrix.not_ready = true;
    matrix.not_ready_ts = new Date();
    matrix.not_ready_ts_readable = moment(matrix.not_ready_ts).format('YYYY-MM-DD HH:mm:ss');

    resumeHandlers['trx' + trx_id] = setTimeout(function() {
        logger.verbose('Resuming supsended modem', {trx_id: trx_id});
        unsuspendPull();
        report({
            trx_id: trx_id,
            rc: '68',
            message: 'USSD timeout'
        })
    }, Number(config.partner.modem.suspend_time_ms) || 20 * 1000 );
}

function unsuspendPull() {
    matrix.not_ready = false;

    if (matrix.not_ready_ts) {
        matrix.not_ready_max_age_secs = Math.max( (new Date() - matrix.not_ready_ts) / 1000, matrix.not_ready_max_age_secs );
    }

    logger.verbose('Modem is ready');
}

function deleteResumeHandler(trx_id) {
    if (resumeHandlers['trx' + trx_id]) {
        logger.verbose('Unregistering delayed resume of trx id ' + trx_id);
        clearTimeout(resumeHandlers['trx' + trx_id]);
        delete resumeHandlers['trx' + trx_id];
    }
}

function onTrxFinish(trx_id) {
    deleteResumeHandler(trx_id);
    pendingArchive.remove(trx_id);
    unsuspendPull();
}

function buy(task) {

    if (!getPin()) {
        report({
            trx_id: task.trx_id,
            rc: '40',
            message: 'INTERNAL: Tidak dapat melakukan transaksi. PIN transaksi tidak terdefinisi.'
        });
        return;
    }

    if (task.product === task.remote_product) {
        report({
            trx_id: task.trx_id,
            rc: '40',
            message: 'INTERNAL: Tidak dapat melakukan transaksi. Kode USSD belum terdefinisi.'
        });
        return;
    }

    suspendPull(task.trx_id);
    last_trx_id = task.trx_id;

    pendingArchive.put(task, function(err) {
        if (err) {
            logger.verbose('Error inserting task to pending archive', {trx_id: task.trx_id, destination: task.destination, product: task.product, err: err});
            onTrxFinish(task.trx_id);
            report({
                trx_id: task.trx_id,
                rc: '40',
                message: 'INTERNAL: Gagal melakukan transaksi. Mungkin ada transaksi dengan nomor dengan produk yang sama yang masih diproses. Silahkan ulangi beberapa saat lagi.'
            });

            return;
        }

        const ussd_command = task.remote_product.split(',')[0].replace("<DESTINATION>", task.destination).replace("<PIN>", getPin());
        logger.verbose('Going to execute USSD', {trx_id: task.trx_id, destination: task.destination, product: task.product, ussd: ussd_command});

        db.run("INSERT INTO ussd VALUES (?, ?, 'OUT', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, 'AT+CUSD=1,"' + ussd_command + '",15', function(err) {
            if (err) {
                logger.warn('Error inserting ussd command to local database', {err: err});
            }
        });
        modem.sendUSSD(ussd_command, function() {});
    })
}

function report(data) {
    if (data.message) {
        if (matrix.modem.imsi) {
            data.message = 'CHIP-IMSI: ' + matrix.modem.imsi + '; ' + data.message;
        }

        if (matrix.modem.msisdn) {
            data.message = 'CHIP-MSISDN: ' + matrix.modem.msisdn + '; ' + data.message;
        }
    }
    pullgw.report(data);
}

exports.buy = buy;