add multi-currency master CRUD (Phase 2)

- add currencyadapter.js: list (with pagination+keyword), detail, history, create, update (auto-log rate change to tbl_currency_log), delete (soft), convertAmount helper
- add controllers/currency.js and routes/currency.js, auto-mounted at /currency
- update dbproc.js: configurable port via HOSTPORT env variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rizki
2026-03-23 20:52:29 +07:00
parent b2c866d793
commit 26d22355fa
4 changed files with 415 additions and 1 deletions

311
adapter/currencyadapter.js Normal file
View File

@ -0,0 +1,311 @@
const db = require('../config/dbproc.js');
const Adapter = require('./dbadapter.js');
class CurrencyAdapter extends Adapter {
constructor() {
super();
}
async queryCurrencyList(req, callback) {
var apires = this.getApiResultDefined();
try {
let limit = req.query.limit;
let offset = req.query.offset;
let keyword = req.query.keyword ?? '';
let qryBase = "select _idx, name, currency, symbol, rate from tbl_currency ";
qryBase += "where isdeleted=0 and (name like '%" + keyword + "%' or currency like '%" + keyword + "%') ";
db.query(qryBase + "order by currency asc", [], function (err, result, fields) {
if (err) {
apires.meta['message'] = err.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
if (result.length > 0) {
let pagination = result.length / limit;
if (!Number.isInteger(pagination)) {
pagination = (Math.floor(result.length / limit)) + 1;
}
apires.success = true;
apires.data.push({
"totalpage": pagination,
"totalrows": result.length
});
let qryPage = qryBase + "order by currency asc limit " + offset + ", " + limit;
db.query(qryPage, [], function (err2, result2, fields2) {
if (err2) {
apires.meta['message'] = err2.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
apires.data.push({
"results": JSON.parse(JSON.stringify(result2))
});
callback(null, apires);
}
});
} else {
apires.meta.code = 200;
apires.meta.message = "Record Not Found";
callback(null, apires);
}
}
});
} catch (err) {
apires.meta.code = 500;
apires.meta.message = err.toString();
callback('error', apires);
}
}
async queryCurrencyDetail(req, callback) {
var apires = this.getApiResultDefined();
try {
let id = req.params.id;
let qry = "select _idx, name, currency, symbol, rate, isdeleted, iby, idt, uby, udt from tbl_currency where _idx='" + id + "' and isdeleted=0";
db.query(qry, [], function (err, result, fields) {
if (err) {
apires.meta['message'] = err.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
if (result.length > 0) {
apires.success = true;
apires.data.push({
"results": JSON.parse(JSON.stringify(result))
});
callback(null, apires);
} else {
apires.meta.code = 200;
apires.meta.message = "Record Not Found";
callback(null, apires);
}
}
});
} catch (err) {
apires.meta.code = 500;
apires.meta.message = err.toString();
callback('error', apires);
}
}
async queryCurrencyHistory(req, callback) {
var apires = this.getApiResultDefined();
try {
let id = req.params.id;
// get currency_code first, then fetch log
let qryCode = "select currency from tbl_currency where _idx='" + id + "'";
db.query(qryCode, [], function (err, result, fields) {
if (err) {
apires.meta['message'] = err.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
if (result.length === 0) {
apires.meta.code = 200;
apires.meta.message = "Record Not Found";
callback(null, apires);
return;
}
let currencyCode = result[0].currency;
let qryLog = "select _idx, currency_code, old_rate, new_rate, source, remark, iby, idt from tbl_currency_log where currency_code='" + currencyCode + "' order by idt desc";
db.query(qryLog, [], function (err2, result2, fields2) {
if (err2) {
apires.meta['message'] = err2.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
apires.success = true;
apires.data.push({
"results": JSON.parse(JSON.stringify(result2))
});
callback(null, apires);
}
});
}
});
} catch (err) {
apires.meta.code = 500;
apires.meta.message = err.toString();
callback('error', apires);
}
}
async queryCreateCurrency(req, callback) {
var apires = this.getApiResultDefined();
try {
let name = req.body.name;
let currency = req.body.currency;
let symbol = req.body.symbol;
let rate = req.body.rate;
let nik = req.nik;
let qry = "insert into tbl_currency set ";
qry += "name='" + name + "',";
qry += "currency='" + currency + "',";
qry += "symbol='" + symbol + "',";
qry += "rate='" + rate + "',";
qry += "isdeleted=0,";
qry += "iby='" + nik + "',idt=now()";
db.query(qry, [], function (err, result, fields) {
if (err) {
apires.meta['message'] = err.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
apires.success = true;
apires.meta.message = "Saved Success";
apires.data = JSON.parse(JSON.stringify(result));
callback(null, apires);
}
});
} catch (err) {
apires.meta.code = 500;
apires.meta.message = err.toString();
callback('error', apires);
}
}
async queryUpdateCurrency(req, callback) {
var apires = this.getApiResultDefined();
try {
let id = req.params.id;
let name = req.body.name;
let currency = req.body.currency;
let symbol = req.body.symbol;
let newRate = req.body.rate;
let nik = req.nik;
// fetch current rate to detect change
let qryOld = "select rate, currency from tbl_currency where _idx='" + id + "' and isdeleted=0";
db.query(qryOld, [], function (err, oldResult, fields) {
if (err) {
apires.meta['message'] = err.toString();
apires.meta['code'] = 500;
callback('err', apires);
return;
}
if (oldResult.length === 0) {
apires.meta.code = 200;
apires.meta.message = "Record Not Found";
callback(null, apires);
return;
}
let oldRate = oldResult[0].rate;
let currencyCode = oldResult[0].currency;
let rateChanged = parseFloat(oldRate) !== parseFloat(newRate);
let qryUpdate = "update tbl_currency set ";
qryUpdate += "name='" + name + "',";
qryUpdate += "currency='" + currency + "',";
qryUpdate += "symbol='" + symbol + "',";
qryUpdate += "rate='" + newRate + "',";
qryUpdate += "uby='" + nik + "',udt=now() ";
qryUpdate += "where _idx='" + id + "'";
db.query(qryUpdate, [], function (err2, result2, fields2) {
if (err2) {
apires.meta['message'] = err2.toString();
apires.meta['code'] = 500;
callback('err', apires);
return;
}
if (!rateChanged) {
apires.success = true;
apires.meta.message = "Updated Success";
apires.data = JSON.parse(JSON.stringify(result2));
callback(null, apires);
return;
}
// log the rate change
let qryLog = "insert into tbl_currency_log set ";
qryLog += "currency_code='" + currencyCode + "',";
qryLog += "old_rate='" + oldRate + "',";
qryLog += "new_rate='" + newRate + "',";
qryLog += "source='manual',";
qryLog += "iby='" + nik + "',idt=now()";
db.query(qryLog, [], function (err3, result3, fields3) {
if (err3) {
apires.meta['message'] = err3.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
apires.success = true;
apires.meta.message = "Updated Success";
apires.data = JSON.parse(JSON.stringify(result2));
callback(null, apires);
}
});
});
});
} catch (err) {
apires.meta.code = 500;
apires.meta.message = err.toString();
callback('error', apires);
}
}
async queryDeleteCurrency(req, callback) {
var apires = this.getApiResultDefined();
try {
let id = req.params.id;
let nik = req.nik;
let qry = "update tbl_currency set isdeleted=1,dby='" + nik + "',ddt=now() where _idx='" + id + "'";
db.query(qry, [], function (err, result, fields) {
if (err) {
apires.meta['message'] = err.toString();
apires.meta['code'] = 500;
callback('err', apires);
} else {
apires.success = true;
apires.meta.message = "Deleted Success";
apires.data = JSON.parse(JSON.stringify(result));
callback(null, apires);
}
});
} catch (err) {
apires.meta.code = 500;
apires.meta.message = err.toString();
callback('error', apires);
}
}
// fetch rates for two currency IDs — used for cross-currency conversion
queryCurrencyRates(fromId, toId, callback) {
var apires = this.getApiResultDefined();
let qry = "select _idx, rate from tbl_currency where _idx in ('" + fromId + "','" + toId + "') and isdeleted=0";
db.query(qry, [], function (err, result, fields) {
if (err) {
callback(err, null);
return;
}
var rates = {};
result.forEach(function (row) {
rates[row._idx] = parseFloat(row.rate);
});
if (rates[fromId] === undefined || rates[toId] === undefined) {
callback(new Error('Currency not found'), null);
return;
}
callback(null, { from: rates[fromId], to: rates[toId] });
});
}
// convert amount from one currency to another using USD as pivot
convertAmount(amount, rateFrom, rateTo, callback) {
if (!rateTo || rateTo === 0) {
callback(new Error('Invalid target rate: rate cannot be zero'), null);
return;
}
callback(null, parseFloat(amount) * (rateFrom / rateTo));
}
}
module.exports = CurrencyAdapter;

