BillTracker/routes/monthly-starting-amounts.js

147 lines
5.0 KiB
JavaScript
Raw Normal View History

2026-05-04 20:12:57 -05:00
const express = require('express');
const router = express.Router();
const { getDb } = require('../db/database');
const { getCycleRange } = require('../services/statusService');
function parseYearMonth(source) {
const now = new Date();
const year = parseInt(source.year || now.getFullYear(), 10);
const month = parseInt(source.month || now.getMonth() + 1, 10);
if (Number.isNaN(year) || year < 2000 || year > 2100) {
return { error: 'year must be a 4-digit integer between 2000 and 2100' };
}
if (Number.isNaN(month) || month < 1 || month > 12) {
return { error: 'month must be an integer between 1 and 12' };
}
return { year, month };
}
function money(value) {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
function getStartingAmounts(db, userId, year, month) {
const row = db.prepare(`
SELECT first_amount, fifteenth_amount, other_amount
FROM monthly_starting_amounts
WHERE user_id = ? AND year = ? AND month = ?
`).get(userId, year, month);
return {
first_amount: money(row?.first_amount || 0),
fifteenth_amount: money(row?.fifteenth_amount || 0),
other_amount: money(row?.other_amount || 0),
};
}
function calculatePaidDeductions(db, userId, year, month) {
const { start, end } = getCycleRange(year, month);
// Paid from first bucket: bills with due_day 1-14
const firstPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
JOIN bills b ON b.id = p.bill_id
WHERE b.user_id = ?
AND p.paid_date BETWEEN ? AND ?
AND p.deleted_at IS NULL
AND b.due_day BETWEEN 1 AND 14
`).get(userId, start, end);
// Paid from fifteenth bucket: bills with due_day 15-31
const fifteenthPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
JOIN bills b ON b.id = p.bill_id
WHERE b.user_id = ?
AND p.paid_date BETWEEN ? AND ?
AND p.deleted_at IS NULL
AND b.due_day BETWEEN 15 AND 31
`).get(userId, start, end);
const totalPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
JOIN bills b ON b.id = p.bill_id
WHERE b.user_id = ?
AND p.paid_date BETWEEN ? AND ?
AND p.deleted_at IS NULL
`).get(userId, start, end);
return {
paid_from_first: money(firstPaid.paid),
paid_from_fifteenth: money(fifteenthPaid.paid),
paid_total: money(totalPaid.paid),
};
}
function buildStartingAmountsResponse(db, userId, year, month) {
const amounts = getStartingAmounts(db, userId, year, month);
const paid = calculatePaidDeductions(db, userId, year, month);
const combined_amount = amounts.first_amount + amounts.fifteenth_amount + amounts.other_amount;
const paid_total = paid.paid_total;
return {
year,
month,
first_amount: amounts.first_amount,
fifteenth_amount: amounts.fifteenth_amount,
other_amount: amounts.other_amount,
combined_amount,
paid_from_first: paid.paid_from_first,
paid_from_fifteenth: paid.paid_from_fifteenth,
paid_total,
first_remaining: amounts.first_amount - paid.paid_from_first,
fifteenth_remaining: amounts.fifteenth_amount - paid.paid_from_fifteenth,
other_remaining: amounts.other_amount,
combined_remaining: combined_amount - paid_total,
};
}
router.get('/', (req, res) => {
const parsed = parseYearMonth(req.query);
if (parsed.error) return res.status(400).json({ error: parsed.error });
const db = getDb();
res.json(buildStartingAmountsResponse(db, req.user.id, parsed.year, parsed.month));
});
router.put('/', (req, res) => {
const parsed = parseYearMonth(req.body || {});
if (parsed.error) return res.status(400).json({ error: parsed.error });
const firstAmount = Number(req.body?.first_amount);
if (!Number.isFinite(firstAmount) || firstAmount < 0 || firstAmount > 1000000000) {
return res.status(400).json({ error: 'first_amount must be a number between 0 and 1000000000' });
}
const fifteenthAmount = Number(req.body?.fifteenth_amount);
if (!Number.isFinite(fifteenthAmount) || fifteenthAmount < 0 || fifteenthAmount > 1000000000) {
return res.status(400).json({ error: 'fifteenth_amount must be a number between 0 and 1000000000' });
}
const otherAmount = Number(req.body?.other_amount);
if (!Number.isFinite(otherAmount) || otherAmount < 0 || otherAmount > 1000000000) {
return res.status(400).json({ error: 'other_amount must be a number between 0 and 1000000000' });
}
const db = getDb();
db.prepare(`
INSERT INTO monthly_starting_amounts (user_id, year, month, first_amount, fifteenth_amount, other_amount, updated_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(user_id, year, month) DO UPDATE SET
first_amount = excluded.first_amount,
fifteenth_amount = excluded.fifteenth_amount,
other_amount = excluded.other_amount,
updated_at = datetime('now')
`).run(req.user.id, parsed.year, parsed.month, firstAmount, fifteenthAmount, otherAmount);
res.json(buildStartingAmountsResponse(db, req.user.id, parsed.year, parsed.month));
});
module.exports = router;