From 8e9a697b1f6c8afae3a4198973e17653df123cd7 Mon Sep 17 00:00:00 2001 From: Guillem Arias Date: Thu, 14 May 2026 21:50:54 +0200 Subject: [PATCH] fix(goals): months_remaining uses day-precision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PF audit edge case. Calendar-month math undercounted near the deadline: May 30 with a June 1 target returned `1` ("save $5k this month"), then June 1 morning returned `0` (falls through to `remaining_amount` charged as one-month-required). Users saw a $5k/mo required rate for a 2-day window, then $5k flat on deadline day — a cliff that doesn't match reality. Replace calendar-month delta with `(target_date - Date.current) / 30.0` so a 2-day-out deadline reports ~0.07 months and `monthly_target_amount` scales proportionally. `[..., 0.0].max` keeps the past-due case zero-clamped. --- app/models/goal.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/goal.rb b/app/models/goal.rb index a8e8beb62..daea1e73f 100644 --- a/app/models/goal.rb +++ b/app/models/goal.rb @@ -92,11 +92,16 @@ class Goal < ApplicationRecord end end + # Day-precision so the near-deadline cliff doesn't kick in: at + # calendar-month precision, May 30 → June 1 returned 1 ("save $5k this + # month") then June 1 → June 1 returned 0 (falls through to + # "remaining_amount in one month"). Now a 2-day-out deadline reports + # ~0.07 months and `monthly_target_amount` scales accordingly. def months_remaining return nil unless target_date - months = (target_date.year - Date.current.year) * 12 + (target_date.month - Date.current.month) - [ months, 0 ].max + days = (target_date - Date.current).to_i + [ (days / 30.0), 0.0 ].max end def monthly_target_amount @@ -107,7 +112,7 @@ class Goal < ApplicationRecord elsif months_remaining.zero? remaining_amount else - (remaining_amount.to_d / months_remaining).ceil(2) + (remaining_amount.to_d / months_remaining.to_d).ceil(2) end end