1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 13:19:39 +02:00

Add income and spending insight cards to dashboard (#668)

* Generate time series for rolling 30 day sum of income and spending

* Highlight accounts with most income and spending in the last 30 days

* Aggregate chips after 3 top accounts in insight card

* Refactor aggregation filter

I think this is easier to read and understand whats happening at a
glance

* Refactor and tidy

* Use family currency for insight cards

* Further reduce risk of sql injection

* Fix lint

* Refactor rolling total queries

* Add test for transaction snapshot
This commit is contained in:
Josh Brown 2024-04-24 13:34:50 +01:00 committed by GitHub
parent 1f6e83ee91
commit 461fa672ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 207 additions and 64 deletions

View file

@ -6,6 +6,15 @@ class PagesController < ApplicationController
@net_worth_series = snapshot[:net_worth_series]
@asset_series = snapshot[:asset_series]
@liability_series = snapshot[:liability_series]
snapshot_transactions = Current.family.snapshot_transactions
@income_series = snapshot_transactions[:income_series]
@spending_series = snapshot_transactions[:spending_series]
snapshot_account_transactions = Current.family.snapshot_account_transactions
@top_spenders = snapshot_account_transactions[:top_spenders]
@top_earners = snapshot_account_transactions[:top_earners]
@account_groups = Current.family.accounts.by_group(period: @period, currency: Current.family.currency)
@transactions = Current.family.transactions.limit(5).order(date: :desc)
@ -13,8 +22,6 @@ class PagesController < ApplicationController
placeholder_series_data = 10.times.map do |i|
{ date: Date.current - i.days, value: Money.new(0) }
end
@income_series = TimeSeries.new(placeholder_series_data)
@spending_series = TimeSeries.new(placeholder_series_data)
@savings_rate_series = TimeSeries.new(10.times.map { |i| { date: Date.current - i.days, value: 0 } })
@investing_series = TimeSeries.new(placeholder_series_data)
end

View file

@ -28,6 +28,49 @@ class Family < ApplicationRecord
}
end
def snapshot_account_transactions
period = Period.last_30_days
results = accounts.active.joins(:transactions)
.select(
"accounts.*",
"COALESCE(SUM(amount) FILTER (WHERE amount > 0), 0) AS spending",
"COALESCE(SUM(-amount) FILTER (WHERE amount < 0), 0) AS income"
)
.where("transactions.date >= ?", period.date_range.begin)
.where("transactions.date <= ?", period.date_range.end)
.group("id")
.to_a
{
top_spenders: results.sort_by(&:spending).select { |a| a.spending > 0 }.reverse,
top_earners: results.sort_by(&:income).select { |a| a.income > 0 }.reverse
}
end
def snapshot_transactions
rolling_totals = Transaction.daily_rolling_totals(transactions, period: Period.last_30_days, currency: self.currency)
spending = []
income = []
rolling_totals.each do |r|
spending << {
date: r.date,
value: Money.new(r.rolling_spend, self.currency)
}
income << {
date: r.date,
value: Money.new(r.rolling_income, self.currency)
}
end
{
income_series: TimeSeries.new(income, favorable_direction: "up"),
spending_series: TimeSeries.new(spending, favorable_direction: "down")
}
end
def effective_start_date
accounts.active.joins(:balances).minimum("account_balances.date") || Date.current
end

View file

