From 0a4c3e94368e2b79df28f676a6b2a353364e1854 Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Tue, 9 Jun 2026 19:46:54 -0600 Subject: [PATCH] Validate month input and document cycle labeling convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding BE-06 (suspected off-by-one in get_cycle_range) is a FALSE POSITIVE: budget month M composes get_previous_cycle with get_cycle_range to cover [(M-1)/18, M/18), matching both the code comment and the frontend convention. Verified against prod data: May 2026 cycle-window SQL sum matches the API (95 txs exactly, refunds to the colón, 0.13%% delta from one EUR tx). The docstring now spells out the labeling convention, and month is validated 1-12 (BE-07). Co-Authored-By: Claude Fable 5 --- backend/app/services/budget_projection.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/app/services/budget_projection.py b/backend/app/services/budget_projection.py index 695cec4..fcb748f 100644 --- a/backend/app/services/budget_projection.py +++ b/backend/app/services/budget_projection.py @@ -65,8 +65,14 @@ def get_effective_amount(item: RecurringItem, month: int, year: int) -> float | return None +def _validate_month(month: int) -> None: + if not 1 <= month <= 12: + raise ValueError(f"month must be 1-12, got {month}") + + def get_month_range(year: int, month: int) -> tuple[datetime, datetime]: """Return (start, end) for a calendar month.""" + _validate_month(month) start = datetime(year, month, 1) if month == 12: end = datetime(year + 1, 1, 1) @@ -76,7 +82,15 @@ def get_month_range(year: int, month: int) -> tuple[datetime, datetime]: def get_cycle_range(year: int, month: int) -> tuple[datetime, datetime]: - """Return (start, end) for billing cycle: month/18 to month+1/18.""" + """Return (start, end) of the billing cycle that STARTS on the 18th of + `month`: [month/18, month+1/18). + + Note the labeling convention: budget month M covers the cycle that ENDS + on the 18th of M, so callers compose get_previous_cycle(year, M) with + this function — see compute_actuals_by_source. Pinned by + tests/test_cycle_math.py. + """ + _validate_month(month) start = datetime(year, month, 18) if month == 12: end = datetime(year + 1, 1, 18)