const express = require('express'); const router = express.Router(); const { getDb } = require('../db/database'); const { standardizeError } = require('../middleware/errorFormatter'); const { calculateSnowball, calculateAvalanche } = require('../services/snowballService'); const DEBT_LIKE_CLAUSES = `( b.snowball_include = 1 OR ( COALESCE(b.snowball_exempt, 0) = 0 AND ( LOWER(c.name) LIKE '%credit%' OR LOWER(c.name) LIKE '%loan%' OR LOWER(c.name) LIKE '%mortgage%' OR LOWER(c.name) LIKE '%housing%' OR LOWER(c.name) LIKE '%debt%' ) ) )`; // GET /api/snowball — server-filtered debt bills, pre-sorted by snowball_order router.get('/', (req, res) => { const db = getDb(); const bills = db.prepare(` SELECT b.*, c.name AS category_name FROM bills b LEFT JOIN categories c ON b.category_id = c.id AND c.user_id = b.user_id WHERE b.user_id = ? AND b.active = 1 AND ${DEBT_LIKE_CLAUSES} ORDER BY CASE WHEN b.snowball_order IS NULL THEN 1 ELSE 0 END ASC, b.snowball_order ASC, CASE WHEN b.current_balance IS NULL THEN 1 ELSE 0 END ASC, b.current_balance ASC `).all(req.user.id); res.json(bills); }); // GET /api/snowball/settings — extra monthly payment for this user router.get('/settings', (req, res) => { const db = getDb(); const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(req.user.id); res.json({ extra_payment: user?.snowball_extra_payment ?? 0 }); }); // PATCH /api/snowball/settings — save extra monthly payment router.patch('/settings', (req, res) => { const { extra_payment } = req.body; let val = 0; if (extra_payment !== undefined && extra_payment !== null && extra_payment !== '') { val = parseFloat(extra_payment); if (!Number.isFinite(val) || val < 0) { return res.status(400).json(standardizeError( 'extra_payment must be a non-negative number', 'VALIDATION_ERROR', 'extra_payment' )); } } const db = getDb(); db.prepare('UPDATE users SET snowball_extra_payment = ? WHERE id = ?').run(val, req.user.id); res.json({ extra_payment: val }); }); // GET /api/snowball/projection — payoff timeline using the snowball math service router.get('/projection', (req, res) => { const db = getDb(); const bills = db.prepare(` SELECT b.*, c.name AS category_name FROM bills b LEFT JOIN categories c ON b.category_id = c.id AND c.user_id = b.user_id WHERE b.user_id = ? AND b.active = 1 AND ${DEBT_LIKE_CLAUSES} ORDER BY CASE WHEN b.snowball_order IS NULL THEN 1 ELSE 0 END ASC, b.snowball_order ASC, CASE WHEN b.current_balance IS NULL THEN 1 ELSE 0 END ASC, b.current_balance ASC `).all(req.user.id); const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(req.user.id); const extraPayment = user?.snowball_extra_payment ?? 0; const now = new Date(); const snowball = calculateSnowball(bills, extraPayment, now); const avalanche = calculateAvalanche(bills, extraPayment, now); res.json({ snowball, avalanche }); }); // PATCH /api/snowball/order — batch-save snowball_order positions router.patch('/order', (req, res) => { const items = req.body; if (!Array.isArray(items)) { return res.status(400).json(standardizeError('Request body must be an array', 'VALIDATION_ERROR')); } const db = getDb(); const userId = req.user.id; const update = db.prepare('UPDATE bills SET snowball_order = ? WHERE id = ? AND user_id = ?'); db.transaction((rows) => { for (const row of rows) { const id = parseInt(row.id, 10); const order = parseInt(row.snowball_order, 10); if (!Number.isInteger(id) || id <= 0) continue; if (!Number.isInteger(order) || order < 0) continue; update.run(order, id, userId); } })(items); res.json({ success: true }); }); module.exports = router;