implemented quick pay within bank

This commit is contained in:
2025-06-25 23:46:17 +05:30
parent d813784305
commit e3bd1657c0
14 changed files with 262 additions and 13 deletions

35
openapi.yml Normal file
View 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

View File

@@ -1,4 +1,4 @@
const { validateUser } = require('../services/auth.service');
const authService = require('../services/auth.service');
const { generateToken } = require('../util/jwt');
const { logger } = require('../util/logger');
@@ -12,9 +12,9 @@ async function login(req, res) {
}
try {
const user = await validateUser(customerNo, password);
const user = await authService.validateUser(customerNo, password);
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 });
} catch (err) {
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 };

View File

@@ -8,7 +8,6 @@ async function getDetails(customerNo) {
{ params: { stcustno: customerNo } }
);
const details = response.data;
logger.info(details, 'response from cbs');
const processedDetails = details.map((acc) => ({
...acc,
activeAccounts: details.length,
@@ -16,6 +15,7 @@ async function getDetails(customerNo) {
}));
return processedDetails;
} catch (error) {
logger.error('while fetching customer details', error);
throw new Error(
'API call failed: ' + (error.response?.data?.message || error.message)
);

View 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 };

View File

@@ -14,7 +14,7 @@ function auth(req, res, next) {
try {
const payload = verifyToken(token);
req.user = payload;
req.user = payload.customerNo;
next();
} catch (err) {
logger.error(err, 'error verifying token');

View File

@@ -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;

View File

@@ -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;

View File

@@ -2,11 +2,14 @@ const express = require('express');
const authRoute = require('./auth.route');
const detailsRoute = require('./customer_details.route');
const transactionRoute = require('./transactions.route');
const authenticate = require('../middlewares/auth.middleware');
const transferRoute = require('./transfer.route');
const router = express.Router();
router.use('/auth', authRoute);
router.use('/customer', detailsRoute);
router.use('/transactions', transactionRoute);
router.use('/customer', authenticate, detailsRoute);
router.use('/transactions/account/:accountNo', authenticate, transactionRoute);
router.use('/payment/transfer', authenticate, transferRoute);
module.exports = router;

View File

@@ -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;

View 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;

View File

@@ -1,5 +1,5 @@
const db = require('../config/db');
const { comparePassword } = require('../util/hash');
const { comparePassword, hashPassword } = require('../util/hash');
async function findUserByCustomerNo(customerNo) {
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;
}
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 };

View File

@@ -1,12 +1,15 @@
const jwt = require('jsonwebtoken');
const { jwtSecret } = require('../config/config');
const { logger } = require('./logger');
function generateToken(payload, expiresIn = '1h') {
return jwt.sign({ payload }, jwtSecret, { expiresIn });
function generateToken(customerNo, expiresIn = '1d') {
logger.info({ customerNo }, 'payload to encode');
return jwt.sign({ customerNo }, jwtSecret, { expiresIn });
}
function verifyToken(token) {
return jwt.verify(token, jwtSecret);
const payload = jwt.verify(token, jwtSecret);
return payload;
}
module.exports = {

View 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;

View 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;