From 119d53c33a31d622a2d7d1c40121b292aff5b23f Mon Sep 17 00:00:00 2001 From: Christian Hood Date: Thu, 19 Mar 2026 20:37:37 -0400 Subject: [PATCH] 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 --- CLAUDE.md | 4 ++-- client/src/pages/Financing.jsx | 10 ++++++++++ server/src/routes/financing.js | 16 ++++++++-------- server/src/routes/paychecks.js | 28 ++++++++++++++++++++++++++-- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a8e000e..07d7e27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,8 +61,8 @@ The default route `/` renders the paycheck-centric main view (`client/src/pages/ **Variable amount bills:** Bills with `variable_amount = true` require the amount to be entered each month in the paycheck view (stored as `amount_override` on `paycheck_bills`). The bill's `amount` field serves as an optional typical/estimated value. -**Lazy paycheck generation:** `GET /api/paychecks` returns virtual (unsaved) data with `id: null` when no DB record exists for the month. Paychecks are only persisted when the first interaction occurs (bill toggle, expense add, etc.). The "↺ Refresh amounts" button on the paycheck view re-runs `POST /api/paychecks/generate` to sync gross/net from current Settings. Individual paycheck gross/net can also be edited inline via the pencil icon. +**Lazy paycheck generation:** `GET /api/paychecks` returns virtual (unsaved) data with `id: null` when no DB record exists for the month. Paychecks are only persisted when the first interaction occurs (bill toggle, expense add, etc.). The "↺ Refresh amounts" button on the paycheck view re-runs `POST /api/paychecks/generate` to sync gross/net from current Settings — but gross/net are never overwritten when the paycheck already has any paid bills, preserving historical income values. Individual paycheck gross/net can also be edited inline via the pencil icon. -**Financing:** `GET/POST /api/financing`, `PUT/DELETE /api/financing/:id`, `PATCH /api/financing-payments/:id/paid`. Plans track a total amount and payoff due date. Payment per period is auto-calculated as `(remaining balance) / (remaining periods)`. Split plans (`assigned_paycheck = null`) divide each period's payment across both paychecks. Plans auto-close when fully paid. Financing payments are included in the paycheck remaining balance. +**Financing:** `GET/POST /api/financing`, `PUT/DELETE /api/financing/:id`, `PATCH /api/financing-payments/:id/paid`. Plans track a total amount, payoff due date, and `start_date`. Payment per period is auto-calculated as `(remaining balance) / (remaining periods)`. Split plans (`assigned_paycheck = null`) divide each period's payment across both paychecks. Plans auto-close when fully paid. Financing payments are included in the paycheck remaining balance. `start_date` prevents a plan from appearing on paycheck months before it was created — both virtual previews and `generate` respect this guard. **Migrations:** SQL files in `db/migrations/` are applied in filename order on server startup. Add new migrations as `00N_description.sql` — they run once and are tracked in the `migrations` table. diff --git a/client/src/pages/Financing.jsx b/client/src/pages/Financing.jsx index 519c288..f103fe1 100644 --- a/client/src/pages/Financing.jsx +++ b/client/src/pages/Financing.jsx @@ -20,10 +20,13 @@ function ProgressBar({ paid, total }) { ); } +const TODAY = new Date().toISOString().slice(0, 10); + const EMPTY_FORM = { name: '', total_amount: '', due_date: '', + start_date: TODAY, assigned_paycheck: 'both', }; @@ -111,6 +114,7 @@ export default function Financing() { name: plan.name, total_amount: plan.total_amount, due_date: plan.due_date, + start_date: plan.start_date || TODAY, assigned_paycheck: plan.assigned_paycheck == null ? 'both' : String(plan.assigned_paycheck), }); setFormError(null); @@ -138,6 +142,7 @@ export default function Financing() { name: form.name, total_amount: parseFloat(form.total_amount), due_date: form.due_date, + start_date: form.start_date || TODAY, assigned_paycheck: form.assigned_paycheck === 'both' ? null : parseInt(form.assigned_paycheck, 10), }; const url = editingId ? `/api/financing/${editingId}` : '/api/financing'; @@ -204,6 +209,11 @@ export default function Financing() { +
+ + +