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;