@ -14,6 +14,10 @@ class Period
@date_range = date_range
end
def extend_backward(duration)
Period.new(name: name + "_extended", date_range: (date_range.first - duration)..date_range.last)
end
BUILTIN = [
new(name: "all", date_range: nil..Date.current),
new(name: "last_7_days", date_range: 7.days.ago.to_date..Date.current),

View file

@ -11,6 +11,45 @@ class Transaction < ApplicationRecord
scope :inflows, -> { where("amount > 0") }
scope :outflows, -> { where("amount < 0") }
scope :active, -> { where(excluded: false) }
scope :with_converted_amount, ->(currency = Current.family.currency) {
# Join with exchange rates to convert the amount to the given currency
# If no rate is available, exclude the transaction from the results
select(
"transactions.*",
"transactions.amount * COALESCE(er.rate, 1) AS converted_amount"
)
.joins(sanitize_sql_array([ "LEFT JOIN exchange_rates er ON transactions.date = er.date AND transactions.currency = er.base_currency AND er.converted_currency = ?", currency ]))
.where("er.rate IS NOT NULL OR transactions.currency = ?", currency)
}
def self.daily_totals(transactions, period: Period.last_30_days, currency: Current.family.currency)
# Sum spending and income for each day in the period with the given currency
select(
"gs.date",
"COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending",
"COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income"
)
.from(transactions.with_converted_amount(currency), :t)
.joins(sanitize_sql([ "RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date", period.date_range.first, period.date_range.last ]))
.group("gs.date")
end
def self.daily_rolling_totals(transactions, period: Period.last_30_days, currency: Current.family.currency)
# Extend the period to include the rolling window
period_with_rolling = period.extend_backward(period.date_range.count.days)
# Aggregate the rolling sum of spending and income based on daily totals
rolling_totals = from(daily_totals(transactions, period: period_with_rolling, currency: currency))
.select(
"*",
sanitize_sql_array([ "SUM(spending) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_spend", "#{period.date_range.count} days" ]),
sanitize_sql_array([ "SUM(income) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_income", "#{period.date_range.count} days" ])
)
.order("date")
# Trim the results to the original period
select("*").from(rolling_totals).where("date >= ?", period.date_range.first)
end
def self.ransackable_attributes(auth_object = nil)
%w[name amount date]

View file

@ -33,12 +33,13 @@
</section>
<section class="grid grid-cols-2 gap-4">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex gap-4 h-full">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".income"),
period: @period,
value: @income_series.last.value,
period: Period.last_30_days,
value: @income_series.last&.value,
trend: @income_series.trend
} %>
</div>
@ -47,16 +48,32 @@
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @income_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_earners.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<div class="flex items-center justify-center text-xs w-5 h-5 rounded-full <%= accountable_text_class(account.accountable_type) %> <%= accountable_bg_transparent_class(account.accountable_type) %>">
<%= account.name[0].upcase %>
</div>
<span>+<%= Money.new(account.income, account.currency) %></span>
<% end %>
<% end %>
<% if @top_earners.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_earners.count - 3 %></div>
<% end %>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex gap-4 h-full">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".spending"),
period: @period,
value: @spending_series.last.value,
period: Period.last_30_days,
value: @spending_series.last&.value,
trend: @spending_series.trend
} %>
</div>
@ -65,7 +82,22 @@
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @spending_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_spenders.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-gray-900 font-medium hover:bg-gray-25" do %>
<div class="flex items-center justify-center text-xs w-5 h-5 rounded-full <%= accountable_text_class(account.accountable_type) %> <%= accountable_bg_transparent_class(account.accountable_type) %>">
<%= account.name[0].upcase %>
</div>
-<%= Money.new(account.spending, account.currency) %>
<% end %>
<% end %>
<% if @top_spenders.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-gray-500">+<%= @top_spenders.count - 3 %></div>
<% end %>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">

View file

@ -7,14 +7,14 @@ en:
debts: Debts
categories: Categories
greeting: Welcome back, %{name}
income: Income (coming soon...)
income: Income
investing: Investing (coming soon...)
net_worth: Net Worth
new: New account
no_transactions: You have no recent transactions
recurring: Recurring
savings_rate: Savings Rate (coming soon...)
spending: Spending (coming soon...)
spending: Spending
subtitle: Here's what's happening today
transactions: Transactions
view_all: View all

View file

