mirror of
https://github.com/we-promise/sure.git
synced 2026-09-08 16:14:23 +00:00
* feat(goals): show what each account still has room to earmark `Account#free_to_earmark` has existed, unused, since earmarks shipped — its own comment said the UI was a follow-up. This is that follow-up, and the wording is the substance of it. It does not say "over-allocated". `free_to_earmark` is negative for as long as the saving is unfinished, which is the normal condition of anyone with goals in progress: a 6,000 account backing two goals of 5,000 gives −4,000 and is a perfectly correct setup. A warning phrased as a fault would fire permanently and teach people to ignore it. The message states the consequence instead — the goals come to X for a balance of Y, so they progress pro rata — and is never styled as an error. The trap is the goal being edited. `goal_earmarked_total` counts every goal including that one, so reopening a goal that earmarks 5,000 on a 6,000 account shows 1,000 of headroom, and re-entering the same 5,000 trips a message about a setup the user has not touched. `earmarked_by_other_goals` excludes it, and only when it is persisted — a goal being created has nothing to exclude. The pool is read once per render and passed down, never per account: the form lists every fundable account the user can see. A test counts the query and fails at two. The Stimulus controller is its own, with 3 targets. goal_form_controller is at 10 against the 7 the project guidelines suggest, needs none of this state, and is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DJ1npaGEHr6t2HW1rYZdt4 * fix(goals): read the typed amount strictly, and format it in the app's locale Addresses review feedback on #3166. `Number.parseFloat` accepts prefixes, so "500abc" became 500, and the bare comma-to-dot swap turned a thousands-separated "1,500" into 1.5. Either way the preview described an amount the user had not typed — and the second case is a habit from another locale, not a typo, so it would have gone unnoticed. The value now has to match a complete number before anything is computed. `Intl.NumberFormat(undefined, ...)` let the BROWSER pick the locale, so a French user on an English-locale browser read separators and symbol placement matching nothing else on the page. The amounts cannot be formatted server-side — they change with every keystroke — so the server passes `I18n.locale` and the client applies it. That puts the decision where the rest of the app's formatting already lives. bin/rails test: 6954 runs, 0 failures. RuboCop, erb_lint and biome clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * fix(goals): let the assistant create a second goal on a claimed account Review on #3166. The function always built whole-account links and had no way to express an earmark, so once exclusivity landed, asking for a second goal on an account another goal already claimed came back as a bare `validation_failed` — while the account list still advertised the account as available. A common request became an unexplained refusal. Three changes, and the list is the important one: it now says what is left on each account and which are claimed in full, because the assistant reasons from that list and had no way to know otherwise. `earmarks` is an optional map of account name to amount, so the assistant can reserve a slice rather than the whole balance. Accounts left out keep the previous behaviour and take whatever is spare. The refusal is named before the save — `account_claimed_in_full`, with the account names — so the assistant gets a reason it can act on and ask about, rather than a validation message it can only relay. Checked after the currency check, which is the more fundamental of the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * test(goals): move the spend tests back out of the private section The merge of `main` into this branch landed #3176's tests between `count_pool_queries` and the helpers below it, inside the `private` section and at the wrong indentation. `ci / lint` has been failing on `Layout/IndentationConsistency` since. They still ran — `test` is a class method, so `private` does not hide them — which is why the unit job stayed green while lint went red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
75 lines
2.3 KiB
Ruby
75 lines
2.3 KiB
Ruby
require "test_helper"
|
|
|
|
class GoalsHelperTest < ActionView::TestCase
|
|
include GoalsHelper
|
|
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@account = Account.create!(
|
|
family: @family, accountable: Depository.new,
|
|
name: "Helper Savings", currency: "USD", balance: 6_000
|
|
)
|
|
end
|
|
|
|
test "sums the fixed earmarks other goals hold on the account" do
|
|
build_goal("A", 2_000)
|
|
build_goal("B", 1_500)
|
|
|
|
assert_equal BigDecimal("3500"), earmarked_by_other_goals(@account, pooled: pooled)
|
|
end
|
|
|
|
# The whole point of the helper: reopening a goal must not count itself.
|
|
test "excludes the goal currently being edited" do
|
|
mine = build_goal("Mine", 5_000)
|
|
build_goal("Theirs", 500)
|
|
|
|
assert_equal BigDecimal("500"), earmarked_by_other_goals(@account, pooled: pooled, current_goal: mine)
|
|
end
|
|
|
|
test "a goal that does not exist yet excludes nothing" do
|
|
build_goal("Existing", 2_000)
|
|
unsaved = @family.goals.new(name: "New", target_amount: 1_000, currency: "USD")
|
|
|
|
assert_equal BigDecimal("2000"), earmarked_by_other_goals(@account, pooled: pooled, current_goal: unsaved)
|
|
end
|
|
|
|
test "whole-balance links reserve no fixed slice" do
|
|
build_goal("Whole", nil)
|
|
|
|
assert_equal BigDecimal("0"), earmarked_by_other_goals(@account, pooled: pooled)
|
|
end
|
|
|
|
test "an account no goal touches has nothing earmarked" do
|
|
untouched = Account.create!(
|
|
family: @family, accountable: Depository.new,
|
|
name: "Untouched", currency: "USD", balance: 1_000
|
|
)
|
|
|
|
assert_equal BigDecimal("0"), earmarked_by_other_goals(untouched, pooled: pooled)
|
|
end
|
|
|
|
# Depends on Lot B1: both released states drop out of the pool, so neither
|
|
# can inflate the headroom warning.
|
|
test "archived and completed goals are absent from the pool" do
|
|
archived = build_goal("Archived", 1_000)
|
|
completed = build_goal("Completed", 1_000)
|
|
build_goal("Live", 750)
|
|
|
|
archived.archive!
|
|
completed.complete!
|
|
|
|
assert_equal BigDecimal("750"), earmarked_by_other_goals(@account, pooled: pooled)
|
|
end
|
|
|
|
private
|
|
def build_goal(name, allocated)
|
|
@family.goals.create!(name: name, target_amount: 10_000, currency: "USD") do |g|
|
|
g.goal_accounts.build(account: @account, allocated_amount: allocated)
|
|
end
|
|
end
|
|
|
|
def pooled
|
|
Goal.pooled_allocations_for(@family)
|
|
end
|
|
end
|