diff --git a/routes/wallet.ts b/routes/wallet.ts index 15629bb437d..5561c7cd43d 100644 --- a/routes/wallet.ts +++ b/routes/wallet.ts @@ -7,6 +7,24 @@ import { type Request, type Response, type NextFunction } from 'express' import { WalletModel } from '../models/wallet' import { CardModel } from '../models/card' +const MIN_TOP_UP_AMOUNT = 10 +const MAX_TOP_UP_AMOUNT = 1000 + +function parseTopUpAmount (value: unknown): number | null { + const amount = typeof value === 'number' ? value : (typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN) + if (!Number.isInteger(amount) || amount < MIN_TOP_UP_AMOUNT || amount > MAX_TOP_UP_AMOUNT) { + return null + } + return amount +} + +function isCardExpired (card: CardModel): boolean { + const now = new Date() + const year = now.getFullYear() + const month = now.getMonth() + 1 + return card.expYear < year || (card.expYear === year && card.expMonth < month) +} + export function getWalletBalance () { return async (req: Request, res: Response, next: NextFunction) => { const wallet = await WalletModel.findOne({ where: { UserId: req.body.UserId } }) @@ -20,12 +38,17 @@ export function getWalletBalance () { export function addWalletBalance () { return async (req: Request, res: Response, next: NextFunction) => { - const cardId = req.body.paymentId - const card = cardId ? await CardModel.findOne({ where: { id: cardId, UserId: req.body.UserId } }) : null - if (card != null) { + const amount = parseTopUpAmount(req.body.balance) + if (amount === null) { + res.status(400).json({ status: 'error', message: `Top-up amount must be a whole number between ${MIN_TOP_UP_AMOUNT} and ${MAX_TOP_UP_AMOUNT}.` }) + return + } + const cardId = Number(req.body.paymentId) + const card = Number.isInteger(cardId) && cardId > 0 ? await CardModel.findOne({ where: { id: cardId, UserId: req.body.UserId } }) : null + if (card != null && !isCardExpired(card)) { try { - await WalletModel.increment({ balance: req.body.balance }, { where: { UserId: req.body.UserId } }) - res.status(200).json({ status: 'success', data: req.body.balance }) + await WalletModel.increment({ balance: amount }, { where: { UserId: req.body.UserId } }) + res.status(200).json({ status: 'success', data: amount }) } catch { res.status(404).json({ status: 'error' }) } diff --git a/test/api/wallet.test.ts b/test/api/wallet.test.ts index a3ff16d452a..e576e2a9dca 100644 --- a/test/api/wallet.test.ts +++ b/test/api/wallet.test.ts @@ -58,6 +58,43 @@ void describe('/api/Wallets', () => { assert.equal(balanceRes.body.data, 210) }) + void it('PUT charge wallet with negative amount is rejected', async () => { + const res = await request(app) + .put('/rest/wallet/balance') + .set(authHeader) + .send({ balance: -500, paymentId: 2 }) + assert.equal(res.status, 400) + }) + + void it('PUT charge wallet with amount above maximum is rejected', async () => { + const res = await request(app) + .put('/rest/wallet/balance') + .set(authHeader) + .send({ balance: 999999, paymentId: 2 }) + assert.equal(res.status, 400) + + const balanceRes = await request(app) + .get('/rest/wallet/balance') + .set(authHeader) + assert.equal(balanceRes.body.data, 210) + }) + + void it('PUT charge wallet with non-numeric amount is rejected', async () => { + const res = await request(app) + .put('/rest/wallet/balance') + .set(authHeader) + .send({ balance: 'lots', paymentId: 2 }) + assert.equal(res.status, 400) + }) + + void it('PUT charge wallet with non-integer amount is rejected', async () => { + const res = await request(app) + .put('/rest/wallet/balance') + .set(authHeader) + .send({ balance: 10.5, paymentId: 2 }) + assert.equal(res.status, 400) + }) + void it('PUT charge wallet from foreign credit card is forbidden', async () => { const res = await request(app) .put('/rest/wallet/balance')