mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
fix(goals): address AI review on PR #1798 (CodeRabbit + Codex)
Correctness: - GoalPledge#matches? rejects outflows on transfer pledges so a +$200 purchase no longer satisfies a $200 deposit pledge after .abs - GoalsController#sync_linked_accounts! saves through the goal so currency/depository/family validations actually run on update - AlreadyClaimedError replaces empty RecordInvalid in resolve_with! and reconciler rescues the dedicated class - SweepExpiredGoalPledgesJob wraps each expire! in a per-record rescue - Assistant::Function::CreateGoal disambiguates duplicate account names and returns an absolute URL via mailer host config - Family#savings_inflow_velocity defensively scopes from the family's accounts (was Account.joins(:goal_accounts).where(goal_id: ...)) - GoalPledgesController#set_goal preloads linked_accounts + providers to drop the N+1 on any_connected_account? - Stepper subtitle update walks to the enclosing dialog before querySelector so two stepper instances don't fight over one header - categories/_form.html.erb data-action targets color-icon-picker, not the non-existent "category" controller UX / visual: - Projection chart drops preserveAspectRatio="none" and pins endDate at today for past-due goals so the today marker stays in-domain - _color_picker / categories form swap non-standard border-1 for border - Goals index search input uses ring-alpha-black-100 (was raw gray-500) Refactors: - Goal#header_summary extracts the multi-line ERB header block - Goal#catch_up_delta_money sums open_pledges in SQL - Goal#projection_summary uses I18n.l for the on-track month label - Account#default_pledge_kind moves the manual/transfer decision out of GoalPledgesController - GoalPledge::Reconciler iterates ordered (created_at, id) so first-claim wins is deterministic under non-sequential PKs - Goals::FundingAccountsBreakdownComponent + Goals::AccountStackComponent use clamp(0..) instead of Float::INFINITY / [x, 0].max - Goals::StatusPillComponent#label provides a titleize fallback - Goal projection chart skips the redundant initial _draw and reuses the snapped point in the past branch (no double-bisect) - Goal pledge preview drops maximumFractionDigits: 0 so USD/EUR show cents while JPY/KRW stay whole-unit - Demo generator captures the Wedding fund goal in the seed loop instead of looking it up by hardcoded name Tests: - GoalPledgeTest: outflow rejection - GoalsControllerTest: cross-currency attachment rejected on update - SweepExpiredGoalPledgesJobTest: cancelled coverage + per-record rescue - GoalTest: pledge_action_label_key flips to manual_save without an unconditional guard
This commit is contained in:
@@ -82,8 +82,8 @@ class Assistant::Function::CreateGoal < Assistant::Function
|
||||
)
|
||||
end
|
||||
|
||||
matched = family.accounts.where(accountable_type: "Depository").visible.where(name: linked_account_names).to_a
|
||||
missing = linked_account_names - matched.map(&:name)
|
||||
available = family.accounts.where(accountable_type: "Depository").visible.where(name: linked_account_names)
|
||||
missing = linked_account_names - available.pluck(:name).uniq
|
||||
if missing.any?
|
||||
return error(
|
||||
"unknown_accounts",
|
||||
@@ -93,6 +93,22 @@ class Assistant::Function::CreateGoal < Assistant::Function
|
||||
)
|
||||
end
|
||||
|
||||
# Multiple accounts can share a name. Block silent over-linking by
|
||||
# surfacing the ambiguity so the assistant re-asks with disambiguated
|
||||
# input rather than attaching every same-named account to the goal.
|
||||
grouped = available.group_by(&:name)
|
||||
ambiguous_names = grouped.select { |_, accts| accts.size > 1 }.keys
|
||||
if ambiguous_names.any?
|
||||
return error(
|
||||
"ambiguous_accounts",
|
||||
"Multiple accounts share a name. Ask the user which one to use.",
|
||||
ambiguous_names: ambiguous_names,
|
||||
available_accounts: depository_account_payload
|
||||
)
|
||||
end
|
||||
|
||||
matched = linked_account_names.map { |name| grouped[name].first }
|
||||
|
||||
currencies = matched.map(&:currency).uniq
|
||||
if currencies.size > 1
|
||||
return error(
|
||||
@@ -122,15 +138,28 @@ class Assistant::Function::CreateGoal < Assistant::Function
|
||||
target_amount_formatted: goal.target_amount_money.format,
|
||||
currency: goal.currency,
|
||||
target_date: goal.target_date&.iso8601,
|
||||
url: Rails.application.routes.url_helpers.goal_path(goal),
|
||||
url: absolute_url_for(goal),
|
||||
linked_account_names: matched.map(&:name),
|
||||
message: "Created goal '#{goal.name}' (target #{goal.target_amount_money.format}). View it at #{Rails.application.routes.url_helpers.goal_path(goal)}."
|
||||
message: "Created goal '#{goal.name}' (target #{goal.target_amount_money.format}). View it at #{absolute_url_for(goal)}."
|
||||
}
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
error("validation_failed", e.record.errors.full_messages.join("; "))
|
||||
end
|
||||
|
||||
private
|
||||
# Build an absolute URL for the new goal so chat clients (which render
|
||||
# outside the request that produced the goal) can link directly. Falls
|
||||
# back to the relative path when no host is configured (e.g. self-hosted
|
||||
# in a job without ENV).
|
||||
def absolute_url_for(goal)
|
||||
host_opts = Rails.application.config.action_mailer.default_url_options || {}
|
||||
if host_opts[:host].present?
|
||||
Rails.application.routes.url_helpers.goal_url(goal, host_opts)
|
||||
else
|
||||
Rails.application.routes.url_helpers.goal_path(goal)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_decimal(value)
|
||||
return nil if value.nil?
|
||||
BigDecimal(value.to_s)
|
||||
|
||||
Reference in New Issue
Block a user