diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e20c3ce
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+config.ini
+log.txt
+logs/log*
diff --git a/aaa.js b/aaa.js
new file mode 100644
index 0000000..752bfe3
--- /dev/null
+++ b/aaa.js
@@ -0,0 +1,159 @@
+var request = require('request');
+var strftime = require('strftime');
+
+var max_retry = 10;
+var sleep_before_retry = 3000;
+
+var config;
+var partner;
+
+var available_products = [];
+
+function unaliasResponseCode(response_code, config_responsecode_alias) {
+    if (config_responsecode_alias == undefined && config && config.h2h_out && config.h2h_out.responsecode_alias) {
+        config_responsecode_alias = config.h2h_out.responsecode_alias;
+    }
+    
+    if (!config_responsecode_alias) {
+        return response_code;
+    }
+    
+    items = config_responsecode_alias.split(',');
+    items_count = items.length;
+    
+    for (var i=0; i < items_count; i++) {
+        codes = items[i].split(':');
+        
+        if (codes.length <= 0) { continue; }
+        
+        if (response_code == codes[0]) {
+            console.log('Change response code from ' + codes[0] + ' to ' + codes[1]);
+            return codes[1];
+        }
+    }
+    
+    return response_code;
+}
+
+function pullCity() {
+    var url = config.globals.aaa_host + '/pull_city';
+    console.log('Pull cities from AAA - ' + url);
+    request(url, function (error, response, body) {
+        if (!error && response.statusCode == 200) {
+            //console.log('city=' + body);
+        } else {
+            console.log('Error in pull city');
+        }
+    });
+}
+
+function pullProduct() {
+    var url = config.globals.aaa_host + '/pull_product?opr_name=' + config.globals.operators;
+    console.log('Pull products from AAA - ' + url);
+
+    request(url, function (error, response, body) {
+        if (error || response.statusCode != 200) {
+            console.log('Error in pull products');
+            return;
+        }
+
+        var productsAndOperators = body.split(';');
+        var productsCount = productsAndOperators.length;
+
+        for (var i=0; i < productsCount; i++) {
+            var product = productsAndOperators[i].split(',', 1)[0];
+            available_products.push(product);
+        }
+        //console.log(available_products);
+    });
+}
+
+function pull() {
+    var url = config.globals.aaa_host
+        + '/pull?city=ALL&nom=' + config.globals.products
+        + '&chip_info=' + config.globals.gateway_name;
+
+    //console.log('AAA PULL - ' + url);
+    request(url, function (error, response, body) {
+        if (!error && response.statusCode == 200) {
+            if (body == 'NONE') {
+                return;
+            }
+            console.log(body);
+
+            var result = body.split(';');
+            if (result[0] != 'OK') {
+                return;
+            }
+
+            var task = [];
+            task['requestId'] = result[1];
+            task['timestamp'] = result[3];
+            task['destination'] = result[4];
+            task['product'] = result[7];
+
+            if (config.products[task['product']] !== undefined) {
+                task['remoteProduct'] = config.products[task['product']];
+            } else {
+                task['remoteProduct'] = task['product'];
+            }
+
+            partner.topupRequest(task);
+
+        } else {
+            console.log('Error in pull task');
+            return;
+        }
+    });
+}
+
+function pullLoop() {
+    if (!config.globals.pause) {
+        pull();
+    }
+    
+    setTimeout(pullLoop, config.globals.interval);
+}
+
+function callbackReport(requestId, responseCode, message, retry) {
+    if (retry === undefined) {
+        retry = max_retry;
+    }
+    
+    responseCode = unaliasResponseCode(responseCode);
+    
+    timestamp = strftime('%Y%m%d%H%M%S');
+    var url = config.globals.aaa_host
+        + '/topup?trans_id=' + requestId
+        + '&trans_date' + timestamp
+        + '&trans_date=' + timestamp
+        + '&resp_code=' + responseCode
+        + '&ussd_msg=' + config.globals.gateway_name
+        + '$' + encodeURIComponent(message);
+
+    console.log('Report to AAA - ' + url);
+    request(url, function (error, response, body) {
+        if (error || response.statusCode != 200) {
+            console.log('Error report to AAA');
+            
+            if (retry) {
+                console.log('Retrying to report to AAA (' + retry + ')');
+                callbackReport(requestId, responseCode, message, retry - 1);
+            }
+        }
+    });
+}
+
+function start(_config, _partner) {
+    config = _config;
+    partner = _partner;
+
+    pullCity();
+    pullProduct();
+
+    setTimeout(pullLoop, 10 * 1000);
+}
+
+exports.start = start;
+exports.callbackReport = callbackReport;
+exports.unaliasResponseCode = unaliasResponseCode;
diff --git a/config.sample.ini b/config.sample.ini
new file mode 100644
index 0000000..4fad175
--- /dev/null
+++ b/config.sample.ini
@@ -0,0 +1,18 @@
+[globals]
+operators=TEST,XL_ALTERNATIF
+products=TST1
+gateway_name=NODEJS-DEV
+aaa_host=http://172.23.0.12:4250
+interval=1000
+admin_port=17456
+
+[h2h_out]
+partner=https://172.23.0.12:6789
+userid=R97DEV
+password=czcb
+listen_port=13522
+check_interval=2000
+
+[products]
+TST1=TR1
+
diff --git a/httpserver.js b/httpserver.js
new file mode 100644
index 0000000..d90ecc6
--- /dev/null
+++ b/httpserver.js
@@ -0,0 +1,90 @@
+var http = require('http');
+var nsr = require('node-simple-router');
+var router = nsr();
+
+var config;
+var httpServer;
+
+function start(_config) {
+    if (_config != undefined) {
+        config = _config;
+    }
+    listenPort = config.globals.admin_port;
+    
+    router.get("/info", function(request, response) {
+        response.setHeader("Content-Type", "text/plain");
+        response.write('CHIPINFO / GATEWAY NAME: ' + config.globals.gateway_name + "\n");
+        response.write('PRODUCTS: ' + config.globals.products + "\n");
+        response.write('AAA HOST: ' + config.globals.aaa_host + "\n");
+        response.write('PARTNER: ' + config.h2h_out.partner + "\n");
+        response.write('PAUSED: ' + config.globals.pause + "\n");
+        response.write('UPTIME: ' + process.uptime() + "\n");
+        response.write('REQUESTS COUNT: ' + config.globals.requests_count + "\n");
+        response.write('ACTIVE REQUESTS COUNT: ' + config.globals.active_requests_count + "\n");
+        response.write('MAX ACTIVE REQUESTS COUNT: ' + config.globals.max_active_requests_count + "\n");
+        
+        response.end();
+    });
+    
+    router.get("/pause/:apikey", function(request, response) {
+        if (!config.globals.apikey) {
+            response.end('Undefined APIKEY on config');
+            return;
+        }
+        
+        if (request.params.apikey != config.globals.apikey) {
+            response.end('Invalid APIKEY');
+            return;
+        }
+        
+        config.globals.pause = 1;
+        response.end('Paused');
+    });
+    
+    router.get("/resume/:apikey", function(request, response) {
+        if (!config.globals.apikey) {
+            response.end('Undefined APIKEY on config');
+            return;
+        }
+        
+        if (request.params.apikey != config.globals.apikey) {
+            response.end('Invalid APIKEY');
+            return;
+        }
+        
+        delete config.globals.pause;
+        response.end('Resume');
+    });
+    
+    router.get("/reset-stats/:apikey", function(request, response) {
+        if (!config.globals.apikey) {
+            response.end('Undefined APIKEY on config');
+            return;
+        }
+        
+        if (request.params.apikey != config.globals.apikey) {
+            response.end('Invalid APIKEY');
+            return;
+        }
+        
+        config.globals.max_active_requests_count = 0;
+        
+        response.writeHead(307, {
+          'Location': '/info'
+        });
+        
+        response.end();
+    });
+
+    httpServer = http.createServer(router).listen(listenPort);
+    console.log('HTTP server listens on port ' + listenPort);
+
+    return httpServer;
+}
+
+function setConfig(_config) {
+    config = _config;
+}
+
+exports.start = start;
+exports.setConfig = setConfig;
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..70985ec
--- /dev/null
+++ b/index.js
@@ -0,0 +1,13 @@
+var iniparser = require('iniparser');
+var config = iniparser.parseSync('./config.ini');
+
+var aaaHost = config.globals.aaa_host;
+
+HttpServer = require('./httpserver.js');
+var httpServer = HttpServer.start(config);
+
+var aaa = require('./aaa.js');
+var xmlout = require('./xmlout.js');
+
+xmlout.start(config, aaa.callbackReport);
+aaa.start(config, xmlout);
diff --git a/logs/empty b/logs/empty
new file mode 100644
index 0000000..e69de29
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1f5e943
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "sate24-to-sate24",
+  "version": "0.0.1",
+  "description": "ST24 to ST24 H2H OUT",
+  "main": "index.js",
+  "scripts": {
+    "test": "mocha"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git@gitlab.kodesumber.com:reload97/sate24-to-sate24.git"
+  },
+  "keywords": [
+    "st24",
+    "reload97",
+    "ppob",
+    "h2h",
+    "m2m",
+    "xmlrpc"
+  ],
+  "author": "Adhidarma Hadiwinoto <gua@adhisimon.org>",
+  "license": "BSD",
+  "dependencies": {
+    "mocha": "~2.2.5",
+    "request": "~2.57.0",
+    "strftime": "~0.9.2",
+    "iniparser": "~1.0.5",
+    "mathjs": "~1.7.0",
+    "xmlrpc": "~1.3.1"
+  }
+}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..7510e8e
--- /dev/null
+++ b/test.js
@@ -0,0 +1,33 @@
+var assert = require("assert");
+
+
+describe('aaa', function() {
+    var aaa = require('./aaa');
+    
+    describe("#unaliasResponseCode()", function() {
+        it('should return 68', function() {
+            assert.equal('68', aaa.unaliasResponseCode('01', '01:68'));
+        });
+        
+        it('should return 68', function() {
+            assert.equal('68', aaa.unaliasResponseCode('68', '01:68'));
+        });
+        
+        it('should return 00', function() {
+            assert.equal('00', aaa.unaliasResponseCode('00', '01:68'));
+        });
+        
+        it('should return 40', function() {
+            assert.equal('40', aaa.unaliasResponseCode('40', ''));
+        });
+        
+        it('should return 40', function() {
+            assert.equal('40', aaa.unaliasResponseCode('40', ''));
+        });
+        
+        it('should return 40', function() {
+            assert.equal('40', aaa.unaliasResponseCode('40'));
+        });
+    
+    });
+});
diff --git a/xmlout.js b/xmlout.js
new file mode 100644
index 0000000..4534183
--- /dev/null
+++ b/xmlout.js
@@ -0,0 +1,222 @@
+var xmlrpc = require('xmlrpc');
+var url = require('url');
+var math = require('mathjs');
+
+var config;
+var callbackReport;
+
+var max_retry = 2;
+var sleep_before_retry = 2000;
+
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+
+function topupRequest(task, retry) {
+    if (config.globals.requests_count == undefined) {
+        config.globals.requests_count = 1;
+    } else {
+        config.globals.requests_count++;
+    }
+    
+    if (config.globals.active_requests_count == undefined) {
+        config.globals.active_requests_count = 1;
+    } else {
+        config.globals.active_requests_count++;
+    }
+    
+    if (config.globals.max_active_requests_count == undefined) {
+        config.globals.max_active_requests_count = config.globals.active_requests_count;
+    } else {
+        config.globals.max_active_requests_count = math.max(config.globals.max_active_requests_count, config.globals.active_requests_count);
+    }
+    
+    
+    if (retry === undefined) {
+        retry = max_retry;
+    }
+
+    var partnerUrl = url.parse(config.h2h_out.partner);
+    var clientOptions = {
+        host: partnerUrl.hostname
+        , port: partnerUrl.port
+        , path: partnerUrl.pathname
+    };
+    console.log('XMLRPC client options:');
+    console.log(clientOptions);
+
+    var client;
+    if (partnerUrl.protocol == 'https:') {
+        console.log('Use SSL');
+        client = xmlrpc.createSecureClient(clientOptions);
+    } else {
+        console.log('Not using SSL');
+        client = xmlrpc.createClient(clientOptions);
+    }
+
+    var methodName = 'topUpRequest';
+    console.log('methodName: ' + methodName);
+
+    var params = {
+        MSISDN: config.h2h_out.userid,
+        REQUESTID: task['requestId'],
+        PIN: config.h2h_out.password,
+        NOHP: task['destination'],
+        NOM: task['remoteProduct']
+    };
+    console.log('PARAMS:');
+    console.log(params);
+
+
+    client.methodCall(methodName, [ params ], function (error, value) {
+        
+        if (config.globals.active_requests_count == undefined) {
+            config.globals.active_requests_count = 0;
+        } else {
+            config.globals.active_requests_count--;
+        }
+        
+        // Results of the method response
+        if (error) {
+            console.log('XMLRPC Client Error (' + task['requestId'] + '): ');
+            console.log(error);
+            
+            if (retry) {
+                
+                console.log('Retrying topUpRequest (' + retry + ')');
+                setTimeout(function() {
+                    topupRequest(task, retry - 1);
+                }, sleep_before_retry);
+                
+            } else {
+                callbackReport(task['requestId'], '40', 'Gangguan koneksi ke suplier');
+            }
+            return;
+        }
+        
+        console.log('Method response for \'' + methodName + '\': ')
+        console.log(value);
+        
+        if (value['RESPONSECODE'] == '00' && config.h2h_out.parse_sn == 'YES') {
+            value['MESSAGE'] = 'SN=' + parseSN(value['MESSAGE']) + '; ' + value['MESSAGE'];
+            //console.log('Message with SN: ' + value['MESSAGE']);
+        }
+        
+        callbackReport(value['REQUESTID'], value['RESPONSECODE'], value['MESSAGE']);
+    });
+}
+
+function createServer() {
+
+    console.log('Creating XML-RPC server on port ' + config.h2h_out.listen_port);
+    var serverOptions = {
+        port: config.h2h_out.listen_port
+    };
+
+    var server = xmlrpc.createServer(serverOptions);
+
+    server.on('NotFound', function (method, params) {
+        console.log(method + ' is not found on our XML-RPC server');
+        console.log('params:');
+        console.log(params);
+    });
+
+    server.on('topUpReport', function (err, params, callback) {
+        console.log('RECEIVE topUpReport, params:');
+        console.log(params);
+        
+        var paramscount = params.length;
+        for (var i = 0; i < paramscount; i++) {
+            var value = params[i];
+            
+            if (value['RESPONSECODE'] == '00' && config.h2h_out.parse_sn == 'YES') {
+                value['MESSAGE'] = 'SN=' + parseSN(value['MESSAGE']) + '; ' + value['MESSAGE'];
+                //console.log('Message with SN: ' + value['MESSAGE']);
+            }
+                        
+            callbackReport(value['REQUESTID'], value['RESPONSECODE'], value['MESSAGE']);
+        }
+
+        callback(null, 'ACK REPORT OK');
+    })
+
+}
+
+function checkStatus(task) {
+    var partnerUrl = url.parse(config.h2h_out.partner);
+    var clientOptions = {
+        host: partnerUrl.hostname
+        , port: partnerUrl.port
+        , path: partnerUrl.pathname
+    };
+    console.log('XMLRPC client options:');
+    console.log(clientOptions);
+
+    var client;
+    if (partnerUrl.protocol == 'https:') {
+        console.log('Use SSL');
+        client = xmlrpc.createSecureClient(clientOptions);
+    } else {
+        console.log('Not using SSL');
+        client = xmlrpc.createClient(clientOptions);
+    }
+
+    var methodName = 'topUpInquiry';
+    console.log('methodName: ' + methodName);
+
+    var params = {
+        MSISDN: config.h2h_out.userid,
+        REQUESTID: task['requestId'],
+        PIN: config.h2h_out.password,
+        NOHP: task['destination']
+    };
+    console.log('PARAMS:');
+    console.log(params);
+
+    client.methodCall(methodName, [ params ], function (error, value) {
+        // Results of the method response
+        if (error) {
+            console.log('Error: ');
+            console.log(error);
+            return;
+        }
+        console.log('Method response for \'' + methodName + '\': ')
+        console.log(value);
+
+        callbackReport(value['REQUESTID'], value['RESPONSECODE'], value['MESSAGE']);
+    });
+}
+
+function start(_config, _callbackReport) {
+    config = _config;
+    callbackReport = _callbackReport
+
+    createServer();
+}
+
+function parseSN(message) {
+    var sn_regex = new RegExp(config.h2h_out.sn_pattern);
+    var sn_match = message.match(sn_regex);
+    
+    //console.log('SN MATCH:');
+    //console.log(sn_match);
+    
+    if (sn_match <= 0) {
+        console.log('SN Not found: ' + message);
+        return '';
+    }
+    
+    var sn = sn_match[0];
+    var sn_remove_patterns = config.h2h_out.sn_remove_patterns.split(config.h2h_out.sn_remove_patterns_separator);
+    //console.log('SN REMOVE PATTERNS:');
+    //console.log (sn_remove_patterns);
+    
+    var count = sn_remove_patterns.length;
+    
+    for(var i = 0; i < count; i++) {
+        sn = sn.replace(sn_remove_patterns[i], '');
+    }
+    
+    return sn.trim();
+}
+
+exports.start = start;
+exports.topupRequest = topupRequest;