carlos / platform Public

console: closed-month CE settle-and-stop must not fire from the month's own last pull

shouldSkipCEPull treated existing.PulledAt.Day() >= billCEDayGrace as
proof a closed month had settled, but a month's own last daily pull
(e.g. July 31, Day()==31) satisfies that by calendar coincidence, the
moment the month closes — permanently skipping CE's ~24h-lagged final
line items for every month, forever.

Require the settling pull to have happened in a LATER month AND on day
>= billCEDayGrace of it: usage.MonthOf(existing.PulledAt) >
existing.Month (string compare, safe for "2006-01"). Adds the missing
regression: a bill for July with PulledAt July 31, billPass ticking on
Aug 1, must still re-pull July. The existing settled-case test (PulledAt
Aug 5) still passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell d293ca61154408b1d2662abbfa70c683a03af30b parent a4a12c8
2 files changed, +57 −6
  • internal/console/economicsbill_test.go +46 −0
  • internal/console/economicspass.go +11 −6
diff --git a/internal/console/economicsbill_test.go b/internal/console/economicsbill_test.go
index 13e3e84be..901639a1c 100644
--- a/internal/console/economicsbill_test.go
+++ b/internal/console/economicsbill_test.go
@@ -209,6 +209,52 @@ func TestBillPassStopsPullingSettledClosedMonth(t *testing.T) {
}
}
+// TestBillPassRepullsJustClosedMonth is the regression for the "own last
+// daily pull looks settled" bug: a month's last pull WHILE STILL OPEN (e.g.
+// July 31, Day()==31) must not be mistaken for the day-3-of-the-FOLLOWING-
+// month settle-and-stop rule just because Day() happens to be >=
+// billCEDayGrace. The moment the month closes (billPass ticks on Aug 1) it
+// must still be re-pulled — CE's ~24h-lagged final line items for July
+// haven't landed yet at the instant July turns into August.
+func TestBillPassRepullsJustClosedMonth(t *testing.T) {
+ fx := newConsoleFixture(t)
+ ctx := context.Background()
+
+ notYetSettled := economics.Bill{
+ Schema: 1, Month: "2026-07", TotalUSD: 10,
+ Services: map[string]float64{"AmazonEC2": 10}, Source: economics.SourceCostExplorer,
+ PulledAt: time.Date(2026, 7, 31, 3, 0, 0, 0, time.UTC), // the month's own last daily pull
+ }
+ putStoreJSON(t, fx.rel, economics.BillKey("2026-07"), notYetSettled)
+
+ var julyPulled bool
+ fx.srv.ceCosts = func(ctx context.Context, start, end time.Time) (map[string]float64, error) {
+ if start.Format("2006-01") == "2026-07" {
+ julyPulled = true
+ }
+ return map[string]float64{"AmazonEC2": 99}, nil
+ }
+
+ if err := fx.srv.billPass(ctx, time.Date(2026, 8, 1, 3, 0, 0, 0, time.UTC)); err != nil {
+ t.Fatal(err)
+ }
+ if !julyPulled {
+ t.Fatal("billPass did not re-pull July on Aug 1, though July's own PulledAt.Day()==31 satisfies the day-3 grace by coincidence — the just-closed month must still be re-pulled")
+ }
+
+ raw, _, err := fx.rel.Get(ctx, economics.BillKey("2026-07"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got economics.Bill
+ if err := json.Unmarshal(raw, &got); err != nil {
+ t.Fatal(err)
+ }
+ if got.TotalUSD != 99 {
+ t.Fatalf("July bill total = %v, want 99 (re-pulled with the fresh ceCosts value)", got.TotalUSD)
+ }
+}
+
// TestBillPassRetriesAfterTransientCEFailure: a non-auth-shaped ceCosts
// failure (a throttle, a network blip) must not disable further attempts —
// realCECosts' auth-shaped latch (cepull.go, isAuthShapedError) is the only
diff --git a/internal/console/economicspass.go b/internal/console/economicspass.go
index c3b598306..ed8e45cb2 100644
--- a/internal/console/economicspass.go
+++ b/internal/console/economicspass.go
@@ -686,12 +686,17 @@ func shouldSkipCEPull(existing *economics.Bill, closedMonth bool, now time.Time)
if existing == nil || existing.Source != economics.SourceCostExplorer {
return false
}
- if closedMonth && existing.PulledAt.Day() >= billCEDayGrace {
- // Settled: a closed month pulled on or after day 3 of the
- // following month has already seen CE's late-arriving line items
- // land. Its numbers don't move again — re-pulling it forever
- // would be pure waste, unlike the open month, which keeps
- // changing all month.
+ if closedMonth && usage.MonthOf(existing.PulledAt) > existing.Month && existing.PulledAt.Day() >= billCEDayGrace {
+ // Settled: a closed month pulled on or after day 3 of a LATER
+ // month has already seen CE's late-arriving line items land. Its
+ // numbers don't move again — re-pulling it forever would be pure
+ // waste, unlike the open month, which keeps changing all month.
+ //
+ // The month check matters on its own: a pull made from WITHIN the
+ // closing month (e.g. the month's own last daily pull, July 31)
+ // can land on day >= billCEDayGrace purely by calendar coincidence
+ // (Day()==31) without the month having closed at all yet — that
+ // is not settlement, and must not be treated as such.
return true
}
return usage.DayOf(existing.PulledAt) == usage.DayOf(now)