Commit Graph

141 Commits

Author SHA1 Message Date
null 73631ab812 fix(tracker): route error handling + autopay write atomicity (T2)
routes/tracker.js had no try/catch and returned a plain {error} shape; the
three GET handlers now wrap in try/catch + standardizeError (including the
invalid-month path). applyAutopaySuggestions ran INSERT + applyBalanceDelta
as two un-transactional steps on a GET — wrapped both in one db.transaction.

Tests: autopay creates one payment + drops balance (idempotent), route
returns standardized error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:21:28 -05:00
null 10e159352a fix(notifications): honor per-bill reminder_days_before + expose for all bills (BM3)
The notifier used a hard-coded 3-day early reminder and never read
reminder_days_before, so the modal's 'Reminder Days' control was a no-op.
The early reminder now fires at the bill's own lead (>= 2 days so it never
collides with the 1-day/same-day reminders); email subject+body say 'due in
N days'. Lead-time selection extracted to a pure exported reminderTypeFor()
for unit testing. The Reminder Days control now shows for every bill and a
non-subscription save no longer clobbers the column to 3.

Test: tests/notificationLeadTime.test.js

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:16:10 -05:00
null 4a38cc8614 fix(tracker): gate bank card unpaid/remaining by occurrence + add trackerService tests (T1)
buildBankTracking summed expected_amount for all active unpaid bills with no
resolveDueDate gate, so annual/off-month bills inflated unpaid_this_month and
the bank remaining (same class as QA-B5-02, live on the bank path). getTracker
now derives the unpaid total from the already-gated rows (netting partials) and
passes it in. summary.remaining/total_remaining now use the bank card's own
remaining in bank mode (agreeing with safe-to-spend), and a stray balance/100
is now fromCents.

