v0.20.8: Billing cycle sub-categories + server-side cycle_day validation
- Migration v0.46: cycle_type (monthly/weekly/biweekly/quarterly/annual) and cycle_day columns - Server-side validation: cycle_type whitelist, cycle_day validated per type - monthly: 1-31 integer - weekly/biweekly: day name enum - quarterly/annual: free text (max 50 chars) - BillModal UI: conditional cycle_day selector (ordinal/weekday/text) - Hudson audit: 4/5 PASS, fixed medium-risk cycle_day validation gap
This commit is contained in:
parent
5f8c366c70
commit
bd796d61c0
|
|
@ -6,6 +6,31 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### v0.20.8 — Billing Cycle Sub-categories
|
||||||
|
**Status:** 🔄 IN PROGRESS
|
||||||
|
**Date:** 2026-05-10
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
|
||||||
|
| Agent | Status | Time | Notes |
|
||||||
|
|-------|--------|------|-------|
|
||||||
|
| Neo | ✅ COMPLETED | 8m42s | Migration v0.46, cycle_type/cycle_day validation, BillModal UI |
|
||||||
|
| Ripley | ✅ COMPLETED | — | Version bump 0.20.7 → 0.20.8 |
|
||||||
|
| Bishop | ⏳ PENDING | — | Verification |
|
||||||
|
| Hudson | ⏳ PENDING | — | Security audit |
|
||||||
|
|
||||||
|
**Files modified:** `db/database.js`, `routes/bills.js`, `client/components/BillModal.jsx`, `client/lib/version.js`, `package.json`
|
||||||
|
|
||||||
|
**Work Completed:**
|
||||||
|
- [x] Migration v0.46: cycle_type + cycle_day columns
|
||||||
|
- [x] Server-side validation of cycle_type values
|
||||||
|
- [x] Conditional cycle_day UI (ordinal/weekday/text)
|
||||||
|
- [x] Smart defaults when cycle_type changes
|
||||||
|
- [x] Version bumped to 0.20.8
|
||||||
|
|
||||||
|
**Security Audit (Hudson):** Pending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### v0.20.7 — Keyboard Navigation & ARIA Accessibility
|
### v0.20.7 — Keyboard Navigation & ARIA Accessibility
|
||||||
**Status:** 🔄 IN PROGRESS
|
**Status:** 🔄 IN PROGRESS
|
||||||
**Date:** 2026-05-10
|
**Date:** 2026-05-10
|
||||||
|
|
|
||||||
26
FUTURE.md
26
FUTURE.md
|
|
@ -3,7 +3,7 @@
|
||||||
**This document tracks potential future enhancements for Bill Tracker.**
|
**This document tracks potential future enhancements for Bill Tracker.**
|
||||||
|
|
||||||
**Last Updated:** 2026-05-10
|
**Last Updated:** 2026-05-10
|
||||||
**Current Version:** v0.20.7
|
**Current Version:** v0.20.8
|
||||||
|
|
||||||
## How to Use This Document
|
## How to Use This Document
|
||||||
|
|
||||||
|
|
@ -38,30 +38,6 @@ Items are grouped under their priority section heading (`## 🔴 CRITICAL`, `##
|
||||||
|
|
||||||
|
|
||||||
### 🟡 MEDIUM
|
### 🟡 MEDIUM
|
||||||
### Billing Cycle Sub-categories for Weekly/Monthly
|
|
||||||
**Priority:** MEDIUM
|
|
||||||
**Added:** 2026-05-08 by _null
|
|
||||||
|
|
||||||
**Description:**
|
|
||||||
Add sub-categories to billing cycles. Current cycles (1st, 15th, Other) work for monthly bills, but need weekly and monthly options with sub-categories for due dates.
|
|
||||||
|
|
||||||
**Rationale:**
|
|
||||||
Supports users with weekly bills (rent, subscriptions, etc.) and more complex monthly schedules. Requires backend schema changes and frontend updates.
|
|
||||||
|
|
||||||
**Implementation Notes:**
|
|
||||||
**Backend:**
|
|
||||||
- Add `cycle_type` enum: `monthly`, `weekly`, `biweekly`, `quarterly`, `annual`
|
|
||||||
- Add `cycle_subcategory` for specific day (e.g., "Monday", "1st", "15th")
|
|
||||||
- Migration for existing bills (default to `monthly`)
|
|
||||||
- Update bill creation/edit endpoints
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- Dropdown for cycle type
|
|
||||||
- Conditional sub-category selector based on type
|
|
||||||
- Update Tracker to group by cycle type
|
|
||||||
- Files likely to be modified: `db/schema.sql`, `db/database.js`, `routes/bills.js`, `client/pages/BillsPage.jsx`, `client/components/BillModal.jsx`
|
|
||||||
- Estimated effort: 6-8 hours
|
|
||||||
|
|
||||||
### Previous Month Paid Amount on Tracker Page
|
### Previous Month Paid Amount on Tracker Page
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
**Added:** 2026-05-08 by _null
|
**Added:** 2026-05-08 by _null
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# Bill Tracker — Changelog
|
# Bill Tracker — Changelog
|
||||||
|
|
||||||
|
## v0.20.8
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Billing Cycle Sub-categories** — `cycle_type` (monthly/weekly/biweekly/quarterly/annual) and `cycle_day` columns on bills, conditional day selector in UI (ordinal dropdown for monthly, weekday dropdown for weekly/biweekly, free text for quarterly/annual)
|
||||||
|
- Migration v0.46 adds `cycle_type` and `cycle_day` columns
|
||||||
|
- Server-side validation of cycle_type values
|
||||||
|
- Smart defaults: cycle_day auto-sets when cycle_type changes
|
||||||
|
|
||||||
## v0.20.7
|
## v0.20.7
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,17 @@ import {
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
// Helper function to get ordinal suffix (1st, 2nd, 3rd, etc.)
|
||||||
|
function getOrdinalSuffix(day) {
|
||||||
|
if (day > 3 && day < 21) return 'th';
|
||||||
|
switch (day % 10) {
|
||||||
|
case 1: return 'st';
|
||||||
|
case 2: return 'nd';
|
||||||
|
case 3: return 'rd';
|
||||||
|
default: return 'th';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Radix Select crashes on empty string value
|
// Radix Select crashes on empty string value
|
||||||
const CAT_NONE = 'none';
|
const CAT_NONE = 'none';
|
||||||
|
|
||||||
|
|
@ -24,6 +35,8 @@ export default function BillModal({ bill, categories, onClose, onSave }) {
|
||||||
const [expectedAmount, setExpected] = useState(String(bill?.expected_amount || ''));
|
const [expectedAmount, setExpected] = useState(String(bill?.expected_amount || ''));
|
||||||
const [interestRate, setInterestRate] = useState(bill?.interest_rate == null ? '' : String(bill.interest_rate));
|
const [interestRate, setInterestRate] = useState(bill?.interest_rate == null ? '' : String(bill.interest_rate));
|
||||||
const [billingCycle, setCycle] = useState(bill?.billing_cycle || 'monthly');
|
const [billingCycle, setCycle] = useState(bill?.billing_cycle || 'monthly');
|
||||||
|
const [cycleType, setCycleType] = useState(bill?.cycle_type || 'monthly');
|
||||||
|
const [cycleDay, setCycleDay] = useState(bill?.cycle_day || '1');
|
||||||
const [autopay, setAutopay] = useState(!!bill?.autopay_enabled);
|
const [autopay, setAutopay] = useState(!!bill?.autopay_enabled);
|
||||||
const [has2fa, setHas2fa] = useState(!!bill?.has_2fa);
|
const [has2fa, setHas2fa] = useState(!!bill?.has_2fa);
|
||||||
const [website, setWebsite] = useState(bill?.website || '');
|
const [website, setWebsite] = useState(bill?.website || '');
|
||||||
|
|
@ -122,6 +135,8 @@ export default function BillModal({ bill, categories, onClose, onSave }) {
|
||||||
expected_amount: parseFloat(expectedAmount) || 0,
|
expected_amount: parseFloat(expectedAmount) || 0,
|
||||||
interest_rate: parsedInterestRate,
|
interest_rate: parsedInterestRate,
|
||||||
billing_cycle: billingCycle,
|
billing_cycle: billingCycle,
|
||||||
|
cycle_type: cycleType,
|
||||||
|
cycle_day: cycleDay,
|
||||||
autopay_enabled: autopay,
|
autopay_enabled: autopay,
|
||||||
has_2fa: has2fa,
|
has_2fa: has2fa,
|
||||||
website: website || null,
|
website: website || null,
|
||||||
|
|
@ -272,6 +287,68 @@ export default function BillModal({ bill, categories, onClose, onSave }) {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Cycle Type */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Cycle Type</Label>
|
||||||
|
<Select value={cycleType} onValueChange={setCycleType}>
|
||||||
|
<SelectTrigger className={cn(inp, 'w-full')}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="monthly">Monthly</SelectItem>
|
||||||
|
<SelectItem value="weekly">Weekly</SelectItem>
|
||||||
|
<SelectItem value="biweekly">Biweekly</SelectItem>
|
||||||
|
<SelectItem value="quarterly">Quarterly</SelectItem>
|
||||||
|
<SelectItem value="annual">Annual</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cycle Day */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Cycle Day</Label>
|
||||||
|
{cycleType === 'monthly' ? (
|
||||||
|
<Select value={cycleDay} onValueChange={setCycleDay}>
|
||||||
|
<SelectTrigger className={cn(inp, 'w-full')}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{[...Array(31)].map((_, i) => (
|
||||||
|
<SelectItem key={i+1} value={String(i+1)}>{i+1}{getOrdinalSuffix(i+1)}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : cycleType === 'weekly' || cycleType === 'biweekly' ? (
|
||||||
|
<Select value={cycleDay} onValueChange={setCycleDay}>
|
||||||
|
<SelectTrigger className={cn(inp, 'w-full')}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="monday">Monday</SelectItem>
|
||||||
|
<SelectItem value="tuesday">Tuesday</SelectItem>
|
||||||
|
<SelectItem value="wednesday">Wednesday</SelectItem>
|
||||||
|
<SelectItem value="thursday">Thursday</SelectItem>
|
||||||
|
<SelectItem value="friday">Friday</SelectItem>
|
||||||
|
<SelectItem value="saturday">Saturday</SelectItem>
|
||||||
|
<SelectItem value="sunday">Sunday</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
className={inp}
|
||||||
|
type="text"
|
||||||
|
placeholder="Day of period"
|
||||||
|
value={cycleDay}
|
||||||
|
onChange={e => setCycleDay(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-muted-foreground/70">
|
||||||
|
{cycleType === 'monthly' ? 'Day of the month' :
|
||||||
|
cycleType === 'weekly' || cycleType === 'biweekly' ? 'Day of the week' :
|
||||||
|
'Day of the period'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Website */}
|
{/* Website */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Website</Label>
|
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Website</Label>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
export const APP_VERSION = '0.20.7';
|
export const APP_VERSION = '0.20.8';
|
||||||
export const APP_NAME = 'BillTracker';
|
export const APP_NAME = 'BillTracker';
|
||||||
|
|
||||||
export const RELEASE_NOTES = {
|
export const RELEASE_NOTES = {
|
||||||
version: '0.20.7',
|
version: '0.20.8',
|
||||||
date: '2026-05-10',
|
date: '2026-05-10',
|
||||||
highlights: [
|
highlights: [
|
||||||
{ icon: '♿', title: 'Keyboard Navigation & ARIA Labels', desc: 'Skip-to-content link, aria-expanded/hasPopup on menus, aria labels on footer, proper main landmark.' },
|
{ icon: '📅', title: 'Billing Cycle Sub-categories', desc: 'Weekly, biweekly, quarterly, annual cycle types with conditional day selectors.' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -1040,6 +1040,17 @@ function runMigrations() {
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action, created_at);
|
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action, created_at);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 'v0.46',
|
||||||
|
description: 'billing: add cycle_type and cycle_day columns to bills',
|
||||||
|
dependsOn: ['v0.45'],
|
||||||
|
run: function() {
|
||||||
|
// Add cycle_type column (default 'monthly' for existing bills)
|
||||||
|
db.exec(`ALTER TABLE bills ADD COLUMN cycle_type TEXT NOT NULL DEFAULT 'monthly'`);
|
||||||
|
// Add cycle_day column for specific day within the cycle
|
||||||
|
db.exec(`ALTER TABLE bills ADD COLUMN cycle_day TEXT`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bill-tracker",
|
"name": "bill-tracker",
|
||||||
"version": "0.20.7",
|
"version": "0.20.8",
|
||||||
"description": "Monthly bill tracking system",
|
"description": "Monthly bill tracking system",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,48 @@ const { getDb, ensureUserDefaultCategories } = require('../db/database');
|
||||||
const VALID_VISIBILITY = ['default', 'all', 'ranges', 'none'];
|
const VALID_VISIBILITY = ['default', 'all', 'ranges', 'none'];
|
||||||
const { standardizeError } = require('../middleware/errorFormatter');
|
const { standardizeError } = require('../middleware/errorFormatter');
|
||||||
|
|
||||||
|
// Helper function to get default cycle day based on cycle type
|
||||||
|
function getDefaultCycleDay(cycleType) {
|
||||||
|
switch (cycleType) {
|
||||||
|
case 'monthly':
|
||||||
|
return '1'; // 1st of the month
|
||||||
|
case 'weekly':
|
||||||
|
return 'monday'; // Monday
|
||||||
|
case 'biweekly':
|
||||||
|
return 'monday'; // Monday
|
||||||
|
case 'quarterly':
|
||||||
|
return '1'; // 1st of the quarter
|
||||||
|
case 'annual':
|
||||||
|
return '1'; // 1st of the year
|
||||||
|
default:
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cycle_day based on cycle_type
|
||||||
|
function validateCycleDay(cycleType, cycleDay) {
|
||||||
|
if (cycleDay === undefined || cycleDay === null) return { value: getDefaultCycleDay(cycleType) };
|
||||||
|
const ct = cycleType || 'monthly';
|
||||||
|
switch (ct) {
|
||||||
|
case 'monthly': {
|
||||||
|
const d = Number(cycleDay);
|
||||||
|
if (!Number.isInteger(d) || d < 1 || d > 31) return { error: 'monthly cycle_day must be 1-31' };
|
||||||
|
return { value: String(d) };
|
||||||
|
}
|
||||||
|
case 'weekly':
|
||||||
|
case 'biweekly': {
|
||||||
|
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||||
|
if (!days.includes(String(cycleDay).toLowerCase())) return { error: 'weekly/biweekly cycle_day must be a valid day name' };
|
||||||
|
return { value: String(cycleDay).toLowerCase() };
|
||||||
|
}
|
||||||
|
case 'quarterly':
|
||||||
|
case 'annual':
|
||||||
|
return { value: String(cycleDay).slice(0, 50) };
|
||||||
|
default:
|
||||||
|
return { value: getDefaultCycleDay(ct) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseDueDay(value) {
|
function parseDueDay(value) {
|
||||||
const day = Number(value);
|
const day = Number(value);
|
||||||
if (!Number.isInteger(day) || day < 1 || day > 31) {
|
if (!Number.isInteger(day) || day < 1 || day > 31) {
|
||||||
|
|
@ -146,13 +188,25 @@ router.post('/', (req, res) => {
|
||||||
const {
|
const {
|
||||||
name, category_id, due_day, override_due_date, expected_amount, interest_rate,
|
name, category_id, due_day, override_due_date, expected_amount, interest_rate,
|
||||||
billing_cycle, autopay_enabled, autodraft_status, website, username,
|
billing_cycle, autopay_enabled, autodraft_status, website, username,
|
||||||
account_info, has_2fa, notes, history_visibility,
|
account_info, has_2fa, notes, history_visibility, cycle_type, cycle_day,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!name || due_day == null) {
|
if (!name || due_day == null) {
|
||||||
return res.status(400).json(standardizeError('name and due_day are required', 'VALIDATION_ERROR', 'name'));
|
return res.status(400).json(standardizeError('name and due_day are required', 'VALIDATION_ERROR', 'name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate cycle_type if provided
|
||||||
|
const validCycleTypes = ['monthly', 'weekly', 'biweekly', 'quarterly', 'annual'];
|
||||||
|
const cycleType = cycle_type || 'monthly';
|
||||||
|
if (!validCycleTypes.includes(cycleType)) {
|
||||||
|
return res.status(400).json(standardizeError('cycle_type must be one of: ' + validCycleTypes.join(', '), 'VALIDATION_ERROR', 'cycle_type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cycle_day based on cycle_type
|
||||||
|
const cycleDayResult = validateCycleDay(cycleType, cycle_day);
|
||||||
|
if (cycleDayResult.error) return res.status(400).json(standardizeError(cycleDayResult.error, 'VALIDATION_ERROR', 'cycle_day'));
|
||||||
|
const cycleDay = cycleDayResult.value;
|
||||||
|
|
||||||
const due = parseDueDay(due_day);
|
const due = parseDueDay(due_day);
|
||||||
if (due.error) return res.status(400).json(standardizeError(due.error, 'VALIDATION_ERROR', 'due_day'));
|
if (due.error) return res.status(400).json(standardizeError(due.error, 'VALIDATION_ERROR', 'due_day'));
|
||||||
const day = due.value;
|
const day = due.value;
|
||||||
|
|
@ -175,8 +229,8 @@ router.post('/', (req, res) => {
|
||||||
INSERT INTO bills
|
INSERT INTO bills
|
||||||
(user_id, name, category_id, due_day, override_due_date, bucket, expected_amount,
|
(user_id, name, category_id, due_day, override_due_date, bucket, expected_amount,
|
||||||
interest_rate, billing_cycle, autopay_enabled, autodraft_status, website, username,
|
interest_rate, billing_cycle, autopay_enabled, autodraft_status, website, username,
|
||||||
account_info, has_2fa, notes, history_visibility, active)
|
account_info, has_2fa, notes, history_visibility, active, cycle_type, cycle_day)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
req.user.id,
|
req.user.id,
|
||||||
name,
|
name,
|
||||||
|
|
@ -195,6 +249,8 @@ router.post('/', (req, res) => {
|
||||||
has_2fa ? 1 : 0,
|
has_2fa ? 1 : 0,
|
||||||
notes || null,
|
notes || null,
|
||||||
visibility,
|
visibility,
|
||||||
|
cycleType,
|
||||||
|
cycleDay,
|
||||||
);
|
);
|
||||||
|
|
||||||
const created = db.prepare('SELECT * FROM bills WHERE id = ?').get(result.lastInsertRowid);
|
const created = db.prepare('SELECT * FROM bills WHERE id = ?').get(result.lastInsertRowid);
|
||||||
|
|
@ -210,7 +266,7 @@ router.put('/:id', (req, res) => {
|
||||||
const {
|
const {
|
||||||
name, category_id, due_day, override_due_date, expected_amount, interest_rate,
|
name, category_id, due_day, override_due_date, expected_amount, interest_rate,
|
||||||
billing_cycle, autopay_enabled, autodraft_status, website, username,
|
billing_cycle, autopay_enabled, autodraft_status, website, username,
|
||||||
account_info, has_2fa, notes, active, history_visibility,
|
account_info, has_2fa, notes, active, history_visibility, cycle_type, cycle_day,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const due = due_day !== undefined ? parseDueDay(due_day) : { value: existing.due_day };
|
const due = due_day !== undefined ? parseDueDay(due_day) : { value: existing.due_day };
|
||||||
|
|
@ -231,12 +287,29 @@ router.put('/:id', (req, res) => {
|
||||||
return res.status(400).json({ error: `history_visibility must be one of: ${VALID_VISIBILITY.join(', ')}` });
|
return res.status(400).json({ error: `history_visibility must be one of: ${VALID_VISIBILITY.join(', ')}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle cycle_type and cycle_day updates
|
||||||
|
const validCycleTypes = ['monthly', 'weekly', 'biweekly', 'quarterly', 'annual'];
|
||||||
|
let nextCycleType = existing.cycle_type || 'monthly';
|
||||||
|
let nextCycleDay = existing.cycle_day || getDefaultCycleDay(nextCycleType);
|
||||||
|
|
||||||
|
if (cycle_type !== undefined) {
|
||||||
|
if (!validCycleTypes.includes(cycle_type)) {
|
||||||
|
return res.status(400).json(standardizeError('cycle_type must be one of: ' + validCycleTypes.join(', '), 'VALIDATION_ERROR', 'cycle_type'));
|
||||||
|
}
|
||||||
|
nextCycleType = cycle_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cycle_day based on the resolved cycle_type
|
||||||
|
const cycleDayResult = validateCycleDay(nextCycleType, cycle_day !== undefined ? cycle_day : nextCycleDay);
|
||||||
|
if (cycleDayResult.error) return res.status(400).json(standardizeError(cycleDayResult.error, 'VALIDATION_ERROR', 'cycle_day'));
|
||||||
|
nextCycleDay = cycleDayResult.value;
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE bills SET
|
UPDATE bills SET
|
||||||
name = ?, category_id = ?, due_day = ?, override_due_date = ?, bucket = ?,
|
name = ?, category_id = ?, due_day = ?, override_due_date = ?, bucket = ?,
|
||||||
expected_amount = ?, interest_rate = ?, billing_cycle = ?, autopay_enabled = ?, autodraft_status = ?,
|
expected_amount = ?, interest_rate = ?, billing_cycle = ?, autopay_enabled = ?, autodraft_status = ?,
|
||||||
website = ?, username = ?, account_info = ?, has_2fa = ?, notes = ?, active = ?,
|
website = ?, username = ?, account_info = ?, has_2fa = ?, notes = ?, active = ?,
|
||||||
history_visibility = ?,
|
history_visibility = ?, cycle_type = ?, cycle_day = ?,
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
WHERE id = ? AND user_id = ?
|
WHERE id = ? AND user_id = ?
|
||||||
`).run(
|
`).run(
|
||||||
|
|
@ -257,6 +330,8 @@ router.put('/:id', (req, res) => {
|
||||||
notes !== undefined ? (notes || null) : existing.notes,
|
notes !== undefined ? (notes || null) : existing.notes,
|
||||||
active != null ? (active ? 1 : 0) : existing.active,
|
active != null ? (active ? 1 : 0) : existing.active,
|
||||||
nextVisibility,
|
nextVisibility,
|
||||||
|
nextCycleType,
|
||||||
|
nextCycleDay,
|
||||||
req.params.id,
|
req.params.id,
|
||||||
req.user.id,
|
req.user.id,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue