implemented quick pay within bank
This commit is contained in:
35
openapi.yml
Normal file
35
openapi.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: kmobile
|
||||||
|
description: backend for mobile banking application
|
||||||
|
version: 1.0.0
|
||||||
|
servers:
|
||||||
|
- url: http://api.example.com
|
||||||
|
paths:
|
||||||
|
/auth/login:
|
||||||
|
post:
|
||||||
|
summary: Login a user
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
required: [username, password]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful login
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
'401':
|
||||||
|
description: invalid credentials
|
@@ -1,4 +1,4 @@
|
|||||||
const { validateUser } = require('../services/auth.service');
|
const authService = require('../services/auth.service');
|
||||||
const { generateToken } = require('../util/jwt');
|
const { generateToken } = require('../util/jwt');
|
||||||
const { logger } = require('../util/logger');
|
const { logger } = require('../util/logger');
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@ async function login(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await validateUser(customerNo, password);
|
const user = await authService.validateUser(customerNo, password);
|
||||||
if (!user) return res.status(401).json({ error: 'invalid credentials' });
|
if (!user) return res.status(401).json({ error: 'invalid credentials' });
|
||||||
const token = generateToken(user.customer_no);
|
const token = generateToken(user.customer_no, '1d');
|
||||||
res.json({ token });
|
res.json({ token });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, 'login failed');
|
logger.error(err, 'login failed');
|
||||||
@@ -22,4 +22,38 @@ async function login(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { login };
|
async function tpin(req, res) {
|
||||||
|
const customerNo = req.user;
|
||||||
|
try {
|
||||||
|
const user = await authService.findUserByCustomerNo(customerNo);
|
||||||
|
if (!user) return res.status(404).json({ message: 'USER_NOT_FOUND' });
|
||||||
|
if (!user.tpin) {
|
||||||
|
return res.json({ tpinSet: false });
|
||||||
|
} else {
|
||||||
|
return res.json({ tpinSet: true });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, 'error occured while checking tpin');
|
||||||
|
res.status(500).json({ error: 'something went wrong' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setTpin(req, res) {
|
||||||
|
const customerNo = req.user;
|
||||||
|
try {
|
||||||
|
const user = await authService.findUserByCustomerNo(customerNo);
|
||||||
|
if (!user) return res.status(404).json({ error: 'USER_NOT_FOUND' });
|
||||||
|
if (user.tpin)
|
||||||
|
return res.status(400).json({ error: 'USER_ALREADY_HAS_A_TPIN' });
|
||||||
|
const { tpin } = req.body;
|
||||||
|
if (!/^\d{6}$/.test(tpin))
|
||||||
|
return res.status(400).json({ error: 'INVALID_TPIN_FORMAT' });
|
||||||
|
authService.setTpin(customerNo, tpin);
|
||||||
|
return res.json({ message: 'TPIN_SET' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return res.status(500).json({ error: 'SOMETHING_WENT_WRONG' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { login, tpin, setTpin };
|
||||||
|
@@ -8,7 +8,6 @@ async function getDetails(customerNo) {
|
|||||||
{ params: { stcustno: customerNo } }
|
{ params: { stcustno: customerNo } }
|
||||||
);
|
);
|
||||||
const details = response.data;
|
const details = response.data;
|
||||||
logger.info(details, 'response from cbs');
|
|
||||||
const processedDetails = details.map((acc) => ({
|
const processedDetails = details.map((acc) => ({
|
||||||
...acc,
|
...acc,
|
||||||
activeAccounts: details.length,
|
activeAccounts: details.length,
|
||||||
@@ -16,6 +15,7 @@ async function getDetails(customerNo) {
|
|||||||
}));
|
}));
|
||||||
return processedDetails;
|
return processedDetails;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error('while fetching customer details', error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'API call failed: ' + (error.response?.data?.message || error.message)
|
'API call failed: ' + (error.response?.data?.message || error.message)
|
||||||
);
|
);
|
||||||
|
29
src/controllers/transfer.controller.js
Normal file
29
src/controllers/transfer.controller.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
async function transfer(
|
||||||
|
fromAccountNo,
|
||||||
|
toAccountNo,
|
||||||
|
toAccountType,
|
||||||
|
amount,
|
||||||
|
narration = 'tranfer from mobile'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'http://localhost:8689/kccb/Interbankfundtranfer',
|
||||||
|
{
|
||||||
|
fromAccountNo,
|
||||||
|
toAccountNo,
|
||||||
|
toAccountType,
|
||||||
|
amount,
|
||||||
|
narration,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
'API call failed: ' + (error.response?.data?.message || error.message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { transfer };
|
@@ -14,7 +14,7 @@ function auth(req, res, next) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = verifyToken(token);
|
const payload = verifyToken(token);
|
||||||
req.user = payload;
|
req.user = payload.customerNo;
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, 'error verifying token');
|
logger.error(err, 'error verifying token');
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
const authController = require('../controllers/auth.controller');
|
||||||
|
const authenticate = require('../middlewares/auth.middleware');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/login', authController.login);
|
||||||
|
router.get('/tpin', authenticate, authController.tpin);
|
||||||
|
router.post('/tpin', authenticate, authController.setTpin);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
const customerController = require('../controllers/customer_details.controller');
|
||||||
|
const { logger } = require('../util/logger');
|
||||||
|
|
||||||
|
const customerRoute = async (req, res) => {
|
||||||
|
const customerNo = req.user;
|
||||||
|
try {
|
||||||
|
const details = await customerController.getDetails(customerNo);
|
||||||
|
return res.json(details);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
return res.status(500).json({ message: 'INTERNAL_SERVER_ERROR' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = customerRoute;
|
||||||
|
@@ -2,11 +2,14 @@ const express = require('express');
|
|||||||
const authRoute = require('./auth.route');
|
const authRoute = require('./auth.route');
|
||||||
const detailsRoute = require('./customer_details.route');
|
const detailsRoute = require('./customer_details.route');
|
||||||
const transactionRoute = require('./transactions.route');
|
const transactionRoute = require('./transactions.route');
|
||||||
|
const authenticate = require('../middlewares/auth.middleware');
|
||||||
|
const transferRoute = require('./transfer.route');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use('/auth', authRoute);
|
router.use('/auth', authRoute);
|
||||||
router.use('/customer', detailsRoute);
|
router.use('/customer', authenticate, detailsRoute);
|
||||||
router.use('/transactions', transactionRoute);
|
router.use('/transactions/account/:accountNo', authenticate, transactionRoute);
|
||||||
|
router.use('/payment/transfer', authenticate, transferRoute);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
const transactionsController = require('../controllers/transactions.controller');
|
||||||
|
const { logger } = require('../util/logger');
|
||||||
|
|
||||||
|
const transactionsRoute = async (req, res) => {
|
||||||
|
const accountNo = req.params.accountNo;
|
||||||
|
try {
|
||||||
|
const data = await transactionsController.getLastTen(accountNo);
|
||||||
|
return res.json(data);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('error retriving last 10 txns', error);
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ message: 'error occured while fetching transactions' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = transactionsRoute;
|
||||||
|
45
src/routes/transfer.route.js
Normal file
45
src/routes/transfer.route.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const transferController = require('../controllers/transfer.controller');
|
||||||
|
const { logger } = require('../util/logger');
|
||||||
|
const express = require('express');
|
||||||
|
const tpinValidator = require('../validators/tpin.validator');
|
||||||
|
const transferValidator = require('../validators/transfer.validator');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
router.use(tpinValidator, transferValidator);
|
||||||
|
|
||||||
|
const transferRoute = async (req, res) => {
|
||||||
|
const { fromAccount, toAccount, toAccountType, amount } = req.body;
|
||||||
|
try {
|
||||||
|
const result = await transferController.transfer(
|
||||||
|
fromAccount,
|
||||||
|
toAccount,
|
||||||
|
toAccountType,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status === 'O.K.') {
|
||||||
|
return res.json({ message: 'TRANSACTION_SUCCESS' });
|
||||||
|
} else if (result.status.includes('INVALID CHECK DIGIT')) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'INVALID_ACCOUNT_NO', status: result.status });
|
||||||
|
} else if (
|
||||||
|
result.status.includes('CLEARED BAL/FUNDS/DP NOT AVAILABLE.CARE')
|
||||||
|
) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'INSUFFICIENT_BALANCE', status: result.status });
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: 'PROBLEM_TRANSFERRING_FUNDS', status: result.status });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, 'error occured while doing transfer');
|
||||||
|
return res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/', transferRoute);
|
||||||
|
|
||||||
|
module.exports = router;
|
@@ -1,5 +1,5 @@
|
|||||||
const db = require('../config/db');
|
const db = require('../config/db');
|
||||||
const { comparePassword } = require('../util/hash');
|
const { comparePassword, hashPassword } = require('../util/hash');
|
||||||
|
|
||||||
async function findUserByCustomerNo(customerNo) {
|
async function findUserByCustomerNo(customerNo) {
|
||||||
const result = await db.query('SELECT * FROM users WHERE customer_no = $1', [
|
const result = await db.query('SELECT * FROM users WHERE customer_no = $1', [
|
||||||
@@ -15,4 +15,25 @@ async function validateUser(customerNo, password) {
|
|||||||
return isMatch ? user : null;
|
return isMatch ? user : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { validateUser };
|
async function validateTpin(customerNo, tpin) {
|
||||||
|
const user = await findUserByCustomerNo(customerNo);
|
||||||
|
if (!user?.tpin) return null;
|
||||||
|
const isMatch = await comparePassword(tpin, user.tpin);
|
||||||
|
return isMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setTpin(customerNo, tpin) {
|
||||||
|
const hashedTpin = await hashPassword(tpin);
|
||||||
|
try {
|
||||||
|
await db.query('UPDATE users SET tpin = $1 WHERE customer_no = $2', [
|
||||||
|
hashedTpin,
|
||||||
|
customerNo,
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`error occured while while setting new tpin ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateUser, findUserByCustomerNo, setTpin, validateTpin };
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { jwtSecret } = require('../config/config');
|
const { jwtSecret } = require('../config/config');
|
||||||
|
const { logger } = require('./logger');
|
||||||
|
|
||||||
function generateToken(payload, expiresIn = '1h') {
|
function generateToken(customerNo, expiresIn = '1d') {
|
||||||
return jwt.sign({ payload }, jwtSecret, { expiresIn });
|
logger.info({ customerNo }, 'payload to encode');
|
||||||
|
return jwt.sign({ customerNo }, jwtSecret, { expiresIn });
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyToken(token) {
|
function verifyToken(token) {
|
||||||
return jwt.verify(token, jwtSecret);
|
const payload = jwt.verify(token, jwtSecret);
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
16
src/validators/tpin.validator.js
Normal file
16
src/validators/tpin.validator.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const authService = require('../services/auth.service');
|
||||||
|
|
||||||
|
const tpinValidator = async (req, res, next) => {
|
||||||
|
const customerNo = req.user;
|
||||||
|
const { tpin } = req.body;
|
||||||
|
|
||||||
|
if (!tpin) {
|
||||||
|
return res.status(400).json({ error: 'BAD_REQUEST' });
|
||||||
|
}
|
||||||
|
const valid = await authService.validateTpin(customerNo, tpin);
|
||||||
|
if (valid === null) res.status(400).json({ error: 'TPIN_NOT_SET_FOR_USER' });
|
||||||
|
if (!valid) return res.status(401).json({ error: 'INCORRECT_TPIN' });
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = tpinValidator;
|
20
src/validators/transfer.validator.js
Normal file
20
src/validators/transfer.validator.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const transferValidator = async (req, res, next) => {
|
||||||
|
const { fromAccount, toAccount, toAccountType, amount } = req.body;
|
||||||
|
|
||||||
|
const accountTypes = ['SB', 'LN'];
|
||||||
|
if (!fromAccount || fromAccount.length != 11) {
|
||||||
|
return res.status(400).json({ error: 'INVALID_ACCOUNT_NUMBER_FORMAT' });
|
||||||
|
}
|
||||||
|
if (!toAccount || toAccount.length != 11) {
|
||||||
|
return res.status(400).json({ error: 'INVALID_ACCOUNT_NUMBER_FORMAT' });
|
||||||
|
}
|
||||||
|
if (!accountTypes || !accountTypes.includes(toAccountType)) {
|
||||||
|
return res.status(400).json({ error: 'INVALID_ACCOUNT_TYPE' });
|
||||||
|
}
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
return res.status(400).json({ error: 'INVALID_AMOUNT' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = transferValidator;
|
Reference in New Issue
Block a user