@ -1,32 +1,32 @@
date_offset,net_worth,assets,liabilities,depositories,investments,loans,credits,properties,vehicles,other_assets,other_liabilities
-30,48278.57,49318.57,1040.00,48918.57,0.00,0.00,1040.00,0.00,0.00,400.00,0.00
-29,49298.96,50238.96,940.00,49838.96,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-28,49311.01,50251.01,940.00,49851.01,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-27,49248.35,50188.35,940.00,49788.35,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-26,49200.15,50140.15,940.00,49740.15,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-25,48447.74,49387.74,940.00,48987.74,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-24,48428.46,49368.46,940.00,48968.46,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-23,48388.70,49328.70,940.00,48928.70,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-22,49502.26,50442.26,940.00,50042.26,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-21,49509.49,50449.49,940.00,50049.49,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-20,49520.33,50460.33,940.00,50060.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-19,49265.33,50205.33,940.00,49805.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-18,47267.72,48207.72,940.00,47807.72,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-17,47260.55,48200.55,940.00,47800.55,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-16,47249.80,48189.80,940.00,47789.80,0.00,0.00,940.00,0.00,0.00,400.00,0.00
-15,47175.14,48135.14,960.00,47735.14,0.00,0.00,960.00,0.00,0.00,400.00,0.00
-14,47172.75,48132.75,960.00,47732.75,0.00,0.00,960.00,0.00,0.00,400.00,0.00
-13,47166.78,48126.78,960.00,47726.78,0.00,0.00,960.00,0.00,0.00,400.00,0.00
-12,47854.20,48844.20,990.00,48144.20,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-11,47830.30,48820.30,990.00,48120.30,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-10,47906.78,48896.78,990.00,48196.78,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-9,48022.64,49012.64,990.00,48312.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-8,48016.64,49006.64,990.00,48306.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-7,48011.84,49001.84,990.00,48301.84,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-6,47989.04,48979.04,990.00,48279.04,0.00,0.00,990.00,0.00,0.00,700.00,0.00
-5,48154.64,49154.64,1000.00,48454.64,0.00,0.00,1000.00,0.00,0.00,700.00,0.00
-4,47195.60,48195.60,1000.00,47645.60,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
-3,48096.40,49096.40,1000.00,48546.40,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
-2,48032.80,49032.80,1000.00,48482.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
-1,48064.00,49064.00,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
0,47550.80,48550.80,1000.00,48000.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00
date_offset,net_worth,assets,liabilities,depositories,investments,loans,credits,properties,vehicles,other_assets,other_liabilities,spending,income,rolling_spend,rolling_income
-30,48278.57,49318.57,1040.00,48918.57,0.00,0.00,1040.00,0.00,0.00,400.00,0.00,0,0,0,0
-29,49298.96,50238.96,940.00,49838.96,0.00,0.00,940.00,0.00,0.00,400.00,0.00,15,1018.8,15,1018.8
-28,49311.01,50251.01,940.00,49851.01,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8
-27,49248.35,50188.35,940.00,49788.35,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8
-26,49200.15,50140.15,940.00,49740.15,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8
-25,48447.74,49387.74,940.00,48987.74,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8
-24,48428.46,49368.46,940.00,48968.46,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8
-23,48388.70,49328.70,940.00,48928.70,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,1018.8
-22,49502.26,50442.26,940.00,50042.26,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,1075,15,2093.8
-21,49509.49,50449.49,940.00,50049.49,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,2093.8
-20,49520.33,50460.33,940.00,50060.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,15,2093.8
-19,49265.33,50205.33,940.00,49805.33,0.00,0.00,940.00,0.00,0.00,400.00,0.00,216.44,0,231.44,2093.8
-18,47267.72,48207.72,940.00,47807.72,0.00,0.00,940.00,0.00,0.00,400.00,0.00,2000,0,2231.44,2093.8
-17,47260.55,48200.55,940.00,47800.55,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,2231.44,2093.8
-16,47249.80,48189.80,940.00,47789.80,0.00,0.00,940.00,0.00,0.00,400.00,0.00,0,0,2231.44,2093.8
-15,47175.14,48135.14,960.00,47735.14,0.00,0.00,960.00,0.00,0.00,400.00,0.00,40,0,2271.44,2093.8
-14,47172.75,48132.75,960.00,47732.75,0.00,0.00,960.00,0.00,0.00,400.00,0.00,0,0,2271.44,2093.8
-13,47166.78,48126.78,960.00,47726.78,0.00,0.00,960.00,0.00,0.00,400.00,0.00,0,0,2271.44,2093.8
-12,47854.20,48844.20,990.00,48144.20,0.00,0.00,990.00,0.00,0.00,700.00,0.00,60,50,2331.44,2143.8
-11,47830.30,48820.30,990.00,48120.30,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2143.8
-10,47906.78,48896.78,990.00,48196.78,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2143.8
-9,48022.64,49012.64,990.00,48312.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,103.915,2331.44,2247.715
-8,48016.64,49006.64,990.00,48306.64,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2247.715
-7,48011.84,49001.84,990.00,48301.84,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2247.715
-6,47989.04,48979.04,990.00,48279.04,0.00,0.00,990.00,0.00,0.00,700.00,0.00,0,0,2331.44,2247.715
-5,48154.64,49154.64,1000.00,48454.64,0.00,0.00,1000.00,0.00,0.00,700.00,0.00,20,200,2351.44,2447.715
-4,47195.60,48195.60,1000.00,47645.60,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,863.04,0,3214.48,2447.715
-3,48096.40,49096.40,1000.00,48546.40,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715
-2,48032.80,49032.80,1000.00,48482.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715
-1,48064.00,49064.00,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715
0,47550.80,48550.80,1000.00,48000.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715
1 date_offset net_worth assets liabilities depositories investments loans credits properties vehicles other_assets other_liabilities spending income rolling_spend rolling_income
2 -30 48278.57 49318.57 1040.00 48918.57 0.00 0.00 1040.00 0.00 0.00 400.00 0.00 0 0 0 0
3 -29 49298.96 50238.96 940.00 49838.96 0.00 0.00 940.00 0.00 0.00 400.00 0.00 15 1018.8 15 1018.8
4 -28 49311.01 50251.01 940.00 49851.01 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8
5 -27 49248.35 50188.35 940.00 49788.35 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8
6 -26 49200.15 50140.15 940.00 49740.15 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8
7 -25 48447.74 49387.74 940.00 48987.74 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8
8 -24 48428.46 49368.46 940.00 48968.46 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8
9 -23 48388.70 49328.70 940.00 48928.70 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 1018.8
10 -22 49502.26 50442.26 940.00 50042.26 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 1075 15 2093.8
11 -21 49509.49 50449.49 940.00 50049.49 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 2093.8
12 -20 49520.33 50460.33 940.00 50060.33 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 15 2093.8
13 -19 49265.33 50205.33 940.00 49805.33 0.00 0.00 940.00 0.00 0.00 400.00 0.00 216.44 0 231.44 2093.8
14 -18 47267.72 48207.72 940.00 47807.72 0.00 0.00 940.00 0.00 0.00 400.00 0.00 2000 0 2231.44 2093.8
15 -17 47260.55 48200.55 940.00 47800.55 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 2231.44 2093.8
16 -16 47249.80 48189.80 940.00 47789.80 0.00 0.00 940.00 0.00 0.00 400.00 0.00 0 0 2231.44 2093.8
17 -15 47175.14 48135.14 960.00 47735.14 0.00 0.00 960.00 0.00 0.00 400.00 0.00 40 0 2271.44 2093.8
18 -14 47172.75 48132.75 960.00 47732.75 0.00 0.00 960.00 0.00 0.00 400.00 0.00 0 0 2271.44 2093.8
19 -13 47166.78 48126.78 960.00 47726.78 0.00 0.00 960.00 0.00 0.00 400.00 0.00 0 0 2271.44 2093.8
20 -12 47854.20 48844.20 990.00 48144.20 0.00 0.00 990.00 0.00 0.00 700.00 0.00 60 50 2331.44 2143.8
21 -11 47830.30 48820.30 990.00 48120.30 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2143.8
22 -10 47906.78 48896.78 990.00 48196.78 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2143.8
23 -9 48022.64 49012.64 990.00 48312.64 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 103.915 2331.44 2247.715
24 -8 48016.64 49006.64 990.00 48306.64 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2247.715
25 -7 48011.84 49001.84 990.00 48301.84 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2247.715
26 -6 47989.04 48979.04 990.00 48279.04 0.00 0.00 990.00 0.00 0.00 700.00 0.00 0 0 2331.44 2247.715
27 -5 48154.64 49154.64 1000.00 48454.64 0.00 0.00 1000.00 0.00 0.00 700.00 0.00 20 200 2351.44 2447.715
28 -4 47195.60 48195.60 1000.00 47645.60 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 863.04 0 3214.48 2447.715
29 -3 48096.40 49096.40 1000.00 48546.40 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715
30 -2 48032.80 49032.80 1000.00 48482.80 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715
31 -1 48064.00 49064.00 1000.00 48514.00 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715
32 0 47550.80 48550.80 1000.00 48000.80 0.00 0.00 1000.00 0.00 0.00 550.00 0.00 0 0 3214.48 2447.715

