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:
2026-03-19 20:37:37 -04:00
parent 72c2e0a0c7
commit 119d53c33a
4 changed files with 46 additions and 12 deletions

View File

@@ -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) {