feat(goals/edit): funding-accounts editor in the edit modal

Previously a user who linked the wrong account at creation had to
delete + recreate the goal. Now the edit modal carries the same
funding-accounts checkbox group as Step 1 of the stepper, pre-checked
with the goal's current links.

- GoalsController#edit loads @linkable_accounts + @currently_linked_account_ids.
- #update accepts account_ids; when supplied, runs the create / update
  inside a Goal.transaction and syncs linked accounts via
  sync_linked_accounts! (set-diff: destroy_all unselected goal_accounts,
  create the new ones). Validates at least one account before touching
  goal_accounts so the user gets a clean re-render.
- Removing an account preserves the goal's existing contributions —
  GoalContribution#account_must_be_linked_to_goal only fires on save,
  so historical rows stay valid.
- _form_edit partial accepts new locals; edit.html.erb threads them
  through.
- 3 new controller tests: identity-only patch leaves links intact;
  account_ids patch replaces the link set; empty account_ids
  re-renders with error.
This commit is contained in:
Guillem Arias
2026-05-11 20:28:45 +02:00
parent e179abd0b3
commit f4b360bb96
4 changed files with 91 additions and 11 deletions

View File

@@ -71,20 +71,39 @@ class GoalsController < ApplicationController
end
def edit
@linkable_accounts = linkable_accounts_for_new
@currently_linked_account_ids = @goal.goal_accounts.pluck(:account_id).map(&:to_s)
end
def update
if @goal.update(goal_update_params)
flash[:notice] = t(".success")
respond_to do |format|
format.html { redirect_to goal_path(@goal) }
format.turbo_stream do
render turbo_stream: turbo_stream.action(:redirect, goal_path(@goal))
end
end
else
account_ids = params.dig(:goal, :account_ids)
accounts_supplied = !account_ids.nil?
accounts = accounts_supplied ? lookup_accounts(account_ids) : []
if accounts_supplied && accounts.empty?
@goal.errors.add(:base, :at_least_one_linked_account_required)
@linkable_accounts = linkable_accounts_for_new
@currently_linked_account_ids = @goal.goal_accounts.pluck(:account_id).map(&:to_s)
render :edit, status: :unprocessable_entity
return
end
Goal.transaction do
@goal.update!(goal_update_params)
sync_linked_accounts!(@goal, accounts) if accounts_supplied
end
flash[:notice] = t(".success")
respond_to do |format|
format.html { redirect_to goal_path(@goal) }
format.turbo_stream do
render turbo_stream: turbo_stream.action(:redirect, goal_path(@goal))
end
end
rescue ActiveRecord::RecordInvalid
@linkable_accounts = linkable_accounts_for_new
@currently_linked_account_ids = @goal.goal_accounts.pluck(:account_id).map(&:to_s)
render :edit, status: :unprocessable_entity
end
def destroy
@@ -148,6 +167,18 @@ class GoalsController < ApplicationController
Current.family.accounts.where(accountable_type: "Depository").visible.alphabetically.to_a
end
def sync_linked_accounts!(goal, accounts)
desired = accounts.map(&:id).to_set
current = goal.goal_accounts.pluck(:account_id).to_set
(current - desired).each do |id|
goal.goal_accounts.where(account_id: id).destroy_all
end
(desired - current).each do |id|
goal.goal_accounts.create!(account_id: id)
end
end
def create_initial_contribution_if_provided!(goal, accounts)
amount = params.dig(:goal, :initial_contribution_amount)
account_id = params.dig(:goal, :initial_contribution_account_id)