New tests/trackerService.test.js: gating fix, summary totals, bank-mode
remaining agreement, cents<->dollars, getOverdueCount gating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:10:28 -05:00
null db4d33513e fix(debt): honest payoff date for unpayable debts + math tests (Snowball #1,#5)
The payoff simulation reported months_to_freedom by taking Math.max over each
debt's payoff month — but a debt whose minimum never overcomes its interest (and
that the rolling snowball can't cover) never reaches $0, so its "never" counted
as 0 and the projection showed the OTHER debts' last month as the freedom date.
Now months_to_freedom/payoff_date are null when any active debt never clears, the
result is flagged capped, and each such debt is marked never_paid
(services/snowballService.js + the same guard in aprService calculateMinimumOnly).

Also adds tests/snowballMath.test.js (12) — the debt-payoff engine had zero
coverage. Hand-calculated examples for amortization (0% + 12% APR), snowball
rolling, avalanche vs snowball interest, skip reasons, APR snapshot, and the
unpayable-debt edge. Server suite 151 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:01:17 -05:00
null d53a64b604 feat(data): "Erase my data" danger zone (Batch 5)
New services/userDataService.js eraseUserData() permanently wipes a user's
financial + imported data in one transaction (child → parent order for FK
safety): bills (+ cascading payments/monthly_bill_state/bill_history_ranges),
transactions/accounts/data_sources, categories/groups, templates, snowball,
spending rules/budgets, merchant rules, imports, and per-user hint tables. It
PRESERVES the account, sessions, 2FA/WebAuthn, login history and preferences —
this resets your data, not your account — then re-seeds default categories and
writes an audit row to import_history.

- POST /api/user/erase-data — rate-limited (demoDataLimiter), requires a
  type-to-confirm token ("ERASE"), structured errors.
- UI: EraseDataSection danger-zone card (Export & backups pane) — red-accented,
  "download a backup first" nudge, type-to-confirm AlertDialog, toasts; on
  success DataPage reloads all state.

Tests: tests/eraseUserData.test.js — wipes user A only, preserves user B +
account + session, re-seeds categories, audited. Server 139 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:21:07 -05:00
null bd1eee00b0 feat(import): OFX/QFX transaction import (Batch 3)
New services/ofxImportService.js parses OFX 1.x (SGML, unclosed leaf tags),
OFX 2.x (XML) and QFX (+ Intuit tags ignored) into the same normalized shape the
CSV path produces, then writes through the SAME shared primitives (session table,
(user_id, data_source_id, provider_transaction_id) dedupe, import_history) — now
exported from csvTransactionImportService (additive; CSV tests still pass).

- Routes POST /api/import/ofx/{preview,commit} mirror the CSV two-step (raw
  upload → structured commit; no column mapping since OFX is structured).
- UI: ImportOfxSection (upload → preview list → import) in the Import pane;
  amounts shown via formatCentsUSD; toasts on preview/commit/malformed.
- Gap handling: signed TRNAMT → signed cents; DTPOSTED → YYYY-MM-DD; FITID →
  stable provider id (hash fallback); non-OFX / empty files rejected clearly.

Tests: tests/ofxImportService.test.js (SGML + XML/QFX parse, entity decode,
signed cents, preview→commit, re-import dedupe, import_history). Server 129 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:11:59 -05:00
null fa2432265c refactor(match): one canonical writer for transaction match state (IMP-CODE-03)
match_status, matched_bill_id and ignored must move together, but they were
updated by copy-pasted inline UPDATEs across six routes/services — exactly how
they drift apart (QA-B5-04 left match_status='matched' with a NULL bill).

Add services/transactionMatchState.js (markMatched / markUnmatched / markIgnored,
each ownership-scoped, returning rows changed) and route the six single-
transaction transitions through it: matchTransactionToBill, unmatchTransaction,
ignoreTransaction, unignoreTransaction (transactionMatchService), the match/
unmatch handlers (routes/matches), and unmatch-on-payment-delete (routes/
transactions, routes/payments).

Guarded bulk auto-match sweeps (subscription tracking, merchant-rule matching,
historical import) and the retention purge intentionally keep their own queries
— their WHERE clauses carry idempotency guards (AND match_status='unmatched')
the simple helper must not silently drop.

Test: tests/transactionMatchState.test.js (transitions + ownership scoping).
transactionMatchService/subscriptionService regression suites still pass;
server 122 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:02:10 -05:00
null b3168fca70 fix(qa): retention GC orphaned matched transactions on bill purge (QA-B5-04)
Found probing a copy of the live SimpleFIN DB: 3 transactions were
match_status='matched' with matched_bill_id=NULL. Bills are soft-deleted
(retained for recovery), then the retention GC hard-deletes them past the
30-day window. transactions.matched_bill_id is ON DELETE SET NULL, so the
purge nulled the pointer but left match_status='matched' — a limbo row
excluded from spending/analytics (match_status != 'matched') yet attributed
to no bill, silently dropping that spend.

pruneSoftDeletedFinancialRecords now releases those matches back to
'unmatched' in the same transaction and self-heals pre-existing orphans;
retention behaviour is unchanged. Verified on a live-DB copy (3→0 orphans,
0 transactions lost). Regression: 3 tests in backupAndCleanup.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:04:59 -05:00
null 2963d11d1b fix(qa): version check is opt-out-able (QA-B16-01)
- updateCheckService: gate the external request on `update_check_enabled`
  (default on); when off, no network call, returns { disabled: true }
- aboutAdmin: GET/PUT /update-check-setting (admin-only) to toggle it
- StatusPage: a Switch on the admin System Status card to enable/disable
- privacy.js: state that an admin can disable it (was called "optional" with
  no actual opt-out)
- tests/updateCheckOptOut.test.js: proves no external fetch when disabled
- docs: archive QA-B16-01, B16 

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:05:37 -05:00
null c31d8cbe9e fix(qa): escape bill name in reminder email HTML — XSS via bill name (B14-04)
- notificationService buildEmailHtml: the message line interpolated bill.name
  raw (`<strong>${bill.name}</strong> is due…`) while the detail table escaped
  it; a `<img src=x onerror=…>` name landed unescaped in the email HTML. Now
  escaped everywhere. (self-XSS — reminders go to the bill's owner — but a clear
  inconsistent-escaping defect)
- expose buildEmailHtml via _email; add an escaping test across all 4 email types
- docs: archive QA-B14-04

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:18:05 -05:00
null 2050e13407 fix(qa): notification _push export was clobbered → "Send test push" 500'd (B10-01)
- notificationService: `module.exports._push = {...}` was set BEFORE the final
  `module.exports = {...}`, which wiped it, so routes/notifications.js got
  `_push || {}` → sendTestPush undefined → POST /api/notifications/test-push
  always threw "Push service not initialised". Scheduled reminders were fine
  (in-scope calls). Moved the _push assignment after the reassignment.
- add tests/notificationDelivery.test.js (7 tests: ntfy/gotify/discord payloads,
  dispatch, error handling, unknown channel, no token leak in the body)
- docs: archive QA-B10-01

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:11:34 -05:00
null 1bd282f47b fix(qa): Analytics "expected" gates by occurrence (matches Tracker/Summary)
- analyticsService: only add a bill's expected_amount in months it actually
  occurs (resolveDueDate), so annual / off-month quarterly bills no longer
  inflate the expected-vs-actual line every month (QA-B5-03, same root as B5-01)
- add a Tracker<->Analytics reconciliation guard to e2e/api.probe.spec.js
- docs: archive QA-B5-03; cycle log

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:23:37 -05:00
null 127b69ffc2 chore(qa): vendor chunk splitting, remove unused markdown deps, remove dead totalInterestPaid (batch 0.41.0 QA cleanup) 2026-07-02 20:47:50 -05:00
null 029c227685 fix(qa): seed demo data amounts, bill amount validation, negative USD format, a11y aria-labels, Playwright E2E setup (batch 0.41.0 QA) 2026-07-02 20:36:09 -05:00
null 35e5d185de feat(spending): category groups, YNAB-style spending page overhaul, 3-month averages, cover overspending (batch 0.41.0) 2026-06-14 19:21:34 -05:00
null 81ddcb5fc1 feat(banking): bank transactions page with merchant/store matching, transaction matching refactor, bank sync improvements (batch 0.40.0) 2026-06-14 15:15:31 -05:00
null 8ef794a94a feat(settings): safe-to-spend toggle, move notifications from Profile to Settings, fix dark-mode readability 2026-06-12 01:52:48 -05:00
null dc49eb9633 feat(cashflow): safe-to-spend projection with timeline, vitest setup, package upgrades 2026-06-12 01:32:28 -05:00
null d6639f1385 feat(money): cents migration stage 2 — schema flip to integer cents (batch 0.38.4) 2026-06-11 20:12:31 -05:00
null bf66ab1ee6 feat(money): migrate services to cent-exact money.js helpers (batch 0.38.3) 2026-06-10 20:14:13 -05:00
null 947fa3bdf8 feat(auth): add per-username rate limiter, migrate dates to local-time utils (batch 0.38.1) 2026-06-10 19:42:51 -05:00
null 38c8bbd472 feat(server): add trust proxy, CSRF HTTPS detection, error formatting, dates util (batch 0.38.0) 2026-06-10 19:37:19 -05:00
null ca514e5f26 fix(tracker): BillModal save/close race and pending badge logic
- Use controlled Dialog state (setDialogOpen) instead of immediate onClose()
  to let Radix cleanup properly before unmount
- Amber 'Pending' badge now only shows for bank-linked bills — unlinked
  bills skip the pending-cleared check and show 'Paid' directly
- TrackerPage onSave no longer nullifies edit state before BillModal can
  animate closed

(batch 0.37.4)
2026-06-08 16:33:48 -05:00
null fab4945d50 fix(tracker): bank pending counts, overdue center cleanup, and payment source labels
- Add bank_pending_count to tracker rows showing pending bank transaction
  matches for bills with merchant rules
- Remove snoozed-only state from OverdueCommandCenter (always show when
  overdue rows exist)
- Display 'Synced' label for transaction-matched payments in BillModal
- Prioritize 'Pending' badge over StatusBadge when bank has pending matches
- Exclude bank-synced and transaction-matched payments from pending_cleared

(batch 0.37.3)
2026-06-08 16:05:31 -05:00
null 626459322f fix(tracker): live sync label truncation and due_day fallback on partial update
- Shorten 'Live Sync' label to 'Live' for space-constrained layouts
- Add existing bill due_day fallback in validateBillData to prevent
  spurious required-field errors during partial PATCH updates

(batch 0.37.2)
2026-06-08 12:24:51 -05:00
null 80ef1208ae fix(tracker): update payment progress and bills service (batch 0.37.1) 2026-06-08 11:54:47 -05:00
null 426b0fd932 fix(admin): admin/profile routes and services 2026-06-07 21:18:02 -05:00
null 79b51b1c9a fix(bank-sync): transaction matching, services, and worker updates 2026-06-07 20:07:27 -05:00
null 31be51e77f fix(bank-sync): admin config, matching, and worker updates 2026-06-07 19:41:17 -05:00
null 12bcd1d8f3 fix(auth): oidc service updates 2026-06-07 18:05:09 -05:00
null f7ad1c1ebb fix(tracker): table columns and settings improvements 2026-06-07 17:23:14 -05:00
null 13e41aec74 feat: iCal feed for bills (Apple/Google calendar export) 2026-06-07 15:53:46 -05:00
null e1082145ab feat: tracker payment flow and mobile row improvements 2026-06-07 14:49:39 -05:00
null d9cf499dba feat: search filter panel component, search preference persistence, page integration 2026-06-07 01:28:35 -05:00
null ab5e3fbf1f feat: profile settings UI, auth service refactor, schema migration, route tests 2026-06-07 01:17:49 -05:00
null 6811eb8be5 feat: payment accounting service, SQL schema + migration, backend route refactor, test updates 2026-06-07 01:05:48 -05:00
null f1817a520b fix: status service edge case handling 2026-06-07 00:24:43 -05:00
null 8c2ecdb313 fix: subscription service error handling, SubscriptionsPage cleanup 2026-06-07 00:11:00 -05:00
null 4dd01c13c4 feat: live transaction search in merchant rules, link-import preference toggle, tracker row tweaks 2026-06-06 23:04:53 -05:00
null a1e6a308cf feat: existing bill matching in recommendations, feedback tracking, broad-merchant rejection, annual price detection 2026-06-06 21:15:08 -05:00
null 422d8550bb feat: recommendation detail dialog with evidence, ambiguity badges, transaction list 2026-06-06 21:05:01 -05:00
null b2f8f5ef66 feat: dedicated subscription catalog page, evidence badges, price display in recommendations 2026-06-06 20:44:54 -05:00
null 3a034ddeb7 feat: subscription catalog with bank descriptors, custom per-user descriptors, catalog→bill linking 2026-06-06 20:02:13 -05:00
null 7455dff5b8 feat: v0.37.0 — auto-learn merchant rules, ambiguous match protection, session hashing, geolocation opt-in 2026-06-06 18:30:21 -05:00
null 9a2a7ecdee feat: v0.94 — session token hashing, geolocation opt-in privacy setting 2026-06-06 17:00:22 -05:00
null 840620efe2 feat: v0.93 — stable provider keys, per-payment interest tracking with once-per-month gating 2026-06-06 16:34:20 -05:00
null a2ac241cd3 refactor(sync): centralize sync constants in bankSyncConfigService, wire through config/UI 2026-06-06 15:51:56 -05:00
null a73d0afe07 feat(encryption): support TOKEN_ENCRYPTION_KEY env var with startup migration 2026-06-06 15:27:45 -05:00
null 7d42d119c0 fix(simplefin): retry transient fetch failures (3 attempts, 1s/2s backoff) 2026-06-06 15:06:12 -05:00
null a66fe13bc6 fix(simplefin): add 30s AbortSignal timeout to fetch calls 2026-06-06 14:54:00 -05:00