diff --git a/routes/payments.js b/routes/payments.js index 0c60ce8..250f6be 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -6,7 +6,11 @@ const { computeBalanceDelta } = require('../services/billsService'); const { validatePaymentInput } = require('../services/paymentValidation'); const { getCycleRange, resolveDueDate } = require('../services/statusService'); -const LIVE = 'deleted_at IS NULL'; // filter for non-deleted payments +// SQL_NOT_DELETED is a compile-time constant SQL fragment, never user-supplied. +// It cannot be a bind parameter (SQL fragments are not parameterisable — only +// values are). Interpolating a hardcoded constant like this is safe by +// construction; do NOT replace this pattern with dynamic/user-controlled input. +const SQL_NOT_DELETED = 'deleted_at IS NULL'; const TRANSACTION_MATCH_SOURCE = 'transaction_match'; function isTransactionLinkedPayment(payment) { @@ -83,7 +87,7 @@ router.get('/', (req, res) => { } } - let query = `SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.${LIVE} AND b.user_id = ? AND b.deleted_at IS NULL`; + let query = `SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`; const params = [req.user.id]; if (bill_id) { query += ' AND p.bill_id = ?'; params.push(parseInt(bill_id, 10)); } @@ -104,7 +108,7 @@ router.get('/', (req, res) => { // GET /api/payments/:id router.get('/:id', (req, res) => { const db = getDb(); - const payment = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${LIVE} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); + const payment = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); if (!payment) return res.status(404).json(standardizeError('Payment not found', 'NOT_FOUND', 'id')); res.json(payment); }); @@ -319,7 +323,7 @@ router.post('/bulk', (req, res) => { AND p.bill_id = ? AND p.paid_date = ? AND p.amount = ? - AND p.${LIVE}` + AND p.${SQL_NOT_DELETED}` ); const created = []; @@ -360,7 +364,7 @@ router.post('/bulk', (req, res) => { // PUT /api/payments/:id router.put('/:id', (req, res) => { const db = getDb(); - const existing = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${LIVE} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); + const existing = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); if (!existing) return res.status(404).json(standardizeError('Payment not found', 'NOT_FOUND', 'id')); if (isTransactionLinkedPayment(existing)) return rejectTransactionLinkedPayment(res); @@ -413,13 +417,13 @@ router.put('/:id', (req, res) => { req.user.id, ); - res.json(db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${LIVE} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id)); + res.json(db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id)); }); // DELETE /api/payments/:id — soft delete (sets deleted_at) router.delete('/:id', (req, res) => { const db = getDb(); - const payment = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${LIVE} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); + const payment = db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id); if (!payment) return res.status(404).json(standardizeError('Payment not found', 'NOT_FOUND', 'id')); if (isTransactionLinkedPayment(payment)) return rejectTransactionLinkedPayment(res); @@ -453,7 +457,7 @@ router.post('/:id/restore', (req, res) => { } db.prepare('UPDATE payments SET deleted_at = NULL WHERE id = ? AND bill_id IN (SELECT id FROM bills WHERE user_id = ? AND deleted_at IS NULL)').run(req.params.id, req.user.id); - res.json(db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${LIVE} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id)); + res.json(db.prepare(`SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${SQL_NOT_DELETED} AND b.user_id = ? AND b.deleted_at IS NULL`).get(req.params.id, req.user.id)); }); module.exports = router;