Add proactive financial intelligence feed

Shifts the AI assistant from reactive (users ask questions) to proactive
(the system surfaces personalized insights automatically). A nightly job
analyzes every family's financial data across 7 insight types, writes
natural-language explanations via Claude, and surfaces them in a feed on
the dashboard and a standalone /insights page.

Feature is behind a flag: off by default, enable with INSIGHTS_ENABLED=1
(or via Setting.insights_enabled in the admin UI).

Insight types:
- spending_anomaly: category spend >25% above/below 3-month rolling average
- cash_flow_warning: projected cash balance drops below $500 in 30 days
  (uses RecurringTransaction + statistical daily baseline)
- net_worth_milestone: crossed a round-number milestone or all-time high
- subscription_audit: recurring transaction overdue by 45+ days
- savings_rate_change: savings rate changed >5 percentage points vs last month
- idle_cash: $5k+ sitting in depository account with no activity in 60 days
- budget_at_risk / budget_on_track: spending pace vs monthly budget

Architecture:
- Insight model with dedup_key unique index (upsert, not re-create daily)
- Insight::Generator base class + Insight::GeneratorRegistry orchestrator
- LLM used as a writer only — financial math runs in pure Ruby
- GenerateInsightsJob runs at 6 AM UTC daily via sidekiq-cron
- InsightsController with read/dismiss Turbo Stream actions
- Dashboard section gated by Current.user.insights_enabled?

https://claude.ai/code/session_014vY9xohpm3abSAxVxRF27a
This commit is contained in:
Claude
2026-04-12 12:12:49 +00:00
parent 16a0fa08f8
commit 8ae06e37e4
27 changed files with 1173 additions and 0 deletions

75
test/fixtures/insights.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
spending_anomaly_dining:
family: dylan_family
insight_type: spending_anomaly
priority: medium
status: active
title: "Dining spending up 34%"
body: "Your dining spending is up 34% vs your 3-month average — currently on pace for $634 this month versus the $473 average."
metadata:
category_name: "Dining"
percent_change: 34.1
current_amount: 634.50
baseline_amount: 472.80
direction: "up"
delta_amount: 161.70
currency: USD
period_start: <%= Date.current.beginning_of_month %>
period_end: <%= Date.current.end_of_month %>
dedup_key: "spending_anomaly:dining-fixture:2026-04"
generated_at: <%= 1.hour.ago %>
cash_flow_warning:
family: dylan_family
insight_type: cash_flow_warning
priority: high
status: active
title: "Low cash projected around April 28"
body: "Based on your recurring bills and spending patterns, your cash balance may drop to $142 around April 28."
metadata:
projected_low_balance: 142.00
projected_low_date: "2026-04-28"
current_balance: 1800.00
upcoming_outflows: 1847.00
account_count: 1
currency: USD
period_start: <%= Date.current %>
period_end: <%= 30.days.from_now.to_date %>
dedup_key: "cash_flow_warning:2026-04"
generated_at: <%= 1.hour.ago %>
net_worth_milestone:
family: dylan_family
insight_type: net_worth_milestone
priority: high
status: read
title: "Net worth crossed $100,000"
body: "Net worth crossed the $100,000 milestone, now at $101,342."
metadata:
milestone: 100000
current_net_worth: 101342.00
previous_net_worth: 98800.00
all_time_high: true
currency: USD
period_start: <%= 30.days.ago.to_date %>
period_end: <%= Date.current %>
dedup_key: "net_worth_milestone:100000:2026-04"
generated_at: <%= 2.hours.ago %>
read_at: <%= 30.minutes.ago %>
dismissed_insight:
family: dylan_family
insight_type: idle_cash
priority: low
status: dismissed
title: "Idle cash in Savings"
body: "$6,200 has been sitting in Savings for over 60 days without any transactions."
metadata:
account_name: "Savings"
idle_amount: 6200.00
idle_days: 60
currency: USD
period_start: <%= 60.days.ago.to_date %>
period_end: <%= Date.current %>
dedup_key: "idle_cash:savings-fixture:2026-04"
generated_at: <%= 3.hours.ago %>
dismissed_at: <%= 1.hour.ago %>