From 5443b922a7960d4a2770198890a3b3188bcd4088 Mon Sep 17 00:00:00 2001
From: adi surya <adisurya1@gmail.com>
Date: Wed, 27 Jul 2022 14:50:24 +0700
Subject: [PATCH] file lib

---
 .gitignore                                         |   1 +
 config.sample.json                                 |  22 ++++
 index.js                                           |   9 ++
 lib/actions/buy.js                                 |  71 +++++++++++++
 lib/actions/index.js                               |   3 +
 lib/actions/inquiry-to-komodo.js                   |  64 ++++++++++++
 lib/actions/inquiry.js                             |  71 +++++++++++++
 lib/actions/pay.js                                 |  71 +++++++++++++
 lib/actions/payment-to-komodo.js                   |  64 ++++++++++++
 lib/actions/topup-to-komodo.js                     |  64 ++++++++++++
 lib/config/data.js                                 | 113 +++++++++++++++++++++
 lib/config/index.js                                |   3 +
 lib/config/remove-postpaid-product.js              |  33 ++++++
 lib/config/remove-product.js                       |  33 ++++++
 lib/config/save-postpaid-product.js                |  32 ++++++
 lib/config/save-product.js                         |  32 ++++++
 lib/http-server/index.js                           |  40 ++++++++
 lib/http-server/middlewares/check-apikey.js        |  16 +++
 lib/http-server/routers/matrix.js                  |  12 +++
 lib/http-server/routers/postpaid-products/index.js |  80 +++++++++++++++
 lib/http-server/routers/products/index.js          |  80 +++++++++++++++
 lib/http-server/routers/updates/index.js           |  69 +++++++++++++
 lib/matrix/dump.js                                 |  10 ++
 lib/matrix/index.js                                |  19 ++++
 lib/pull/get-inquiry.js                            |  70 +++++++++++++
 lib/pull/get-payment.js                            |  70 +++++++++++++
 lib/pull/get-prepaid.js                            |  69 +++++++++++++
 lib/pull/index.js                                  |   1 +
 lib/pull/run.js                                    |  22 ++++
 29 files changed, 1244 insertions(+)
 create mode 100644 config.sample.json
 create mode 100644 index.js
 create mode 100644 lib/actions/buy.js
 create mode 100644 lib/actions/index.js
 create mode 100644 lib/actions/inquiry-to-komodo.js
 create mode 100644 lib/actions/inquiry.js
 create mode 100644 lib/actions/pay.js
 create mode 100644 lib/actions/payment-to-komodo.js
 create mode 100644 lib/actions/topup-to-komodo.js
 create mode 100644 lib/config/data.js
 create mode 100644 lib/config/index.js
 create mode 100644 lib/config/remove-postpaid-product.js
 create mode 100644 lib/config/remove-product.js
 create mode 100644 lib/config/save-postpaid-product.js
 create mode 100644 lib/config/save-product.js
 create mode 100644 lib/http-server/index.js
 create mode 100644 lib/http-server/middlewares/check-apikey.js
 create mode 100644 lib/http-server/routers/matrix.js
 create mode 100644 lib/http-server/routers/postpaid-products/index.js
 create mode 100644 lib/http-server/routers/products/index.js
 create mode 100644 lib/http-server/routers/updates/index.js
 create mode 100644 lib/matrix/dump.js
 create mode 100644 lib/matrix/index.js
 create mode 100644 lib/pull/get-inquiry.js
 create mode 100644 lib/pull/get-payment.js
 create mode 100644 lib/pull/get-prepaid.js
 create mode 100644 lib/pull/index.js
 create mode 100644 lib/pull/run.js

