mirror of
https://github.com/we-promise/sure.git
synced 2026-09-03 22:01:16 +00:00
* fix(goals): stop the days-left phrase splitting across lines The goal header read "Target 700 € by 08 de Febrer de 2027 · 184 days left" as one text node, so on a phone it wrapped wherever it ran out of room — "184 days" on the first line and "left" orphaned on the second. `header_summary_parts` now returns the dot-separated segments instead of a joined string, and the view renders each as its own span. Only the segments after the first are `whitespace-nowrap`: the first carries the target and a long-format date and has to stay free to wrap, or it would overflow a narrow screen. Checked at 320px, where the first segment takes two lines and "675 days left" still moves down whole. * test(goals): pin the clock for the days-left assertion The count is `target_date - Date.current`, and the target was set from `184.days.from_now` a moment earlier — a suite crossing midnight between the two would compute 183 and fail. Wrap it in travel_to, matching the idiom in budget_category_test and family_export_test. The neighbouring 'omits days left once reached' test asserts only the segment count, which 183 and 184 satisfy equally, so it is left alone.
515 lines
20 KiB
Ruby
515 lines
20 KiB
Ruby
require "test_helper"
|
||
|
||
class GoalTest < ActiveSupport::TestCase
|
||
include EntriesTestHelper
|
||
|
||
setup do
|
||
@family = families(:dylan_family)
|
||
@depository = accounts(:depository)
|
||
@connected = accounts(:connected)
|
||
@goal = goals(:vacation_italy)
|
||
end
|
||
|
||
test "valid fixture goal saves" do
|
||
assert @goal.valid?
|
||
end
|
||
|
||
# The days-left segment is returned separately so the view can keep it
|
||
# unbroken; joined into one string it wrapped after "days" on a phone.
|
||
test "header_summary_parts keeps the days-left phrase in its own segment" do
|
||
# Pinned: the count is `target_date - Date.current`, so a suite that crossed
|
||
# midnight between setting the date and reading it would report 183.
|
||
travel_to Date.new(2026, 6, 1) do
|
||
# Target well above the linked account's balance, or the goal reads as
|
||
# :reached and drops the days-left segment on purpose.
|
||
@goal.target_amount = 1_000_000
|
||
@goal.target_date = Date.current + 184
|
||
|
||
parts = @goal.header_summary_parts
|
||
|
||
assert_equal 2, parts.size
|
||
assert_match(/184 days left/, parts.last)
|
||
assert_no_match(/days left/, parts.first)
|
||
end
|
||
end
|
||
|
||
test "header_summary_parts omits days left once the target is reached" do
|
||
@goal.target_date = 184.days.from_now.to_date
|
||
@goal.complete!
|
||
|
||
assert_equal 1, @goal.header_summary_parts.size
|
||
end
|
||
|
||
test "header_summary_parts is a single segment without a target date" do
|
||
@goal.target_date = nil
|
||
|
||
assert_equal 1, @goal.header_summary_parts.size
|
||
end
|
||
|
||
test "name is required" do
|
||
@goal.name = ""
|
||
assert_not @goal.valid?
|
||
assert_includes @goal.errors[:name], "can't be blank"
|
||
end
|
||
|
||
test "target_amount must be positive" do
|
||
@goal.target_amount = 0
|
||
assert_not @goal.valid?
|
||
end
|
||
|
||
test "color must match hex format" do
|
||
@goal.color = "red; cursor: pointer"
|
||
assert_not @goal.valid?
|
||
assert_includes @goal.errors[:color], "is invalid"
|
||
end
|
||
|
||
test "color accepts standard 6-digit hex" do
|
||
@goal.color = "#abcdef"
|
||
assert @goal.valid?, @goal.errors.full_messages.to_sentence
|
||
end
|
||
|
||
test "display_status follows AASM state after pause! on the same instance" do
|
||
@goal.update!(color: "#4da568") if @goal.color.blank?
|
||
initial = @goal.display_status
|
||
@goal.pause!
|
||
assert_equal :paused, @goal.display_status, "stale memo would have returned #{initial.inspect}"
|
||
end
|
||
|
||
test "must have at least one linked account on create" do
|
||
new_goal = @family.goals.new(name: "Test", target_amount: 100, currency: "USD")
|
||
assert_not new_goal.valid?
|
||
assert_match(/at least one/i, new_goal.errors[:base].join)
|
||
end
|
||
|
||
test "investment accounts are fundable and default to the contributions basis" do
|
||
investment = accounts(:investment)
|
||
new_goal = @family.goals.new(name: "Inv", target_amount: 100, currency: "USD")
|
||
new_goal.goal_accounts.build(account: investment)
|
||
assert new_goal.valid?, new_goal.errors.full_messages.to_sentence
|
||
new_goal.save! # basis is set on save (before_save), not on valid?
|
||
assert_equal "contributions", new_goal.progress_basis
|
||
end
|
||
|
||
test "non-fundable account types are rejected" do
|
||
credit = accounts(:credit_card)
|
||
new_goal = @family.goals.new(name: "Test", target_amount: 100, currency: "USD")
|
||
new_goal.goal_accounts.build(account: credit)
|
||
assert_not new_goal.valid?
|
||
assert_includes new_goal.errors[:linked_accounts], "All linked accounts must be cash or investment accounts."
|
||
end
|
||
|
||
test "linked accounts must belong to family" do
|
||
other_family = Family.create!(name: "Other", currency: "USD", locale: "en", country: "US", timezone: "UTC")
|
||
foreign_account = Account.create!(
|
||
family: other_family,
|
||
accountable: Depository.new,
|
||
name: "Foreign",
|
||
currency: "USD",
|
||
balance: 100
|
||
)
|
||
new_goal = @family.goals.new(name: "T", target_amount: 100, currency: "USD")
|
||
new_goal.goal_accounts.build(account: foreign_account)
|
||
assert_not new_goal.valid?
|
||
assert_includes new_goal.errors[:linked_accounts], "Linked accounts must belong to the same family as the goal."
|
||
end
|
||
|
||
test "linked accounts must share currency with goal" do
|
||
eur_account = Account.create!(
|
||
family: @family,
|
||
accountable: Depository.new,
|
||
name: "Euro Cash",
|
||
currency: "EUR",
|
||
balance: 100
|
||
)
|
||
new_goal = @family.goals.new(name: "T", target_amount: 100, currency: "USD")
|
||
new_goal.goal_accounts.build(account: eur_account)
|
||
assert_not new_goal.valid?
|
||
assert_includes new_goal.errors[:linked_accounts], "All linked accounts must share the same currency."
|
||
end
|
||
|
||
test "currency can't change once linked accounts exist" do
|
||
assert @goal.linked_accounts.exists?
|
||
@goal.currency = "EUR"
|
||
assert_not @goal.valid?
|
||
assert_includes @goal.errors[:currency], "Can't change the currency after the goal is linked to accounts."
|
||
end
|
||
|
||
test "current_balance sums linked account balances" do
|
||
expected = @goal.linked_accounts.sum(&:balance).to_d
|
||
assert_equal expected, @goal.current_balance.to_d
|
||
end
|
||
|
||
test "progress_percent caps at 100" do
|
||
@goal.target_amount = 1
|
||
assert_equal 100, @goal.progress_percent
|
||
end
|
||
|
||
test "progress_percent stays below 100 while remaining amount is positive" do
|
||
account = Account.create!(
|
||
family: @family,
|
||
accountable: Depository.new,
|
||
name: "Almost There Savings",
|
||
currency: "USD",
|
||
balance: BigDecimal("999.50")
|
||
)
|
||
|
||
goal = @family.goals.create!(
|
||
name: "Almost There",
|
||
target_amount: BigDecimal("1000"),
|
||
currency: "USD"
|
||
) { |new_goal| new_goal.goal_accounts.build(account: account) }
|
||
|
||
assert_equal BigDecimal("0.5"), goal.remaining_amount
|
||
assert_equal 99, goal.progress_percent
|
||
assert_equal :no_target_date, goal.status
|
||
end
|
||
|
||
test "status stays reached for a goal completed while underfunded" do
|
||
account = Account.create!(
|
||
family: @family,
|
||
accountable: Depository.new,
|
||
name: "Completed Underfunded Savings",
|
||
currency: "USD",
|
||
balance: BigDecimal("999.50")
|
||
)
|
||
|
||
goal = @family.goals.create!(
|
||
name: "Completed Underfunded",
|
||
target_amount: BigDecimal("1000"),
|
||
currency: "USD"
|
||
) { |new_goal| new_goal.goal_accounts.build(account: account) }
|
||
|
||
goal.complete!
|
||
|
||
assert_equal BigDecimal("0.5"), goal.remaining_amount
|
||
assert_equal 100, goal.progress_percent
|
||
assert_equal :reached, Goal.find(goal.id).status
|
||
end
|
||
|
||
test "progress_percent is 0 for empty active goal" do
|
||
fresh = goals(:car_paydown)
|
||
fresh.update!(target_amount: 10_000)
|
||
fresh.linked_accounts.update_all(balance: 0)
|
||
# Refetch instead of poking @current_balance directly so the test
|
||
# exercises the real memo lifecycle (a request reads progress_percent
|
||
# on a freshly-loaded record after the underlying balances changed).
|
||
reloaded = Goal.find(fresh.id)
|
||
assert_equal 0, reloaded.progress_percent
|
||
end
|
||
|
||
test "remaining_amount is non-negative" do
|
||
@goal.target_amount = 1
|
||
assert_equal 0, @goal.remaining_amount
|
||
end
|
||
|
||
test "pace is zero on a goal whose linked accounts have no transactions" do
|
||
fresh_account = Account.create!(
|
||
family: @family,
|
||
accountable: Depository.new,
|
||
name: "Empty Savings",
|
||
currency: "USD",
|
||
balance: 0
|
||
)
|
||
fresh = @family.goals.create!(
|
||
name: "Fresh goal",
|
||
target_amount: 100,
|
||
currency: "USD"
|
||
) { |g| g.goal_accounts.build(account: fresh_account) }
|
||
|
||
assert_equal 0, fresh.pace.to_d
|
||
end
|
||
|
||
test "pace averages 90-day net inflow, excluding pending and excluded entries" do
|
||
account = Account.create!(
|
||
family: @family,
|
||
accountable: Depository.new,
|
||
name: "Pace Savings",
|
||
currency: "USD",
|
||
balance: 0
|
||
)
|
||
goal = @family.goals.create!(
|
||
name: "Pace goal",
|
||
target_amount: 10_000,
|
||
currency: "USD"
|
||
) { |g| g.goal_accounts.build(account: account) }
|
||
|
||
# Three inflows over the 90-day window. Sure convention: inflows are
|
||
# negative. Net = -900 → pace = 900 / 3 = 300.
|
||
create_transaction(account: account, amount: -300, date: 80.days.ago.to_date)
|
||
create_transaction(account: account, amount: -300, date: 40.days.ago.to_date)
|
||
create_transaction(account: account, amount: -300, date: 5.days.ago.to_date)
|
||
|
||
# Pending inflow that must be excluded by `Transaction.excluding_pending`.
|
||
pending_entry = create_transaction(account: account, amount: -1_000, date: 10.days.ago.to_date)
|
||
pending_entry.transaction.update!(extra: { "plaid" => { "pending" => true } })
|
||
|
||
# User-excluded outflow that must be excluded by `entries.excluded = false`.
|
||
excluded_entry = create_transaction(account: account, amount: 500, date: 20.days.ago.to_date)
|
||
excluded_entry.update!(excluded: true)
|
||
|
||
# Entry outside the 90-day window — must be ignored.
|
||
create_transaction(account: account, amount: -10_000, date: 200.days.ago.to_date)
|
||
|
||
assert_equal 300, goal.pace.to_d
|
||
end
|
||
|
||
test "months_of_runway is nil when goal has a target date" do
|
||
assert_not_nil @goal.target_date
|
||
assert_nil @goal.months_of_runway
|
||
end
|
||
|
||
test "months_of_runway is nil when pace is zero" do
|
||
fresh = goals(:emergency_fund)
|
||
assert_nil fresh.months_of_runway
|
||
end
|
||
|
||
test "AASM transitions" do
|
||
fresh = goals(:emergency_fund)
|
||
assert fresh.active?
|
||
fresh.pause!
|
||
assert fresh.paused?
|
||
fresh.resume!
|
||
assert fresh.active?
|
||
fresh.complete!
|
||
assert fresh.completed?
|
||
fresh.archive!
|
||
assert fresh.archived?
|
||
fresh.unarchive!
|
||
assert fresh.active?
|
||
end
|
||
|
||
test "status: reached when balance >= target" do
|
||
@goal.target_amount = 1
|
||
assert_equal :reached, @goal.status
|
||
end
|
||
|
||
test "status: no_target_date when target_date is nil" do
|
||
@goal.target_date = nil
|
||
@goal.target_amount = 10_000
|
||
@goal.linked_accounts.update_all(balance: 100)
|
||
assert_equal :no_target_date, @goal.status
|
||
end
|
||
|
||
test "display_status returns :archived for archived goal regardless of progress" do
|
||
@goal.save!
|
||
@goal.archive!
|
||
assert_equal :archived, @goal.display_status
|
||
end
|
||
|
||
test "display_status returns :paused for paused goal regardless of progress" do
|
||
@goal.save!
|
||
@goal.pause!
|
||
assert_equal :paused, @goal.display_status
|
||
end
|
||
|
||
test "display_status falls through to status for active goals" do
|
||
@goal.target_amount = 1
|
||
assert_equal :reached, @goal.display_status
|
||
end
|
||
|
||
test "advisory_lock_key_for is stable per family" do
|
||
k1 = Goal.advisory_lock_key_for(@family.id)
|
||
k2 = Goal.advisory_lock_key_for(@family.id)
|
||
assert_equal k1, k2
|
||
assert_kind_of Integer, k1
|
||
end
|
||
|
||
test "any_connected_account? reflects plaid_account presence" do
|
||
assert @goal.any_connected_account?
|
||
only_manual = goals(:emergency_fund)
|
||
only_manual.goal_accounts.where(account_id: @connected.id).destroy_all
|
||
assert_not only_manual.reload.any_connected_account?
|
||
end
|
||
|
||
test "pledge_action_label_key flips on manual-only goals" do
|
||
assert_equal "goals.show.pledge_just_transferred", @goal.pledge_action_label_key
|
||
@goal.goal_accounts.where(account_id: @connected.id).destroy_all
|
||
# After removing the only connected account, the goal is manual-only;
|
||
# the copy must flip to "pledge_just_saved" so users aren't told to
|
||
# wait for a sync that won't run. Refetch to exercise the real
|
||
# request lifecycle rather than poking a memo on the same instance.
|
||
reloaded = Goal.find(@goal.id)
|
||
assert_equal "goals.show.pledge_just_saved", reloaded.pledge_action_label_key
|
||
end
|
||
|
||
test "explicit allocation backs only the earmarked slice" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Split Savings", currency: "USD", balance: 5_000)
|
||
goal = @family.goals.create!(name: "Earmarked", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 1_000)
|
||
end
|
||
assert_equal BigDecimal("1000"), goal.current_balance.to_d
|
||
end
|
||
|
||
test "explicit allocation is capped at the account balance via the haircut" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Over Earmark", currency: "USD", balance: 800)
|
||
goal = @family.goals.create!(name: "Over", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 5_000)
|
||
end
|
||
assert_equal BigDecimal("800"), goal.current_balance.to_d
|
||
end
|
||
|
||
test "unallocated link claims the balance left after another goal's earmark" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Shared Savings", currency: "USD", balance: 5_000)
|
||
earmarked = @family.goals.create!(name: "Earmarked", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 2_000)
|
||
end
|
||
whole = @family.goals.create!(name: "Whole", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account) # NULL = whole-balance remainder
|
||
end
|
||
assert_equal BigDecimal("2000"), earmarked.current_balance.to_d
|
||
assert_equal BigDecimal("3000"), whole.current_balance.to_d
|
||
# The two goals' shares of the shared account never exceed its balance.
|
||
assert_equal account.balance.to_d, earmarked.current_balance.to_d + whole.current_balance.to_d
|
||
end
|
||
|
||
test "over-earmarked account scales fixed slices pro-rata" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Contested Savings", currency: "USD", balance: 5_000)
|
||
a = @family.goals.create!(name: "Goal A", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 4_000)
|
||
end
|
||
b = @family.goals.create!(name: "Goal B", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 4_000)
|
||
end
|
||
# sum_fixed 8000 > balance 5000 -> each scaled by 5000/8000 -> 2500.
|
||
assert_equal BigDecimal("2500"), a.current_balance.to_d
|
||
assert_equal BigDecimal("2500"), b.current_balance.to_d
|
||
assert_equal account.balance.to_d, a.current_balance.to_d + b.current_balance.to_d
|
||
end
|
||
|
||
test "archived goals release their earmark from the shared pool" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Release Savings", currency: "USD", balance: 5_000)
|
||
whole = @family.goals.create!(name: "Whole", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account)
|
||
end
|
||
earmarked = @family.goals.create!(name: "Earmarked", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 2_000)
|
||
end
|
||
assert_equal BigDecimal("3000"), Goal.find(whole.id).current_balance.to_d
|
||
earmarked.archive!
|
||
# Archived goal no longer reserves its slice -> whole reclaims it.
|
||
assert_equal BigDecimal("5000"), Goal.find(whole.id).current_balance.to_d
|
||
end
|
||
|
||
test "account free_to_earmark subtracts non-archived fixed earmarks" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Headroom Savings", currency: "USD", balance: 5_000)
|
||
@family.goals.create!(name: "Earmarker", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 1_500)
|
||
end
|
||
assert_equal BigDecimal("1500"), account.goal_earmarked_total
|
||
assert_equal BigDecimal("3500"), account.free_to_earmark
|
||
end
|
||
|
||
test "an overdrawn account backs nothing for fixed or whole-balance links" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Overdrawn", currency: "USD", balance: BigDecimal("-100"))
|
||
fixed = @family.goals.create!(name: "Fixed OD", target_amount: 1_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 50)
|
||
end
|
||
whole = @family.goals.create!(name: "Whole OD", target_amount: 1_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account)
|
||
end
|
||
assert_equal 0.to_d, fixed.current_balance.to_d
|
||
assert_equal 0.to_d, whole.current_balance.to_d
|
||
end
|
||
|
||
test "an archived goal still shows its own earmark, not the whole balance" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Archived Earmark", currency: "USD", balance: 5_000)
|
||
earmarked = @family.goals.create!(name: "Archived Fixed", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 2_000)
|
||
end
|
||
earmarked.archive!
|
||
# Excluded from the shared pool, but its own earmark is read from its own
|
||
# goal_accounts — so it still reports 2,000, not the whole 5,000.
|
||
assert_equal BigDecimal("2000"), Goal.find(earmarked.id).current_balance.to_d
|
||
end
|
||
|
||
test "earmark edits to an existing linked account persist via goal.save!" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Autosave Savings", currency: "USD", balance: 5_000)
|
||
goal = @family.goals.create!(name: "Autosave goal", target_amount: 10_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account) # NULL = whole balance
|
||
end
|
||
ga = goal.goal_accounts.first
|
||
assert_nil ga.allocated_amount
|
||
ga.allocated_amount = 1_500
|
||
goal.save! # autosave: true must persist the dirty existing child
|
||
assert_equal BigDecimal("1500"), goal.goal_accounts.first.reload.allocated_amount
|
||
end
|
||
|
||
test "progress_percent memo resets after complete! on the same instance" do
|
||
account = Account.create!(family: @family, accountable: Depository.new, name: "Memo Savings", currency: "USD", balance: 100)
|
||
goal = @family.goals.create!(name: "Memo goal", target_amount: 1_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account)
|
||
end
|
||
assert_operator goal.progress_percent, :<, 100 # memoize the underfunded value
|
||
goal.complete!
|
||
assert_equal 100, goal.progress_percent, "stale memo would still report the pre-complete percent"
|
||
end
|
||
|
||
test "contributions basis excludes market gains" do
|
||
account = Account.create!(family: @family, accountable: Investment.new, name: "Brokerage", currency: "USD", balance: 10_000)
|
||
account.balances.create!(date: 10.days.ago.to_date, balance: 10_000, currency: "USD", net_market_flows: 3_000)
|
||
goal = @family.goals.create!(name: "Invest goal", target_amount: 20_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account)
|
||
end
|
||
assert_equal "contributions", goal.progress_basis
|
||
# 10,000 value − 3,000 market gain = 7,000 contributed.
|
||
assert_equal BigDecimal("7000"), goal.current_balance.to_d
|
||
assert_equal BigDecimal("10000"), goal.market_value_money.amount
|
||
end
|
||
|
||
test "reopen transitions a completed goal back to active" do
|
||
fresh = goals(:emergency_fund)
|
||
fresh.complete!
|
||
assert fresh.completed?
|
||
fresh.reopen!
|
||
assert fresh.active?
|
||
end
|
||
|
||
test "investment accounts default to transfer pledge kind, never manual_save" do
|
||
assert_equal "transfer", accounts(:investment).default_pledge_kind
|
||
end
|
||
|
||
test "adding an investment account via update flips a depository goal to contributions" do
|
||
goal = goals(:emergency_fund)
|
||
assert_equal "balance", goal.progress_basis
|
||
goal.goal_accounts.build(account: accounts(:investment))
|
||
goal.save!
|
||
assert_equal "contributions", goal.reload.progress_basis
|
||
end
|
||
|
||
test "earmark is respected on a contributions-basis goal" do
|
||
account = Account.create!(family: @family, accountable: Investment.new, name: "Brokerage2", currency: "USD", balance: 10_000)
|
||
account.balances.create!(date: 5.days.ago.to_date, balance: 10_000, currency: "USD", net_market_flows: 2_000)
|
||
goal = @family.goals.create!(name: "Earmarked invest", target_amount: 20_000, currency: "USD") do |g|
|
||
g.goal_accounts.build(account: account, allocated_amount: 1_000)
|
||
end
|
||
assert_equal "contributions", goal.progress_basis
|
||
# net contributed = 10,000 − 2,000 = 8,000; earmark 1,000 ≤ 8,000 → 1,000.
|
||
assert_equal BigDecimal("1000"), goal.current_balance.to_d
|
||
end
|
||
|
||
test "behind_pace? excludes paused goals even when their raw status is behind" do
|
||
goal = goals(:vacation_italy)
|
||
goal.stubs(:status).returns(:behind)
|
||
|
||
assert goal.behind_pace?
|
||
|
||
goal.update!(state: "paused")
|
||
|
||
assert goal.paused?
|
||
assert_not goal.behind_pace?
|
||
end
|
||
|
||
test "summary_for counts behind goals via behind_pace? and sums one currency" do
|
||
behind = goals(:vacation_italy)
|
||
behind.stubs(:status).returns(:behind)
|
||
paused_behind = goals(:emergency_fund)
|
||
paused_behind.update!(state: "paused")
|
||
paused_behind.stubs(:status).returns(:behind)
|
||
|
||
summary = Goal.summary_for([ behind, paused_behind ], currency: "USD")
|
||
|
||
assert_equal 1, summary[:behind_count]
|
||
assert_kind_of Money, summary[:saved_money]
|
||
end
|
||
end
|