View file

@ -16,7 +16,9 @@ class FamilyTest < ActiveSupport::TestCase
"date" => (Date.current + row["date_offset"].to_i.days).to_date,
"net_worth" => row["net_worth"],
"assets" => row["assets"],
"liabilities" => row["liabilities"]
"liabilities" => row["liabilities"],
"rolling_spend" => row["rolling_spend"],
"rolling_income" => row["rolling_income"]
}
end
end
@ -83,6 +85,22 @@ class FamilyTest < ActiveSupport::TestCase
end
end
test "should calculate transaction snapshot correctly" do
spending_series = @family.snapshot_transactions[:spending_series]
income_series = @family.snapshot_transactions[:income_series]
assert_equal @expected_snapshots.count, spending_series.values.count
assert_equal @expected_snapshots.count, income_series.values.count
@expected_snapshots.each_with_index do |row, index|
expected_spending = TimeSeries::Value.new(date: row["date"], value: Money.new(row["rolling_spend"].to_d))
expected_income = TimeSeries::Value.new(date: row["date"], value: Money.new(row["rolling_income"].to_d))
assert_in_delta expected_spending.value.amount, Money.new(spending_series.values[index].value).amount, 0.01
assert_in_delta expected_income.value.amount, Money.new(income_series.values[index].value).amount, 0.01
end
end
test "should exclude disabled accounts from calculations" do
assets_before = @family.assets
liabilities_before = @family.liabilities