diff --git a/.gitignore b/.gitignore
index 3c3629e..36420af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 node_modules
+config.json
\ No newline at end of file
diff --git a/config.sample.json b/config.sample.json
new file mode 100644
index 0000000..1048184
--- /dev/null
+++ b/config.sample.json
@@ -0,0 +1,22 @@
+{
+    "name": "gateway-sds-ss",
+    "url": "http://localhost",
+    "port": 11330,
+    "apikey": "fd97cf519b979262d9d9004cba6a165629ca8b69350a6afdcaab6ab2c9a996ae",
+    "pull_interval_ms": 5000,
+    "core": {
+        "url": "http://localhost:26840",
+        "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJnZW5lcmF0b3IiOiJDTEkiLCJjbGllbnQiOnsibmFtZSI6IktPTU9ETzItR1ctU0RTLVNTIn0sImlhdCI6MTY1ODkwODA4N30.2Ego28jwgPs3s9-iKSbYipO72FTH5gFC5gkVccqiN24",
+        "request_timeout_ms": 10000
+    },
+    "products": {
+    },
+    "postpaid_products": {
+    },
+    "sds_ss": {
+        "url": "http://localhost:8187/request",
+        "request_timeout_ms": 10000,
+        "username": "user",
+        "password": "1234"
+    }
+}
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..3a2fd9b
--- /dev/null
+++ b/index.js
@@ -0,0 +1,9 @@
+process.chdir(__dirname);
+
+const fs = require('fs');
+const pull = require('./lib/pull');
+require('./lib/http-server');
+
+pull.run();
+
+fs.writeFileSync('pid.txt', process.pid.toString());
diff --git a/lib/actions/buy.js b/lib/actions/buy.js
new file mode 100644
index 0000000..3c6b34d
--- /dev/null
+++ b/lib/actions/buy.js
@@ -0,0 +1,71 @@
+const MODULE_NAME = 'ACTIONS.BUY';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+
+const config = require('../config');
+const configData = require('../config/data');
+
+const topupToKomodo = require('./topup-to-komodo');
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+/**
+ * Buy a product from supplier komodo
+ *
+ * @param {string} xid
+ * @param {object} transaction
+ *
+ */
+module.exports = async (xid, transaction) => {
+    try {
+        logger.verbose(`${MODULE_NAME} 4B139379: Buy product to komodo`, {
+            xid,
+            transaction,
+        });
+        const iConfig = await configData.all();
+
+        let productName = transaction.product_name;
+        if (
+            iConfig.products[transaction.product_name]
+            && iConfig.products[transaction.product_name].remote
+        ) {
+            productName = iConfig.products[transaction.product_name].remote;
+        }
+
+        const callbackUrl = `${iConfig.url}:${iConfig.port}/apikey/${iConfig.apikey}/updates`;
+        const result = await topupToKomodo(
+            xid,
+            transaction.id,
+            transaction.destination,
+            productName,
+            callbackUrl,
+        );
+        logger.verbose(`${MODULE_NAME} 5BDFAF41: result from komodo`, {
+            xid,
+            trxId: transaction.id,
+            result,
+        });
+        const params = {
+            id: result.request_id,
+            rc: result.rc,
+            amount: result.amount || null,
+            message: result.message,
+            sn: result.sn || null,
+        };
+
+        await client.post('/transactions/gateway-update', params);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} E9887C98: Exception`, {
+            xid,
+            message: e.message,
+            code: e.code,
+        });
+    }
+};
diff --git a/lib/actions/index.js b/lib/actions/index.js
new file mode 100644
index 0000000..580d9ad
--- /dev/null
+++ b/lib/actions/index.js
@@ -0,0 +1,3 @@
+exports.buy = require('./buy');
+exports.inquiry = require('./inquiry');
+exports.pay = require('./pay');
diff --git a/lib/actions/inquiry-to-komodo.js b/lib/actions/inquiry-to-komodo.js
new file mode 100644
index 0000000..19a60c0
--- /dev/null
+++ b/lib/actions/inquiry-to-komodo.js
@@ -0,0 +1,64 @@
+const MODULE_NAME = 'ACTIONS.INQUIRY-TO-KOMODO';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+
+const config = require('../config');
+
+const client = axios.create({
+    baseURL: config.komodo_http_get_x.url,
+    timeout: config.komodo_http_get_x.request_timeout_ms,
+});
+
+/**
+ * request inquiry transaction to komodo
+ *
+ * @param {string} xid
+ * @param {string} requestId
+ * @param {string} destination
+ * @param {string} productName
+ * @param {string} reverseUrl
+ */
+module.exports = async (
+    xid,
+    requestId,
+    destination,
+    productName,
+    reverseUrl,
+) => {
+    logger.verbose(`${MODULE_NAME} 4AD4DF41: request inquiry to komodo`, {
+        requestId, destination, productName, reverseUrl, xid,
+    });
+
+    const params = {
+        request_id: requestId,
+        terminal_name: config.komodo_http_get_x.terminal_name,
+        password: config.komodo_http_get_x.password,
+        destination,
+        product_name: productName,
+        reverse_url: reverseUrl,
+    };
+
+    try {
+        const response = await client.get('/inquiry', {
+            params,
+        });
+        if (!response) {
+            throw new Error(`${MODULE_NAME} 6CE3E06E: Empty response from komodo`);
+        }
+        if (!response.data) {
+            throw new Error(`${MODULE_NAME} F236476F: Empty response data from komodo`);
+        }
+
+        return response.data;
+    } catch (err) {
+        logger.warn(`${MODULE_NAME} 48BAA6B7: Exception`, {
+            xid,
+            requestId,
+            destination,
+            productName,
+            message: err.message,
+        });
+        throw err;
+    }
+};
diff --git a/lib/actions/inquiry.js b/lib/actions/inquiry.js
new file mode 100644
index 0000000..555e31c
--- /dev/null
+++ b/lib/actions/inquiry.js
@@ -0,0 +1,71 @@
+const MODULE_NAME = 'ACTIONS.INQUIRY';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+
+const config = require('../config');
+const configData = require('../config/data');
+
+const inquiryToKomodo = require('./inquiry-to-komodo');
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+/**
+ * Inquiry a product from supplier komodo
+ *
+ * @param {string} xid
+ * @param {object} transaction
+ *
+ */
+module.exports = async (xid, transaction) => {
+    try {
+        logger.verbose(`${MODULE_NAME} ABE82225: Inquiry product to komodo`, {
+            xid,
+            transaction,
+        });
+        const iConfig = await configData.all();
+
+        let productName = transaction.product_name;
+        if (
+            iConfig.postpaid_products[transaction.product_name]
+            && iConfig.postpaid_products[transaction.product_name].remote
+        ) {
+            productName = iConfig.postpaid_products[transaction.product_name].remote;
+        }
+
+        const callbackUrl = `${iConfig.url}:${iConfig.port}/apikey/${iConfig.apikey}/updates`;
+        const result = await inquiryToKomodo(
+            xid,
+            transaction.id,
+            transaction.destination,
+            productName,
+            callbackUrl,
+        );
+        logger.verbose(`${MODULE_NAME} 076C1206: result from komodo`, {
+            xid,
+            trxId: transaction.id,
+            result,
+        });
+        const params = {
+            id: result.request_id,
+            rc: result.rc,
+            amount: result.amount || null,
+            message: result.message,
+            sn: result.sn || null,
+        };
+
+        await client.post('/transactions/gateway-update', params);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} D1A1B698: Exception`, {
+            xid,
+            message: e.message,
+            code: e.code,
+        });
+    }
+};
diff --git a/lib/actions/pay.js b/lib/actions/pay.js
new file mode 100644
index 0000000..ac7ed6f
--- /dev/null
+++ b/lib/actions/pay.js
@@ -0,0 +1,71 @@
+const MODULE_NAME = 'ACTIONS.PAY';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+
+const config = require('../config');
+const configData = require('../config/data');
+
+const paymentToKomodo = require('./payment-to-komodo');
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+/**
+ * Pay a product from supplier komodo
+ *
+ * @param {string} xid
+ * @param {object} transaction
+ *
+ */
+module.exports = async (xid, transaction) => {
+    try {
+        logger.verbose(`${MODULE_NAME} CB9506E9: Pay product to komodo`, {
+            xid,
+            transaction,
+        });
+        const iConfig = await configData.all();
+
+        let productName = transaction.product_name;
+        if (
+            iConfig.postpaid_products[transaction.product_name]
+            && iConfig.postpaid_products[transaction.product_name].remote
+        ) {
+            productName = iConfig.postpaid_products[transaction.product_name].remote;
+        }
+
+        const callbackUrl = `${iConfig.url}:${iConfig.port}/apikey/${iConfig.apikey}/updates`;
+        const result = await paymentToKomodo(
+            xid,
+            transaction.id,
+            transaction.destination,
+            productName,
+            callbackUrl,
+        );
+        logger.verbose(`${MODULE_NAME} 4F45F9E3: result from komodo`, {
+            xid,
+            trxId: transaction.id,
+            result,
+        });
+        const params = {
+            id: result.request_id,
+            rc: result.rc,
+            amount: result.amount || null,
+            message: result.message,
+            sn: result.sn || null,
+        };
+
+        await client.post('/transactions/gateway-update', params);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} EF0EE887: Exception`, {
+            xid,
+            message: e.message,
+            code: e.code,
+        });
+    }
+};
diff --git a/lib/actions/payment-to-komodo.js b/lib/actions/payment-to-komodo.js
new file mode 100644
index 0000000..3132ef0
--- /dev/null
+++ b/lib/actions/payment-to-komodo.js
@@ -0,0 +1,64 @@
+const MODULE_NAME = 'ACTIONS.PAY-TO-KOMODO';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+
+const config = require('../config');
+
+const client = axios.create({
+    baseURL: config.komodo_http_get_x.url,
+    timeout: config.komodo_http_get_x.request_timeout_ms,
+});
+
+/**
+ * request pay transaction to komodo
+ *
+ * @param {string} xid
+ * @param {string} requestId
+ * @param {string} destination
+ * @param {string} productName
+ * @param {string} reverseUrl
+ */
+module.exports = async (
+    xid,
+    requestId,
+    destination,
+    productName,
+    reverseUrl,
+) => {
+    logger.verbose(`${MODULE_NAME} 740EF164: pay to komodo`, {
+        requestId, destination, productName, xid,
+    });
+
+    const params = {
+        request_id: requestId,
+        terminal_name: config.komodo_http_get_x.terminal_name,
+        password: config.komodo_http_get_x.password,
+        destination,
+        product_name: productName,
+        reverse_url: reverseUrl,
+    };
+
+    try {
+        const response = await client.get('/pay', {
+            params,
+        });
+        if (!response) {
+            throw new Error(`${MODULE_NAME} 29D8D56F: Empty response from komodo`);
+        }
+        if (!response.data) {
+            throw new Error(`${MODULE_NAME} 288CD049: Empty response data from komodo`);
+        }
+
+        return response.data;
+    } catch (err) {
+        logger.warn(`${MODULE_NAME} DCE65215: Exception`, {
+            xid,
+            requestId,
+            destination,
+            productName,
+            message: err.message,
+        });
+        throw err;
+    }
+};
diff --git a/lib/actions/topup-to-komodo.js b/lib/actions/topup-to-komodo.js
new file mode 100644
index 0000000..c712eac
--- /dev/null
+++ b/lib/actions/topup-to-komodo.js
@@ -0,0 +1,64 @@
+const MODULE_NAME = 'ACTIONS.TOPUP-TO-KOMODO';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+
+const config = require('../config');
+
+const client = axios.create({
+    baseURL: config.komodo_http_get_x.url,
+    timeout: config.komodo_http_get_x.request_timeout_ms,
+});
+
+/**
+ * request topup transaction to komodo
+ *
+ * @param {string} xid
+ * @param {string} requestId
+ * @param {string} destination
+ * @param {string} productName
+ * @param {string} reverseUrl
+ */
+module.exports = async (
+    xid,
+    requestId,
+    destination,
+    productName,
+    reverseUrl,
+) => {
+    logger.verbose(`${MODULE_NAME} 4AD4DF41: topup to komodo`, {
+        requestId, destination, productName, xid,
+    });
+
+    const params = {
+        request_id: requestId,
+        terminal_name: config.komodo_http_get_x.terminal_name,
+        password: config.komodo_http_get_x.password,
+        destination,
+        product_name: productName,
+        reverse_url: reverseUrl,
+    };
+
+    try {
+        const response = await client.get('/topup', {
+            params,
+        });
+        if (!response) {
+            throw new Error(`${MODULE_NAME} 6CE3E06E: Empty response from komodo`);
+        }
+        if (!response.data) {
+            throw new Error(`${MODULE_NAME} F236476F: Empty response data from komodo`);
+        }
+
+        return response.data;
+    } catch (err) {
+        logger.warn(`${MODULE_NAME} 48BAA6B7: Exception`, {
+            xid,
+            requestId,
+            destination,
+            productName,
+            message: err.message,
+        });
+        throw err;
+    }
+};
diff --git a/lib/config/data.js b/lib/config/data.js
new file mode 100644
index 0000000..f25a9e3
--- /dev/null
+++ b/lib/config/data.js
@@ -0,0 +1,113 @@
+const MODULE_NAME = 'CONFIG.DATA';
+
+const fs = require('fs/promises');
+const logger = require('tektrans-logger');
+
+let config = {};
+
+/**
+ * Get all config
+ *
+ * @param {string} xid
+ *
+ * @returns <object>
+ */
+async function all(xid) {
+    try {
+        if (Object.keys(config).length === 0) {
+            const configString = await fs.readFile('config.json');
+            config = JSON.parse(configString);
+        }
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} ED8D390C: Exception`, {
+            xid,
+            eMessage: e.message,
+            eCode: e.code,
+        });
+    }
+    return config;
+}
+
+/**
+ * Get active product prepaid config
+ *
+ * @param {string} xid
+ *
+ * @returns <Array>
+ */
+async function getActiveProductArray(xid) {
+    try {
+        if (Object.keys(config).length === 0) {
+            const configString = await fs.readFile('config.json');
+            config = JSON.parse(configString);
+        }
+        const allProductsArray = Object.values(config.products);
+        if (Array.isArray(allProductsArray)) {
+            const activeProductArray = allProductsArray.filter((val) => val.active === 1);
+            const activeProductNameArray = activeProductArray.map((val) => val.name);
+            return activeProductNameArray;
+        }
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 2D49D035: Exception`, {
+            xid,
+            eMessage: e.message,
+            eCode: e.code,
+        });
+    }
+    return [];
+}
+
+/**
+ * Get active product postpaid config
+ *
+ * @param {string} xid
+ *
+ * @returns <Array>
+ */
+async function getActiveProductPostpaidArray(xid) {
+    try {
+        if (Object.keys(config).length === 0) {
+            const configString = await fs.readFile('config.json');
+            config = JSON.parse(configString);
+        }
+        const allProductsArray = Object.values(config.postpaid_products);
+        if (Array.isArray(allProductsArray)) {
+            const activeProductArray = allProductsArray.filter((val) => val.active === 1);
+            const activeProductNameArray = activeProductArray.map((val) => val.name);
+            return activeProductNameArray;
+        }
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} B5414DB0: Exception`, {
+            xid,
+            eMessage: e.message,
+            eCode: e.code,
+        });
+    }
+    return [];
+}
+
+/**
+ * Reload config
+ *
+ * @param {string} xid
+ *
+ * @returns <object>
+ */
+async function reload(xid) {
+    try {
+        const configString = await fs.readFile('config.json');
+        config = JSON.parse(configString);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} D005AED3: Exception`, {
+            xid,
+            eMessage: e.message,
+            eCode: e.code,
+        });
+    }
+    return config;
+}
+
+exports.all = all;
+exports.reload = reload;
+exports.getActiveProductArray = getActiveProductArray;
+exports.getActiveProductPostpaidArray = getActiveProductPostpaidArray;
diff --git a/lib/config/index.js b/lib/config/index.js
new file mode 100644
index 0000000..e04cb9d
--- /dev/null
+++ b/lib/config/index.js
@@ -0,0 +1,3 @@
+const data = require('../../config.json');
+
+module.exports = data;
diff --git a/lib/config/remove-postpaid-product.js b/lib/config/remove-postpaid-product.js
new file mode 100644
index 0000000..e7ee7f9
--- /dev/null
+++ b/lib/config/remove-postpaid-product.js
@@ -0,0 +1,33 @@
+const MODULE_NAME = 'CONFIG.REMOVE-POSTPAID-PRODUCT';
+
+const fs = require('fs/promises');
+const logger = require('tektrans-logger');
+
+const configData = require('./data');
+
+/**
+ * Menghapus nilai terkini config.products (postpaid)
+ *
+ * @param {string} name
+ *
+ */
+module.exports = async (xid, name) => {
+    try {
+        logger.verbose(`${MODULE_NAME} B4361668: remove postpaid product from config file`, {
+            xid,
+            key: name,
+        });
+        const config = await configData.reload();
+
+        delete config.postpaid_products[name];
+        await fs.writeFile('config.json', JSON.stringify(config, null, 4));
+
+        await configData.reload();
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 9E5C6976: Exception`, {
+            xid,
+            eCode: e.code,
+            eMessage: e.message,
+        });
+    }
+};
diff --git a/lib/config/remove-product.js b/lib/config/remove-product.js
new file mode 100644
index 0000000..5e88d07
--- /dev/null
+++ b/lib/config/remove-product.js
@@ -0,0 +1,33 @@
+const MODULE_NAME = 'CONFIG.REMOVE-PREPAID-PRODUCT';
+
+const fs = require('fs/promises');
+const logger = require('tektrans-logger');
+
+const configData = require('./data');
+
+/**
+ * Menghapus nilai terkini config.products (prepaid)
+ *
+ * @param {string} name
+ *
+ */
+module.exports = async (xid, name) => {
+    try {
+        logger.verbose(`${MODULE_NAME} 80D1628F: remove prepaid product from config file`, {
+            xid,
+            key: name,
+        });
+        const config = await configData.reload();
+
+        delete config.products[name];
+        await fs.writeFile('config.json', JSON.stringify(config, null, 4));
+
+        await configData.reload();
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 190BD3D7: Exception`, {
+            xid,
+            eCode: e.code,
+            eMessage: e.message,
+        });
+    }
+};
diff --git a/lib/config/save-postpaid-product.js b/lib/config/save-postpaid-product.js
new file mode 100644
index 0000000..8ee1f1f
--- /dev/null
+++ b/lib/config/save-postpaid-product.js
@@ -0,0 +1,32 @@
+const MODULE_NAME = 'CONFIG.SAVE-POSTPAID-PRODUCT';
+
+const fs = require('fs/promises');
+const logger = require('tektrans-logger');
+
+const configData = require('./data');
+
+/**
+ * Menyimpan nilai terkini config.products (postpaid)
+ *
+ */
+module.exports = async (xid, key, value) => {
+    try {
+        logger.verbose(`${MODULE_NAME} 19BB0554: Saving postpaid product to config file`, {
+            xid,
+            key,
+            value,
+        });
+        const config = await configData.reload();
+
+        config.postpaid_products[key] = value;
+        await fs.writeFile('config.json', JSON.stringify(config, null, 4));
+
+        await configData.reload();
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} D0F796A6: Exception`, {
+            xid,
+            eCode: e.code,
+            eMessage: e.message,
+        });
+    }
+};
diff --git a/lib/config/save-product.js b/lib/config/save-product.js
new file mode 100644
index 0000000..d9fac58
--- /dev/null
+++ b/lib/config/save-product.js
@@ -0,0 +1,32 @@
+const MODULE_NAME = 'CONFIG.SAVE-PREPAID-PRODUCT';
+
+const fs = require('fs/promises');
+const logger = require('tektrans-logger');
+
+const configData = require('./data');
+
+/**
+ * Menyimpan nilai terkini config.products (prepaid)
+ *
+ */
+module.exports = async (xid, key, value) => {
+    try {
+        logger.verbose(`${MODULE_NAME} C6D936BF: Saving prepaid product to config file`, {
+            xid,
+            key,
+            value,
+        });
+        const config = await configData.reload();
+
+        config.products[key] = value;
+        await fs.writeFile('config.json', JSON.stringify(config, null, 4));
+
+        await configData.reload();
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 1D1937A9: Exception`, {
+            xid,
+            eCode: e.code,
+            eMessage: e.message,
+        });
+    }
+};
diff --git a/lib/http-server/index.js b/lib/http-server/index.js
new file mode 100644
index 0000000..5b44a6b
--- /dev/null
+++ b/lib/http-server/index.js
@@ -0,0 +1,40 @@
+const MODULE_NAME = 'HTTP-SERVER';
+
+const express = require('express');
+const uniqid = require('uniqid');
+const logger = require('tektrans-logger');
+
+const config = require('../config');
+const matrix = require('../matrix');
+const checkApikey = require('./middlewares/check-apikey');
+
+const routerMatrix = require('./routers/matrix');
+const routerUpdates = require('./routers/updates');
+const routerProducts = require('./routers/products');
+const routerPostpaidProducts = require('./routers/postpaid-products');
+
+const app = express();
+
+app.use((req, res, next) => {
+    matrix.httpServer.requestCounter += 1;
+    res.locals.xid = uniqid();
+
+    next();
+});
+
+app.use('/matrix', routerMatrix);
+app.use('/apikey/:apikey/updates', [checkApikey], routerUpdates);
+app.use('/apikey/:apikey/products', [checkApikey], routerProducts);
+app.use('/apikey/:apikey/postpaid/products', [checkApikey], routerPostpaidProducts);
+
+app.use((req, res) => {
+    res.status(404).json({
+        error: true,
+        message: 'Method/service not found',
+    });
+});
+
+const { port } = config;
+app.listen(port, () => {
+    logger.info(`${MODULE_NAME} 35069698: Listening`, { port });
+});
diff --git a/lib/http-server/middlewares/check-apikey.js b/lib/http-server/middlewares/check-apikey.js
new file mode 100644
index 0000000..89cec38
--- /dev/null
+++ b/lib/http-server/middlewares/check-apikey.js
@@ -0,0 +1,16 @@
+const config = require('../../config');
+
+module.exports = async (req, res, next) => {
+    const { apikey } = req.params;
+
+    if (!config.apikey) {
+        next();
+    } else if (config.apikey === apikey) {
+        next();
+    } else {
+        res.status(403).json({
+            error: true,
+            message: 'Invalid apikey',
+        });
+    }
+};
diff --git a/lib/http-server/routers/matrix.js b/lib/http-server/routers/matrix.js
new file mode 100644
index 0000000..ee26c3d
--- /dev/null
+++ b/lib/http-server/routers/matrix.js
@@ -0,0 +1,12 @@
+const express = require('express');
+
+const matrixDump = require('../../matrix/dump');
+
+const router = express.Router();
+module.exports = router;
+
+const pageMain = (req, res) => {
+    res.json(matrixDump());
+};
+
+router.all('/', pageMain);
diff --git a/lib/http-server/routers/postpaid-products/index.js b/lib/http-server/routers/postpaid-products/index.js
new file mode 100644
index 0000000..3901983
--- /dev/null
+++ b/lib/http-server/routers/postpaid-products/index.js
@@ -0,0 +1,80 @@
+const MODULE_NAME = 'HTTP-SERVER.ROUTER.POSTPAID-PRODUCTS';
+
+const express = require('express');
+const logger = require('tektrans-logger');
+
+const configData = require('../../../config/data');
+const configSaveProduct = require('../../../config/save-postpaid-product');
+const configRemoveProduct = require('../../../config/remove-postpaid-product');
+
+const router = express.Router();
+
+module.exports = router;
+
+const pageIndex = async (req, res) => {
+    const { xid } = res.locals;
+    try {
+        logger.verbose(`${MODULE_NAME} 24D7D9B4: get postpaid product configuration`, { xid });
+        const products = (await configData.all()).postpaid_products || {};
+        res.json({ error: false, message: 'OK', result: products });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 1DB45AC5: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+const pageSave = async (req, res) => {
+    const { xid } = res.locals;
+    const { name, remote = null, active = 0 } = req.body;
+    try {
+        logger.verbose(`${MODULE_NAME} A083E9DC: save postpaid product configuration`, { xid, data: req.body });
+
+        const params = {
+            name,
+            remote,
+            active,
+        };
+        await configSaveProduct(xid, name, params);
+        res.json({ error: false, message: 'OK' });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 88B9218F: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+const pageRemove = async (req, res) => {
+    const { xid } = res.locals;
+    const { name } = req.body;
+    try {
+        logger.verbose(`${MODULE_NAME} 7AD392AE: remove postpaid product configuration`, { xid, name });
+
+        await configRemoveProduct(xid, name);
+        res.json({ error: false, message: 'OK' });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} F714088B: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+router.get('/', [express.json()], pageIndex);
+
+router.post('/save', [express.json()], pageSave);
+router.post('/remove', [express.json()], pageRemove);
diff --git a/lib/http-server/routers/products/index.js b/lib/http-server/routers/products/index.js
new file mode 100644
index 0000000..c9eee25
--- /dev/null
+++ b/lib/http-server/routers/products/index.js
@@ -0,0 +1,80 @@
+const MODULE_NAME = 'HTTP-SERVER.ROUTER.PRODUCTS';
+
+const express = require('express');
+const logger = require('tektrans-logger');
+
+const configData = require('../../../config/data');
+const configSaveProduct = require('../../../config/save-product');
+const configRemoveProduct = require('../../../config/remove-product');
+
+const router = express.Router();
+
+module.exports = router;
+
+const pageIndex = async (req, res) => {
+    const { xid } = res.locals;
+    try {
+        logger.verbose(`${MODULE_NAME} F1BF0675: get product configuration`, { xid });
+        const products = (await configData.all()).products || {};
+        res.json({ error: false, message: 'OK', result: products });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 839E55E0: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+const pageSave = async (req, res) => {
+    const { xid } = res.locals;
+    const { name, remote = null, active = 0 } = req.body;
+    try {
+        logger.verbose(`${MODULE_NAME} 59CB2503: save product configuration`, { xid, data: req.body });
+
+        const params = {
+            name,
+            remote,
+            active,
+        };
+        await configSaveProduct(xid, name, params);
+        res.json({ error: false, message: 'OK' });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 47F57E23: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+const pageRemove = async (req, res) => {
+    const { xid } = res.locals;
+    const { name } = req.body;
+    try {
+        logger.verbose(`${MODULE_NAME} E94A7B38: remove product configuration`, { xid, name });
+
+        await configRemoveProduct(xid, name);
+        res.json({ error: false, message: 'OK' });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 955FD7E3: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+router.get('/', [express.json()], pageIndex);
+
+router.post('/save', [express.json()], pageSave);
+router.post('/remove', [express.json()], pageRemove);
diff --git a/lib/http-server/routers/updates/index.js b/lib/http-server/routers/updates/index.js
new file mode 100644
index 0000000..1bbf3ac
--- /dev/null
+++ b/lib/http-server/routers/updates/index.js
@@ -0,0 +1,69 @@
+const MODULE_NAME = 'HTTP-SERVER.ROUTER.UPDATES';
+
+const express = require('express');
+const axios = require('axios').default;
+const logger = require('tektrans-logger');
+
+const config = require('../../../config');
+
+const router = express.Router();
+
+module.exports = router;
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+const pageUpdate = async (req, res) => {
+    const { xid } = res.locals;
+    let data = req.query;
+    if (req.method.toUpperCase() !== 'GET') {
+        data = req.body;
+    }
+
+    try {
+        logger.verbose(`${MODULE_NAME} 9E5C70C8: update from komodo`, { xid, data });
+        const params = {
+            id: data.request_id,
+            rc: data.rc,
+            amount: data.amount || null,
+            message: data.message,
+            sn: data.sn || null,
+            bill_count: data.bill_count,
+            bill_amount: data.bill_amount,
+            related_data: null,
+            amount_to_charge: data.amount_to_charge,
+        };
+
+        client.post('/transactions/gateway-update', params).then((result) => {
+            logger.verbose(`${MODULE_NAME} A8DA0D04: response from core2`, {
+                xid,
+                response: result.data,
+            });
+        }).catch((err) => {
+            logger.warn(`${MODULE_NAME} 32EB485C: Exception on request to core2`, {
+                xid,
+                eMessage: err.message,
+                eCode: err.code,
+            });
+        });
+
+        res.json({ error: false, message: 'OK' });
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} B12B4A2C: Exception.`, {
+            xid, eMessage: e.message, eCode: e.code,
+        });
+        res.status(500).json({
+            error: true,
+            error_code: e.code,
+            message: e.message,
+        });
+    }
+};
+
+router.get('/', pageUpdate);
+router.post('/', [express.urlencoded({ extended: true }), express.json()], pageUpdate);
diff --git a/lib/matrix/dump.js b/lib/matrix/dump.js
new file mode 100644
index 0000000..0ac7f24
--- /dev/null
+++ b/lib/matrix/dump.js
@@ -0,0 +1,10 @@
+const matrix = require('./index');
+
+module.exports = () => {
+    matrix.processTitle = process.title;
+    matrix.uptimeSecs = process.uptime();
+    matrix.memoryUsage = process.memoryUsage();
+    matrix.resourceUsage = process.resourceUsage();
+
+    return matrix;
+};
diff --git a/lib/matrix/index.js b/lib/matrix/index.js
new file mode 100644
index 0000000..ca3b317
--- /dev/null
+++ b/lib/matrix/index.js
@@ -0,0 +1,19 @@
+const data = {
+    processTitle: process.title,
+    pid: process.pid,
+    workingDirectory: process.cwd(),
+    uptimeSecs: process.uptime(),
+
+    memoryUsage: process.memoryUsage(),
+    resourceUsage: process.resourceUsage(),
+
+    platform: process.platform,
+    nodeVersion: process.version,
+    nodeRelease: process.release,
+
+    httpServer: {
+        requestCounter: 0,
+    },
+};
+
+module.exports = data;
diff --git a/lib/pull/get-inquiry.js b/lib/pull/get-inquiry.js
new file mode 100644
index 0000000..00aac65
--- /dev/null
+++ b/lib/pull/get-inquiry.js
@@ -0,0 +1,70 @@
+const MODULE_NAME = 'PULL.GET-INQUIRY';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+const uniqid = require('uniqid');
+const config = require('../config');
+const configData = require('../config/data');
+const actions = require('../actions');
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+let onPull = false;
+
+/**
+ * pull unprocessed inquiry transaction from core
+ */
+module.exports = async () => {
+    logger.verbose(`${MODULE_NAME} 64E2EFDE: Pull inquiry transaction from core`);
+    if (onPull) {
+        logger.verbose(`${MODULE_NAME} EECB3ECC: Pull inquiry already running`);
+        return false;
+    }
+    onPull = true;
+
+    const xid = uniqid();
+
+    try {
+        const products = await configData.getActiveProductPostpaidArray(xid);
+        const response = await client.post('/transactions/inquiry-pull', {
+            gateway: {
+                name: `${config.name}-postpaid`,
+                url: `${config.url}:${config.port}/apikey/${config.apikey}/postpaid`,
+                postpaid: 1,
+            },
+            products,
+        });
+        if (response.data.error) {
+            logger.info(`${MODULE_NAME} E082E007: Error when pulling inquiry transaction`, {
+                xid,
+                message: response.data.message,
+                error: response.data.error,
+            });
+        }
+
+        if (!response.data.result) {
+            logger.info(`${MODULE_NAME} 72C1FAC5: Empty inquiry transaction result`, {
+                xid,
+                result: response.data.result,
+            });
+            return null;
+        }
+
+        await actions.inquiry(xid, response.data.result);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 1E5D9D56: Exception`, {
+            xid,
+            message: e.message,
+            code: e.code,
+        });
+    } finally {
+        onPull = false;
+    }
+    return true;
+};
diff --git a/lib/pull/get-payment.js b/lib/pull/get-payment.js
new file mode 100644
index 0000000..a7587be
--- /dev/null
+++ b/lib/pull/get-payment.js
@@ -0,0 +1,70 @@
+const MODULE_NAME = 'PULL.GET-PAYMENT';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+const uniqid = require('uniqid');
+const config = require('../config');
+const configData = require('../config/data');
+const actions = require('../actions');
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+let onPull = false;
+
+/**
+ * pull unprocessed payment transaction from core
+ */
+module.exports = async () => {
+    logger.verbose(`${MODULE_NAME} 68CB1245: Pull payment transaction from core`);
+    if (onPull) {
+        logger.verbose(`${MODULE_NAME} 576C1D1A: Pull payment already running`);
+        return false;
+    }
+    onPull = true;
+
+    const xid = uniqid();
+
+    try {
+        const products = await configData.getActiveProductPostpaidArray(xid);
+        const response = await client.post('/transactions/payment-pull', {
+            gateway: {
+                name: `${config.name}-postpaid`,
+                url: `${config.url}:${config.port}/apikey/${config.apikey}/postpaid`,
+                postpaid: 1,
+            },
+            products,
+        });
+        if (response.data.error) {
+            logger.info(`${MODULE_NAME} E3093F6D: Error when pulling payment transaction`, {
+                xid,
+                message: response.data.message,
+                error: response.data.error,
+            });
+        }
+
+        if (!response.data.result) {
+            logger.info(`${MODULE_NAME} 230E9E0F: Empty payment transaction result`, {
+                xid,
+                result: response.data.result,
+            });
+            return null;
+        }
+
+        await actions.pay(xid, response.data.result);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 7E6D9444: Exception`, {
+            xid,
+            message: e.message,
+            code: e.code,
+        });
+    } finally {
+        onPull = false;
+    }
+    return true;
+};
diff --git a/lib/pull/get-prepaid.js b/lib/pull/get-prepaid.js
new file mode 100644
index 0000000..cce1534
--- /dev/null
+++ b/lib/pull/get-prepaid.js
@@ -0,0 +1,69 @@
+const MODULE_NAME = 'PULL.GET-PREPAID';
+
+const logger = require('tektrans-logger');
+const axios = require('axios').default;
+const uniqid = require('uniqid');
+const config = require('../config');
+const configData = require('../config/data');
+const actions = require('../actions');
+
+const client = axios.create({
+    baseURL: config.core.url,
+    timeout: config.core.request_timeout_ms,
+    headers: {
+        'x-access-token': config.core.access_token,
+    },
+});
+
+let onPull = false;
+
+/**
+ * pull unprocessed prepaid transaction from core
+ */
+module.exports = async () => {
+    logger.verbose(`${MODULE_NAME} 385A1B0D: Pull prepaid transaction from core`);
+    if (onPull) {
+        logger.verbose(`${MODULE_NAME} 6563C2C9: Pull prepaid already running`);
+        return false;
+    }
+    onPull = true;
+
+    const xid = uniqid();
+
+    try {
+        const products = await configData.getActiveProductArray(xid);
+        const response = await client.post('/transactions/prepaid-pull', {
+            gateway: {
+                name: config.name,
+                url: `${config.url}:${config.port}/apikey/${config.apikey}`,
+            },
+            products,
+        });
+        if (response.data.error) {
+            logger.info(`${MODULE_NAME} A397BA74: Error when pulling prepaid transaction`, {
+                xid,
+                message: response.data.message,
+                error: response.data.error,
+            });
+        }
+
+        if (!response.data.result) {
+            logger.info(`${MODULE_NAME} 712130A5: Empty prepaid transaction result`, {
+                xid,
+                result: response.data.result,
+            });
+            return null;
+        }
+
+        await actions.buy(xid, response.data.result);
+    } catch (e) {
+        logger.warn(`${MODULE_NAME} 008B2FA5: Exception`, {
+            xid,
+            message: e.message,
+            code: e.code,
+        });
+    } finally {
+        onPull = false;
+    }
+    return true;
+};
diff --git a/lib/pull/index.js b/lib/pull/index.js
new file mode 100644
index 0000000..503ece1
--- /dev/null
+++ b/lib/pull/index.js
@@ -0,0 +1 @@
+exports.run = require('./run');
diff --git a/lib/pull/run.js b/lib/pull/run.js
new file mode 100644
index 0000000..9d818b6
--- /dev/null
+++ b/lib/pull/run.js
@@ -0,0 +1,22 @@
+const MODULE_NAME = 'PULL.RUN';
+const logger = require('tektrans-logger');
+
+const config = require('../config');
+const getInquiry = require('./get-inquiry');
+const getPrepaid = require('./get-prepaid');
+const getPayment = require('./get-payment');
+
+/**
+ * Run pulling schedule
+ */
+module.exports = async () => {
+    logger.verbose(`${MODULE_NAME} 34022A49: Run pulling schedule`, {
+        interval: config.pull_interval_ms,
+    });
+    setInterval(() => {
+        logger.verbose(`${MODULE_NAME} 06B8C652: Pull run`);
+        getPrepaid();
+        getInquiry();
+        getPayment();
+    }, config.pull_interval_ms);
+};
-- 
1.9.0