mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 15:59:02 +00:00
Second pass on user-facing strings after the em-dash sweep and
yellow-pill demotion. Voice/abbreviation/edge-value parity.
Voice consistency:
- `index.pending_pledges_callout` reframed from "Sure is watching
your linked accounts" (system-as-watcher voice) to "You have
pending pledges. Sure will confirm them on the next sync."
(user-actor, system-action). Matches the surrounding
user-centric voice on the KPI strip and the helper-text pattern
("Sure will look for…", "Sure will catch it") used elsewhere.
- `goal_pledges.new.helper_manual` flipped pronoun "We'll record"
to "Sure will record" so the modal's two helper lines share a
single narrator. The transfer-helper already says "Sure will
look for"; this matches.
- `form_stepper.errors.*` dropped the apologetic "Please …" voice
("Please give your goal a name.") for the terse imperative
the rest of the feature uses ("Give your goal a name." / "Set
a target above zero." / "Pick at least one funding account.").
Parallelism:
- `kpi.velocity_delta_zero_base` was the only `velocity_delta_*`
string spelling out "30 days" while siblings used `30d`. Switch
to "First 30d of activity" so the sub-tile reads in one unit.
- `Depository` titlecase in `at_least_one_linked_account_required`,
`must_be_depository`, and `no_depository_accounts` collapsed to
lowercase. Common noun, not a UI label. Matches the empty-state
body in `funding_accounts.empty.body` which was already lowercase.
Test fixture for `must_be_depository` updated.
- `projection.reached` was the same string as `celebration.heading`
("Goal reached. Nice work."), making the celebration moment feel
templated. The projection slot is the chart's empty state when
there's nothing to project; rephrase to "You've hit the target.
No projection needed." Celebration keeps the warm tone.
Edge value:
- `celebration.body` was "You hit your $X target." When the user
marks a goal complete at sub-100% (a flow the new
`confirm_complete_body_short` already warns about), this lied
about the achievement. Rewrite to "Goal closed at %{saved} of
%{target}. Keep it as a record, or archive it now." Interpolation
now passes both `saved` and `target` from the show template, so
the celebration card honors the actual saved amount whether the
user hit, overshot, or stopped short.
Notes deferred (verify-only, not string changes):
- `goal_card.footer_catch_up` is interpolated with
`catch_up_delta_money` in `CardComponent#footer_line`; the show-
page guard `.amount.positive?` already lives there. No copy
change needed.
- `pending_pledge.title.zero` bucket fires only when `count: 0`
reaches the I18n call; `GoalPledge#days_left` clamps at 0, so
the friendlier "expires today" copy is reachable.
- `paused_banner.title` / `inactive.heading_paused` duplicate
strings noted but left in place; consolidation is a separate
refactor.
193 lines
6.0 KiB
Ruby
193 lines
6.0 KiB
Ruby
require "test_helper"
|
|
|
|
class GoalTest < ActiveSupport::TestCase
|
|
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
|
|
|
|
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 "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 "linked accounts must be depository" do
|
|
investment = accounts(:investment)
|
|
new_goal = @family.goals.new(name: "Test", target_amount: 100, currency: "USD")
|
|
new_goal.goal_accounts.build(account: investment)
|
|
assert_not new_goal.valid?
|
|
assert_includes new_goal.errors[:linked_accounts], "All linked accounts must be depository (checking, savings, HSA, CD, money-market)."
|
|
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 is 0 for empty active goal" do
|
|
fresh = goals(:car_paydown)
|
|
fresh.target_amount = 10_000
|
|
fresh.linked_accounts.update_all(balance: 0)
|
|
fresh.instance_variable_set(:@current_balance, nil)
|
|
fresh.linked_accounts.reload
|
|
assert_equal 0, fresh.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 "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
|
|
@goal.reload
|
|
@goal.instance_variable_set(:@current_balance, nil)
|
|
assert_equal "goals.show.pledge_just_transferred", @goal.pledge_action_label_key if @goal.linked_accounts.any?(&:plaid_account)
|
|
end
|
|
end
|