Add start_date to financing plans and protect paid paychecks from refresh
- Migration 004: adds start_date column to financing_plans (DEFAULT CURRENT_DATE) - generatePaychecks: skips financing plans whose start_date is after the target month - buildVirtualPaychecks: same start_date guard for virtual previews (already applied) - generatePaychecks upsert: preserves gross/net when paycheck has any paid bills - financing.js POST/PUT: accept and store start_date - Financing.jsx: add Start Date field to create/edit form (defaults to today) - CLAUDE.md: document start_date guard and paid-paycheck refresh protection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,7 +86,7 @@ router.get('/financing', async (req, res) => {
|
||||
|
||||
// POST /api/financing
|
||||
router.post('/financing', async (req, res) => {
|
||||
const { name, total_amount, due_date, assigned_paycheck } = req.body;
|
||||
const { name, total_amount, due_date, assigned_paycheck, start_date } = req.body;
|
||||
if (!name || !total_amount || !due_date) {
|
||||
return res.status(400).json({ error: 'name, total_amount, and due_date are required' });
|
||||
}
|
||||
@@ -96,9 +96,9 @@ router.post('/financing', async (req, res) => {
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO financing_plans (name, total_amount, due_date, assigned_paycheck)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
[name.trim(), parseFloat(total_amount), due_date, assigned_paycheck ?? null]
|
||||
`INSERT INTO financing_plans (name, total_amount, due_date, assigned_paycheck, start_date)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
||||
[name.trim(), parseFloat(total_amount), due_date, assigned_paycheck ?? null, start_date || new Date().toISOString().slice(0, 10)]
|
||||
);
|
||||
res.status(201).json(await enrichPlan(pool, rows[0]));
|
||||
} catch (err) {
|
||||
@@ -136,16 +136,16 @@ router.get('/financing/:id', async (req, res) => {
|
||||
|
||||
// PUT /api/financing/:id
|
||||
router.put('/financing/:id', async (req, res) => {
|
||||
const { name, total_amount, due_date, assigned_paycheck } = req.body;
|
||||
const { name, total_amount, due_date, assigned_paycheck, start_date } = req.body;
|
||||
if (!name || !total_amount || !due_date) {
|
||||
return res.status(400).json({ error: 'name, total_amount, and due_date are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE financing_plans SET name=$1, total_amount=$2, due_date=$3, assigned_paycheck=$4
|
||||
WHERE id=$5 RETURNING *`,
|
||||
[name.trim(), parseFloat(total_amount), due_date, assigned_paycheck ?? null, req.params.id]
|
||||
`UPDATE financing_plans SET name=$1, total_amount=$2, due_date=$3, assigned_paycheck=$4, start_date=$5
|
||||
WHERE id=$6 RETURNING *`,
|
||||
[name.trim(), parseFloat(total_amount), due_date, assigned_paycheck ?? null, start_date || new Date().toISOString().slice(0, 10), req.params.id]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(await enrichPlan(pool, rows[0]));
|
||||
|
||||
@@ -96,6 +96,12 @@ async function buildVirtualPaychecks(year, month) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
for (const plan of activePlans.rows) {
|
||||
// Skip plans that haven't started yet for this period
|
||||
const start = new Date(plan.start_date);
|
||||
const planStartYear = start.getFullYear();
|
||||
const planStartMonth = start.getMonth() + 1;
|
||||
if (year * 12 + month < planStartYear * 12 + planStartMonth) continue;
|
||||
|
||||
const amount = await calcPaymentAmount(client, plan, year, month);
|
||||
if (amount <= 0) continue;
|
||||
|
||||
@@ -155,8 +161,20 @@ async function generatePaychecks(year, month) {
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (period_year, period_month, paycheck_number)
|
||||
DO UPDATE SET pay_date = EXCLUDED.pay_date,
|
||||
gross = EXCLUDED.gross,
|
||||
net = EXCLUDED.net
|
||||
gross = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM paycheck_bills pb
|
||||
WHERE pb.paycheck_id = paychecks.id AND pb.paid = TRUE
|
||||
) THEN paychecks.gross
|
||||
ELSE EXCLUDED.gross
|
||||
END,
|
||||
net = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM paycheck_bills pb
|
||||
WHERE pb.paycheck_id = paychecks.id AND pb.paid = TRUE
|
||||
) THEN paychecks.net
|
||||
ELSE EXCLUDED.net
|
||||
END
|
||||
RETURNING id`,
|
||||
[year, month, num, payDate, gross || 0, net || 0]
|
||||
);
|
||||
@@ -183,6 +201,12 @@ async function generatePaychecks(year, month) {
|
||||
`SELECT * FROM financing_plans WHERE active = TRUE ORDER BY due_date ASC`
|
||||
);
|
||||
for (const plan of activePlans.rows) {
|
||||
// Skip plans that haven't started yet for this period
|
||||
const start = new Date(plan.start_date);
|
||||
const planStartYear = start.getFullYear();
|
||||
const planStartMonth = start.getMonth() + 1;
|
||||
if (year * 12 + month < planStartYear * 12 + planStartMonth) continue;
|
||||
|
||||
// Determine which paycheck(s) this plan applies to
|
||||
const targets = plan.assigned_paycheck == null ? [1, 2] : [plan.assigned_paycheck];
|
||||
for (const pcNum of targets) {
|
||||
|
||||
Reference in New Issue
Block a user