View File

@ -7,7 +7,8 @@ const dbcon = mysql.createConnection({
database : process.env.DBHOST,
acquireTimeout: 30000,
insecureAuth: true,
timezone: 'utc'
timezone: 'utc',
port : process.env.HOSTPORT ?? 3306
});
dbcon.connect(function(err) {

89
controllers/currency.js Normal file
View File

@ -0,0 +1,89 @@
const CurrencyAdapter = require('../adapter/currencyadapter.js');
const currencyadapter = new CurrencyAdapter();
const Controllers = require('./controller.js');
const controllers = new Controllers();
var apireshandler = controllers.getApiResultDefined();
exports.getCurrencyList = (req, res) => {
try {
currencyadapter.queryCurrencyList(req, function (err, data) {
let statusCode = data != null ? data.meta.code : 200;
if (err) statusCode = 500;
currencyadapter.sendResponse(statusCode, data, res);
});
} catch (err) {
apireshandler.meta.code = 502;
apireshandler.meta.message = "[getCurrencyList] : Currency controller, " + err.toString();
currencyadapter.sendResponse(502, apireshandler, res);
}
}
exports.getCurrencyDetail = (req, res) => {
try {
currencyadapter.queryCurrencyDetail(req, function (err, data) {
let statusCode = data != null ? data.meta.code : 200;
if (err) statusCode = 500;
currencyadapter.sendResponse(statusCode, data, res);
});
} catch (err) {
apireshandler.meta.code = 502;
apireshandler.meta.message = "[getCurrencyDetail] : Currency controller, " + err.toString();
currencyadapter.sendResponse(502, apireshandler, res);
}
}
exports.getCurrencyHistory = (req, res) => {
try {
currencyadapter.queryCurrencyHistory(req, function (err, data) {
let statusCode = data != null ? data.meta.code : 200;
if (err) statusCode = 500;
currencyadapter.sendResponse(statusCode, data, res);
});
} catch (err) {
apireshandler.meta.code = 502;
apireshandler.meta.message = "[getCurrencyHistory] : Currency controller, " + err.toString();
currencyadapter.sendResponse(502, apireshandler, res);
}
}
exports.createCurrency = (req, res) => {
try {
currencyadapter.queryCreateCurrency(req, function (err, data) {
let statusCode = data != null ? data.meta.code : 200;
if (err) statusCode = 500;
currencyadapter.sendResponse(statusCode, data, res);
});
} catch (err) {
apireshandler.meta.code = 502;
apireshandler.meta.message = "[createCurrency] : Currency controller, " + err.toString();
currencyadapter.sendResponse(502, apireshandler, res);
}
}
exports.updateCurrency = (req, res) => {
try {
currencyadapter.queryUpdateCurrency(req, function (err, data) {
let statusCode = data != null ? data.meta.code : 200;
if (err) statusCode = 500;
currencyadapter.sendResponse(statusCode, data, res);
});
} catch (err) {
apireshandler.meta.code = 502;
apireshandler.meta.message = "[updateCurrency] : Currency controller, " + err.toString();
currencyadapter.sendResponse(502, apireshandler, res);
}
}
exports.deleteCurrency = (req, res) => {
try {
currencyadapter.queryDeleteCurrency(req, function (err, data) {
let statusCode = data != null ? data.meta.code : 200;
if (err) statusCode = 500;
currencyadapter.sendResponse(statusCode, data, res);
});
} catch (err) {
apireshandler.meta.code = 502;
apireshandler.meta.message = "[deleteCurrency] : Currency controller, " + err.toString();
currencyadapter.sendResponse(502, apireshandler, res);
}
}

13
routes/currency.js Normal file
View File

@ -0,0 +1,13 @@
const express = require('express');
const currencycontroller = require('../controllers/currency');
const jwtauth = require('../middlewares/auth.js');
const router = express.Router();
router.get('/list', [jwtauth], currencycontroller.getCurrencyList);
router.get('/detail/:id', [jwtauth], currencycontroller.getCurrencyDetail);
router.get('/history/:id', [jwtauth], currencycontroller.getCurrencyHistory);
router.post('/create', [jwtauth], currencycontroller.createCurrency);
router.put('/update/:id', [jwtauth], currencycontroller.updateCurrency);
router.delete('/delete/:id', [jwtauth], currencycontroller.deleteCurrency);
module.exports = router;