diff --git a/.gitignore b/.gitignore
index c2658d7..eaf7deb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
 node_modules/
+logs/
+config.ini
diff --git a/package.json b/package.json
index 8a0200c..7c9a61f 100644
--- a/package.json
+++ b/package.json
@@ -23,5 +23,9 @@
     "request": "^2.75.0",
     "sate24": "git+http://gitlab.kodesumber.com/reload97/node-sate24.git",
     "sate24-expresso": "git+http://gitlab.kodesumber.com/reload97/sate24-expresso.git"
+  },
+  "devDependencies": {
+    "mocha": "^3.1.0",
+    "should": "^11.1.0"
   }
 }
diff --git a/partner-otomax.js b/partner-otomax.js
new file mode 100644
index 0000000..b7d23b7
--- /dev/null
+++ b/partner-otomax.js
@@ -0,0 +1,151 @@
+"use strict";
+
+var request = require('request');
+var crypto = require('crypto');
+
+var taskHistory = require('sate24/task-history');
+var antiSameDayDupe = require('sate24/anti-same-day-dupe');
+
+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);
+    }
+
+    resendDelay.init({
+        config: config,
+        topupRequest: topupRequest,
+        logger: logger
+    });
+
+    taskHistory.init(options);
+    antiSameDayDupe.init(options);
+}
+
+function callbackReport(requestId, rc, message) {
+    if (responseCode != '68' || dontResendDelay) {
+        resendDelay.cancel(requestId);
+    } else {
+        taskHistory.get(requestId, function(err, archivedTask) {
+            if (archivedTask) {
+                logger.verbose('DEBUG', {archivedTask: archivedTask});
+                resendDelay.register(archivedTask);
+            }
+        });
+    }
+
+    options.aaa.callbackReportWithPushToMongoDb(requestId, rc, message);
+}
+
+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;
+    }
+
+    aaa.insertTaskToMongoDb(task);
+    antiSameDayDupe.check(task, _topupRequest, onSameDayDupe, _topupRequest);
+}
+
+function _topupRequest(task) {
+    taskHistory.put(task, function() {
+        requestToPartner(task);;
+    });
+}
+
+function requestToPartner(task) {
+    let requestOptions = createRequestOptions(task);
+
+    request(requestOptions, function(error, response, body) {
+        if (error) {
+            logger.warn('Error requesting to partner', {task: task, error: error});
+            callbackReport(task.requestId, '68', 'Error requesting to partner. ' + error);
+            return;
+        }
+
+        if (response.statusCode != 200) {
+            logger.warn('HTTP status code is not 200', {task: task, http_status_code: response.statusCode});
+            callbackReport(task.requestId, '40', 'HTTP status code ' + response.statusCode);
+            return;
+        }
+
+        parseMessage(task, message);
+    });
+}
+
+function parseMessage(task, message) {
+    let rc = '68';
+
+    if (message.indexOf('SUKSES') >= 0) {
+        rc = '00';
+    }
+    else if (message.indexOf('GAGAL') >= 0) {
+        rc = '40';
+    }
+
+    callbackReport(task.requestId, rc, message);
+}
+
+function generateSign(userid, remoteProduct, destination, requestId, pin, password) {
+    let plain = ["OtomaX", userid, remoteProduct, destination, requestId, pin, password].join("|");
+    let sha1 = crypto.createHash('sha1').update(plain).digest().toString('hex');
+    let buffer = new Buffer(sha1);
+
+    return buffer.toString('base64');
+}
+
+function createRequestOptions(task) {
+    return requestOptions = {
+        url: config.h2h_out.partner,
+        qs: {
+            memberID: config.h2h_out.userid,
+            product: task.remoteProduct,
+            dest: task.destination,
+            refID: task.requestId,
+            sign: generateSign(
+                config.h2h_out.userid,
+                task.remoteProduct,
+                task.destination,
+                task.requestId,
+                config.h2h_out.pin,
+                confg.h2h_out.password
+            )
+        }
+    };
+}
+
+function onSameDayDupe(task) {
+    callbackReport(task.requestId, '55', 'Transaksi duplikat dalam satu hari yang sama');
+}
+
+exports.start = start;
+exports.topupRequest = topupRequest;
+exports.generateSign = generateSign;
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..9b352ed
--- /dev/null
+++ b/test.js
@@ -0,0 +1,10 @@
+var should = require('should');
+var partner = require('./partner-otomax');
+
+describe ('#partner', function() {
+    describe('generateSign', function() {
+        it('should return correct sign based on example from otomax doc', function() {
+            partner.generateSign('YUSUF', 'XX10', '08123456789', '2140669', '1144', 'abcd').should.equal('YmU1YWNkZjU4YmFlZTMxMWMwNGZjZmRiNWM4NTA3MmIwZDhkOGM3YQ==');
+        })
+    });
+});