Skip to content

feat(forecast): learn from budget months and complete months only - #464

Closed
saesch83 wants to merge 4 commits into
otherworld-dev:devfrom
saesch83:feature/forecast-budget-months
Closed

saesch83 wants to merge 4 commits into
otherworld-dev:devfrom
saesch83:feature/forecast-budget-months

Conversation

@saesch83

Copy link
Copy Markdown
Contributor

Part of #457. Builds on #458: it uses that PR's Months in dashboard charts and reports setting and BudgetPeriod::monthsBetween(). Until #458 is merged this branch includes its commits; only the last commit, feat(forecast): …, is new here.

Problem

The live forecast (Forecast page and dashboard tile, ForecastService::getLiveForecast()) has two issues that skew its averages.

  1. It ignores the budget start day. It learns from the twelve complete calendar months before the current one, groups by calendar month, and projects calendar months, whatever the user's start day.
  2. It counts a partly covered month as a whole one. Bank sync fetches 90 days, so the first month in the history holds only its last days.
    • Real case: history from 26 September, today 30 October. The forecast learned from one month that was 5 days of September. It included the salary and almost no spending, so the average savings came out several times too high.

Changes

  • Budget months: when the user chose budget months in Settings (feat(reports): let dashboard charts and reports count budget months #458), the forecast:

    • learns from the complete budget months before the running one,
    • groups them with BudgetPeriod::monthContaining() (PatternAnalyzer::aggregateMonthlyData() takes the start day),
    • projects budget months.

    Calendar mode is unchanged. The cache key includes the start day, and the response carries monthStartDay. The Forecast page shows the same budget months note as the charts.

  • Complete months only, in both modes: a month counts only when every account that held money before its first transaction has history from that month's start.

    • "Held money before" means a non-zero opening balance. Bank-synced accounts get one when their opening balance is matched to the bank's.
    • Up to 3 days without bookings at the start of the month are tolerated. A first transaction on the 3rd usually means a weekend, not missing history.
    • Accounts opened from nothing don't limit the history. Neither does an account whose history starts after the forecast window, so adding a new account doesn't leave the forecast with no months at all.
    • The new TransactionMapper::getFirstTransactionDates() gives each account's first transaction date in one grouped query.
    • With no complete month left, the forecast has no history. The page then shows its existing Not Enough Data state, plus one line explaining that partly covered months are left out.

Testing

  • Unit tests:
    • Calendar and budget history ranges and projected months.
    • The partial first month, in budget and calendar mode.
    • A history starting on the 1st, and one starting a weekend in.
    • The latest history across several accounts.
    • Accounts without an opening balance.
    • An account added after the window.
    • Budget-month aggregation in PatternAnalyzer.
    • The UI note.
  • Full suites:
    • PHPUnit: 7,514 tests pass (1 skipped).
    • vitest: the forecast and month-grouping tests pass.
    • cs:check, Psalm and npm run lint are clean.
  • Manual (Nextcloud 31), real data with history from 26 September, start day 26:
    • Calendar mode now reports 0 complete months instead of a 5-day September.
    • Budget mode learns from one complete budget month (26 Sep – 25 Oct) and projects budget months.

🤖 Generated with Claude Code

saesch83 and others added 4 commits October 28, 2026 13:49
…therworld-dev#457)

A new setting next to the Budget Cycle Start Day chooses whether the
dashboard charts and the month-by-month reports (income vs. expenses
trend, cash flow, category by month, and their exports) count calendar
months, the default, or the user's budget months. With budget months a
salary on the 26th counts towards the next month, as on the Budget page.

The queries group by date and fold the rows into budget months in PHP,
since a budget month can't be cut out of a date in SQL all supported
databases run. Year-over-year and the digest keep calendar months.

The hero tiles' "vs last month" now compares the current budget period
with the whole previous one. It used the summary's calendar-month
buckets, which with a start day cut the current period in two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The income vs. expenses trend stepped a month at a time from the start
date, so a range starting later in the month than it ends lost its last
month (26 Jul - 25 Oct showed no October), and one starting on 31 January
skipped February.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… months

With budget months chosen for the reports, "This Month" still meant the
calendar month, so with start day 26 the report showed two budget months:
1-25 October as October and 26-31 October as November, with the money
split between them. The presets now cover whole budget months: This
Month is the running budget month, Last 3 Months three of them, and Year
to Date and Last Year start with the budget month of January.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…herworld-dev#457)

The forecast learned from calendar months whatever the budget start day,
and counted a month its history only partly held as a whole one. Bank
sync fetches 90 days, so the first month holds only its last days: five
days of September, salary included and hardly any spending, made the
average savings several times too high.

With budget months chosen for the charts and reports, the forecast now
learns from the complete budget months before the running one and
projects budget months. In either mode a month counts only when every
account that held money before its first transaction (an opening
balance) has history from the month's start, give or take a weekend of
no bookings. An account added after the history window bounds nothing,
so adding one does not empty the forecast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant