mirror of
https://github.com/we-promise/sure.git
synced 2026-06-01 00:39:01 +00:00
fix(goals): catch-up subtracts pending pledges from the demand
UX audit finding. The catch-up alert demanded $X/mo without accounting for pledges the user had already recorded. The user recorded a $20k pledge → catch-up still demanded a fresh $20k → double-counting → stacked yellow CTAs telling them to do the thing they'd just done. Goal#catch_up_delta_money now subtracts `open_pledges.sum(amount)` from the demand: delta = max(monthly_target − pace − sum(open_pledges), 0) Uses the in-memory preloaded `open_pledges` collection (controllers already eager-load it), so no extra query. The clamp at zero keeps "$0/mo more" from rendering when pending pledges fully cover the gap. Alert branch in show.html.erb now also gates on `@goal.catch_up_delta_money.amount.positive?` — when the demand zeroes out via pending pledges, suppress the alert entirely. Status pill stays `:behind` (because `pace < required`), but the action surface goes quiet because the user already took it.
This commit is contained in:
@@ -281,13 +281,17 @@ class Goal < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
# Monthly extra needed beyond the current pace to hit the target on time.
|
||||
# Clamps at zero — never asks the user to "make up" a deficit they're
|
||||
# already ahead of.
|
||||
# Monthly extra needed beyond the current pace + currently-open pledges
|
||||
# to hit the target on time. Pending pledges are approximate (one-off
|
||||
# amounts treated as this-month inflow) but excluding them produced the
|
||||
# bad case where the alert demanded $X/mo while the user had already
|
||||
# pledged $X — telling them to act on top of the action they just took.
|
||||
# Clamps at zero so a fully-covered goal doesn't surface a $0 demand.
|
||||
def catch_up_delta_money
|
||||
return Money.new(0, currency) if monthly_target_amount.nil?
|
||||
|
||||
delta = [ monthly_target_amount.to_d - pace.to_d, 0 ].max
|
||||
pending = open_pledges.to_a.sum { |p| p.amount.to_d }
|
||||
delta = [ monthly_target_amount.to_d - pace.to_d - pending, 0 ].max
|
||||
Money.new(delta, currency)
|
||||
end
|
||||
|
||||
|
||||
@@ -139,11 +139,13 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% elsif @goal.status == :behind && @goal.monthly_target_amount %>
|
||||
<% elsif @goal.status == :behind && @goal.monthly_target_amount && @goal.catch_up_delta_money.amount.positive? %>
|
||||
<%# Title uses the *delta* — the amount the user must add to current pace
|
||||
to make the date. The body carries the absolute numbers. Pre-fill the
|
||||
to make the date, *after* subtracting open pledges. When pending
|
||||
pledges already cover the gap, `catch_up_delta_money` returns zero
|
||||
and this branch suppresses — no "Save $0/mo" demand. Pre-fill the
|
||||
pledge CTA with the delta too, so submitting once funds the gap
|
||||
instead of double-counting on top of the existing pace. %>
|
||||
instead of double-counting on top of pace and pending. %>
|
||||
<%= render DS::Alert.new(variant: "warning", title: t("goals.show.catch_up.title", amount: @goal.catch_up_delta_money.format(precision: 0))) do %>
|
||||
<p class="text-secondary">
|
||||
<%= t("goals.show.catch_up.body", avg: @goal.pace_money.format(precision: 0), required: Money.new(@goal.monthly_target_amount, @goal.currency).format(precision: 0)) %>
|
||||
|
||||
Reference in New Issue
Block a user