From 26d22355fab253884591d8caa906184a008dee20 Mon Sep 17 00:00:00 2001 From: Rizki Date: Mon, 23 Mar 2026 20:52:29 +0700 Subject: [PATCH] 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 --- adapter/currencyadapter.js | 311 +++++++++++++++++++++++++++++++++++++ config/dbproc.js | 3 +- controllers/currency.js | 89 +++++++++++ routes/currency.js | 13 ++ 4 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 adapter/currencyadapter.js create mode 100644 controllers/currency.js create mode 100644 routes/currency.js diff --git a/adapter/currencyadapter.js b/adapter/currencyadapter.js new file mode 100644 index 0000000..2234bb8 --- /dev/null +++ b/adapter/currencyadapter.js @@ -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; diff --git a/config/dbproc.js b/config/dbproc.js index 8d27732..d3986ea 100644 --- a/config/dbproc.js +++ b/config/dbproc.js @@ -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) { diff --git a/controllers/currency.js b/controllers/currency.js new file mode 100644 index 0000000..5e72a7a --- /dev/null +++ b/controllers/currency.js @@ -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); + } +} diff --git a/routes/currency.js b/routes/currency.js new file mode 100644 index 0000000..bbe0a28 --- /dev/null +++ b/routes/currency.js @@ -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;