fix: daily worker quarterly and annual bill cycle bugs
- getCycleRange() now called per-bill so quarterly/annual bills are checked in their full window, not just the calendar month - Null-safe guard after buildTrackerRow() prevents TypeError on cyclically-inactive bills
This commit is contained in:
parent
c353dd9f40
commit
1f1c505115
|
|
@ -28,6 +28,8 @@
|
|||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Daily worker cycle range bugs for quarterly and annual bills** — `dailyWorker.js` had two bugs affecting non-monthly billing cycles. First, `getCycleRange(year, month)` was called without a bill argument, always producing the calendar-month range for payment lookups. A quarterly bill paid in January would be invisible to the autopay check in February and March because the worker only searched that calendar month — a payment that existed was treated as missing. Fixed by calling `getCycleRange(year, month, bill)` per-bill so quarterly bills look at their full 3-month window and annual bills look at the full year. Second, `buildTrackerRow()` returns `null` for bills whose cycle does not apply in the current month (quarterly/annual bills in non-due months), and the code immediately accessed `row.due_date` with no null check. JavaScript's `&&` short-circuit masked the crash for non-autopay bills, but any autopay-enabled quarterly or annual bill in a non-applicable month would throw `TypeError: Cannot read properties of null`. Fixed with an early `continue` when `getCycleRange` returns null and a defensive guard after `buildTrackerRow()`. The issue description incorrectly stated that `resolveDueDate()` and `getCycleRange()` ignore cycle types — both functions already handle quarterly, annual, biweekly, and weekly correctly; the tracker already filters non-applicable bills via `.filter(Boolean)`; no new scheduler was needed.
|
||||
|
||||
- **Client snowball projection replaced with server call** — `computeLiveProjection()` in `SnowballPage.jsx` was an 86-line client-side reimplementation of `snowballService.js`. A comment acknowledged the duplication but there was no mechanism to detect drift — a bug fix on the server would silently diverge from the client preview. The function has been deleted. `GET /api/snowball/projection` now accepts an optional `?extra=N` query parameter that overrides the stored extra payment for that request without saving it, giving the client a way to preview an unsaved amount using the authoritative server simulation. The `useMemo` live projection is replaced with a 220 ms debounced `useEffect` that calls the endpoint; the existing `projectionLoading` state and loading indicator fire naturally. Drift between client and server projections is now mechanically impossible.
|
||||
|
||||
- **Snowball order PATCH validates all rows before writing** — `PATCH /api/snowball/order` previously iterated through the submitted array with a `continue` on invalid entries, silently skipping bad rows and always returning `{ success: true }`. Any item with a non-integer or negative `id`/`snowball_order` now immediately returns `400` with the specific index and value that failed. The transaction only runs after all items pass validation. Response now includes `updated` count. Soft-deleted bills are also excluded from the UPDATE (`deleted_at IS NULL`), which simultaneously closes issue #53.
|
||||
|
|
|
|||
|
|
@ -25,17 +25,27 @@ async function runDailyTasks() {
|
|||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
const todayStr = now.toISOString().slice(0, 10);
|
||||
const { start, end } = getCycleRange(year, month);
|
||||
|
||||
const bills = db.prepare('SELECT * FROM bills WHERE active = 1').all();
|
||||
|
||||
for (const bill of bills) {
|
||||
// Use the bill's own cycle range so quarterly/annual bills look at the
|
||||
// correct payment window — not just the calendar month.
|
||||
const range = getCycleRange(year, month, bill);
|
||||
|
||||
// Bill does not apply this month (quarterly/annual in a non-due month).
|
||||
if (!range) continue;
|
||||
|
||||
const payments = db.prepare(
|
||||
'SELECT * FROM payments WHERE bill_id = ? AND paid_date BETWEEN ? AND ? AND deleted_at IS NULL'
|
||||
).all(bill.id, start, end);
|
||||
).all(bill.id, range.start, range.end);
|
||||
|
||||
const row = buildTrackerRow(bill, payments, year, month, todayStr);
|
||||
|
||||
// row is null when the bill's cycle produces no due date this month.
|
||||
// Guard defensively in case getCycleRange and resolveDueDate ever disagree.
|
||||
if (!row) continue;
|
||||
|
||||
// Auto-mark autopay bills as assumed_paid on due date
|
||||
if (
|
||||
bill.autopay_enabled &&
|
||||
|
|
|
|||
Loading…
Reference in New Issue