const express = require('express'); const router = require('express').Router(); const { getDb } = require('../db/database'); const LIVE = 'deleted_at IS NULL'; // filter for non-deleted payments // GET /api/payments?bill_id=&year=&month= router.get('/', (req, res) => { const db = getDb(); const { bill_id, year, month } = req.query; // Validate year/month when provided if ((year || month) && !(year && month)) { return res.status(400).json({ error: 'Both year and month are required when filtering by date' }); } let y, m; if (year && month) { y = parseInt(year, 10); m = parseInt(month, 10); if (!Number.isInteger(y) || y < 2000 || y > 2100) { return res.status(400).json({ error: 'year must be a 4-digit integer between 2000 and 2100' }); } if (!Number.isInteger(m) || m < 1 || m > 12) { return res.status(400).json({ error: 'month must be an integer between 1 and 12' }); } } let query = `SELECT p.* FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.${LIVE} AND b.user_id = ?`; const params = [req.user.id]; if (bill_id) { query += ' AND p.bill_id = ?'; params.push(parseInt(bill_id, 10)); } if (y && m) { const yStr = String(y); const mStr = String(m).padStart(2, '0'); const daysInMonth = new Date(y, m, 0).getDate(); const endDay = String(daysInMonth).padStart(2, '0'); query += ' AND p.paid_date BETWEEN ? AND ?'; params.push(`${yStr}-${mStr}-01`, `${yStr}-${mStr}-${endDay}`); } query += ' ORDER BY p.paid_date DESC'; res.json(db.prepare(query).all(...params)); }); // 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 = ?`).get(req.params.id, req.user.id); if (!payment) return res.status(404).json({ error: 'Payment not found' }); res.json(payment); }); // POST /api/payments — create single payment router.post('/', (req, res) => { const db = getDb(); const { bill_id, amount, paid_date, method, notes } = req.body; if (!bill_id || amount == null || !paid_date) return res.status(400).json({ error: 'bill_id, amount, and paid_date are required' }); const parsedAmount = parseFloat(amount); if (isNaN(parsedAmount) || parsedAmount <= 0) return res.status(400).json({ error: 'amount must be a positive number' }); if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ?').get(bill_id, req.user.id)) return res.status(404).json({ error: 'Bill not found' }); const result = db.prepare( 'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)' ).run(bill_id, parsedAmount, paid_date, method || null, notes || null); res.status(201).json(db.prepare('SELECT * FROM payments WHERE id = ?').get(result.lastInsertRowid)); }); // POST /api/payments/quick — pay a bill (expected amount, today) router.post('/quick', (req, res) => { const db = getDb(); const { bill_id, amount, paid_date, method, notes } = req.body; if (!bill_id) return res.status(400).json({ error: 'bill_id is required' }); const bill = db.prepare('SELECT * FROM bills WHERE id = ? AND user_id = ?').get(bill_id, req.user.id); if (!bill) return res.status(404).json({ error: 'Bill not found' }); const payAmount = amount != null ? parseFloat(amount) : bill.expected_amount; if (isNaN(payAmount) || payAmount <= 0) return res.status(400).json({ error: 'amount must be a positive number' }); const payDate = paid_date || new Date().toISOString().slice(0, 10); const result = db.prepare( 'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)' ).run(bill_id, payAmount, payDate, method || null, notes || null); if (bill.autopay_enabled) { db.prepare("UPDATE bills SET autodraft_status='confirmed', updated_at=datetime('now') WHERE id=?").run(bill_id); } res.status(201).json(db.prepare('SELECT * FROM payments WHERE id = ?').get(result.lastInsertRowid)); }); // POST /api/payments/bulk — record multiple payments in one request router.post('/bulk', (req, res) => { const db = getDb(); const items = req.body; if (!Array.isArray(items) || items.length === 0) return res.status(400).json({ error: 'Body must be a non-empty array of payments' }); const insert = db.prepare( 'INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?)' ); const created = []; const errors = []; const runBulk = db.transaction(() => { for (const item of items) { const { bill_id, amount, paid_date, method, notes } = item; if (!bill_id || amount == null || !paid_date) { errors.push({ item, error: 'bill_id, amount, and paid_date are required' }); continue; } const parsedAmt = parseFloat(amount); if (isNaN(parsedAmt) || parsedAmt <= 0) { errors.push({ item, error: 'amount must be a positive number' }); continue; } if (!db.prepare('SELECT id FROM bills WHERE id = ? AND user_id = ?').get(bill_id, req.user.id)) { errors.push({ item, error: `Bill ${bill_id} not found` }); continue; } const r = insert.run(bill_id, parsedAmt, paid_date, method || null, notes || null); created.push(db.prepare('SELECT * FROM payments WHERE id = ?').get(r.lastInsertRowid)); } }); runBulk(); res.status(201).json({ created, errors }); }); // 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 = ?`).get(req.params.id, req.user.id); if (!existing) return res.status(404).json({ error: 'Payment not found' }); const { amount, paid_date, method, notes } = req.body; db.prepare(` UPDATE payments SET amount = ?, paid_date = ?, method = ?, notes = ?, updated_at = datetime('now') WHERE id = ? `).run( amount != null ? parseFloat(amount) : existing.amount, paid_date ?? existing.paid_date, method !== undefined ? (method || null) : existing.method, notes !== undefined ? (notes || null) : existing.notes, req.params.id, ); res.json(db.prepare('SELECT * FROM payments WHERE id = ?').get(req.params.id)); }); // DELETE /api/payments/:id — soft delete (sets deleted_at) router.delete('/:id', (req, res) => { const db = getDb(); const payment = db.prepare(`SELECT p.id FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.${LIVE} AND b.user_id = ?`).get(req.params.id, req.user.id); if (!payment) return res.status(404).json({ error: 'Payment not found' }); db.prepare("UPDATE payments SET deleted_at = datetime('now') WHERE id = ?").run(req.params.id); res.json({ success: true }); }); // POST /api/payments/:id/restore — undo soft delete router.post('/:id/restore', (req, res) => { const db = getDb(); const payment = db.prepare('SELECT p.id FROM payments p JOIN bills b ON b.id = p.bill_id WHERE p.id = ? AND p.deleted_at IS NOT NULL AND b.user_id = ?').get(req.params.id, req.user.id); if (!payment) return res.status(404).json({ error: 'Deleted payment not found' }); db.prepare('UPDATE payments SET deleted_at = NULL WHERE id = ?').run(req.params.id); res.json(db.prepare('SELECT * FROM payments WHERE id = ?').get(req.params.id)); }); module.exports = router;