Skip to content

[WIP] Enable budget planning by pay periods#7226

Draft
code-with-jov wants to merge 38 commits intoactualbudget:masterfrom
code-with-jov:opsx_pp_13_99
Draft

[WIP] Enable budget planning by pay periods#7226
code-with-jov wants to merge 38 commits intoactualbudget:masterfrom
code-with-jov:opsx_pp_13_99

Conversation

@code-with-jov
Copy link

@code-with-jov code-with-jov commented Mar 17, 2026

Why

Users are paid on schedules that don't align with calendar months — weekly, biweekly, or on specific dates — making month-based budgeting disconnected from their actual financial reality. Pay Periods brings the budget unit in line with the paycheck, so users can plan, track, and carry over funds within the period they actually have money to spend.

Related Links:

#5730
#5849
#975
#6412

Testing

packages/desktop-client/e2e/pay-periods.test.ts
packages/desktop-client/e2e/pay-periods.mobile.test.ts

Checklist

  • Release notes added (see link above)
  • No obvious regressions in affected areas
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 26 11.99 MB → 12.01 MB (+24.68 kB) +0.20%
loot-core 1 4.83 MB → 4.84 MB (+8.51 kB) +0.17%
api 4 4.06 MB → 4.07 MB (+9.25 kB) +0.22%
cli 1 7.88 MB 0%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
26 11.99 MB → 12.01 MB (+24.68 kB) +0.20%
Changeset
File Δ Size
src/components/settings/PayPeriodSettings.tsx 🆕 +7.38 kB 0 B → 7.38 kB
home/runner/work/actual/actual/packages/loot-core/src/shared/pay-periods.ts 🆕 +5.79 kB 0 B → 5.79 kB
src/components/budget/PayPeriodContext.tsx 🆕 +569 B 0 B → 569 B
src/components/mobile/budget/CategoryPage.tsx 📈 +1.11 kB (+49.69%) 2.24 kB → 3.35 kB
home/runner/work/actual/actual/packages/loot-core/src/shared/months.ts 📈 +2.2 kB (+22.42%) 9.8 kB → 12 kB
src/components/budget/index.tsx 📈 +1.67 kB (+14.62%) 11.44 kB → 13.11 kB
src/components/mobile/budget/CategoryTransactions.tsx 📈 +458 B (+9.45%) 4.73 kB → 5.18 kB
src/components/mobile/budget/BudgetPage.tsx 📈 +3.07 kB (+8.76%) 35.02 kB → 38.08 kB
src/components/budget/MonthsContext.tsx 📈 +92 B (+7.82%) 1.15 kB → 1.24 kB
src/components/budget/MonthPicker.tsx 📈 +550 B (+6.55%) 8.2 kB → 8.74 kB
src/components/budget/tracking/TrackingBudgetContext.tsx 📈 +81 B (+5.80%) 1.36 kB → 1.44 kB
src/components/budget/envelope/EnvelopeBudgetContext.tsx 📈 +81 B (+5.80%) 1.36 kB → 1.44 kB
src/hooks/useFeatureFlag.ts 📈 +27 B (+5.44%) 496 B → 523 B
src/components/budget/DynamicBudgetTable.tsx 📈 +354 B (+4.91%) 7.04 kB → 7.39 kB
src/components/budget/BudgetSummaries.tsx 📈 +194 B (+4.33%) 4.37 kB → 4.56 kB
src/components/settings/index.tsx 📈 +376 B (+3.67%) 10.02 kB → 10.38 kB
src/components/budget/envelope/budgetsummary/BudgetSummary.tsx 📈 +335 B (+3.03%) 10.8 kB → 11.13 kB
src/components/budget/tracking/budgetsummary/BudgetSummary.tsx 📈 +239 B (+2.31%) 10.12 kB → 10.36 kB
src/components/settings/Experimental.tsx 📈 +177 B (+1.75%) 9.87 kB → 10.05 kB
src/components/budget/util.ts 📈 +33 B (+1.00%) 3.24 kB → 3.27 kB
node_modules/date-fns/differenceInDays.js 📈 +2 B (+0.19%) 1.03 kB → 1.04 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
static/js/index.js 3.23 MB → 3.24 MB (+10.98 kB) +0.33%
static/js/useTransactionBatchActions.js 4.29 MB → 4.3 MB (+10.11 kB) +0.23%
static/js/narrow.js 354.12 kB → 358.75 kB (+4.63 kB) +1.31%

Smaller

Asset File Size % Changed
static/js/ReportRouter.js 1021.25 kB → 1020.22 kB (-1.03 kB) -0.10%

Unchanged

Asset File Size % Changed
static/js/BackgroundImage.js 119.98 kB 0%
static/js/FormulaEditor.js 846.44 kB 0%
static/js/TransactionList.js 81.29 kB 0%
static/js/ca.js 185.57 kB 0%
static/js/da.js 104.66 kB 0%
static/js/de.js 177.58 kB 0%
static/js/en-GB.js 7.16 kB 0%
static/js/en.js 170.68 kB 0%
static/js/es.js 172.13 kB 0%
static/js/fr.js 177.57 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.46 kB 0%
static/js/it.js 168.97 kB 0%
static/js/nb-NO.js 154.72 kB 0%
static/js/nl.js 111.58 kB 0%
static/js/pl.js 88.34 kB 0%
static/js/pt-BR.js 180.5 kB 0%
static/js/resize-observer.js 18.03 kB 0%
static/js/th.js 179.94 kB 0%
static/js/theme.js 30.68 kB 0%
static/js/uk.js 213.14 kB 0%
static/js/wide.js 418 B 0%
static/js/workbox-window.prod.es5.js 7.28 kB 0%

loot-core

Total

Files count Total bundle size % Changed
1 4.83 MB → 4.84 MB (+8.51 kB) +0.17%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/shared/pay-periods.ts 🆕 +4.81 kB 0 B → 4.81 kB
home/runner/work/actual/actual/packages/loot-core/src/shared/months.ts 📈 +2.06 kB (+43.80%) 4.71 kB → 6.78 kB
home/runner/work/actual/actual/packages/loot-core/src/server/preferences/app.ts 📈 +701 B (+14.26%) 4.8 kB → 5.49 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/base.ts 📈 +688 B (+9.55%) 7.04 kB → 7.71 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/envelope.ts 📈 +104 B (+1.11%) 9.11 kB → 9.21 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📈 +122 B (+1.11%) 10.77 kB → 10.89 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/app.ts 📈 +61 B (+0.70%) 8.5 kB → 8.56 kB
home/runner/work/actual/actual/packages/loot-core/src/server/encryption/app.ts 📉 -3 B (-0.16%) 1.85 kB → 1.85 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.CXBsxhFz.js 0 B → 4.84 MB (+4.84 MB) -

Removed

Asset File Size % Changed
kcab.worker.Dmj0rSrb.js 4.83 MB → 0 B (-4.83 MB) -100%

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged
No assets were unchanged


api

Total

Files count Total bundle size % Changed
4 4.06 MB → 4.07 MB (+9.25 kB) +0.22%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/shared/pay-periods.ts 🆕 +5.61 kB 0 B → 5.61 kB
home/runner/work/actual/actual/packages/loot-core/src/shared/months.ts 📈 +2.05 kB (+44.35%) 4.62 kB → 6.67 kB
home/runner/work/actual/actual/packages/loot-core/src/server/preferences/app.ts 📈 +682 B (+14.14%) 4.71 kB → 5.38 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/base.ts 📈 +674 B (+9.47%) 6.95 kB → 7.61 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/envelope.ts 📈 +104 B (+1.14%) 8.9 kB → 9.01 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budgetfiles/app.ts 📈 +120 B (+1.12%) 10.45 kB → 10.57 kB
home/runner/work/actual/actual/packages/loot-core/src/server/budget/app.ts 📈 +60 B (+0.70%) 8.35 kB → 8.4 kB
home/runner/work/actual/actual/packages/loot-core/src/server/encryption/app.ts 📉 -3 B (-0.16%) 1.78 kB → 1.78 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
index.js 3.84 MB → 3.85 MB (+9.25 kB) +0.24%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
from-Bl-Hslp4.js 167.73 kB 0%
multipart-parser-BnDysoMr.js 8.1 kB 0%
src-iMkUmuwR.js 43.64 kB 0%

cli

Total

Files count Total bundle size % Changed
1 7.88 MB 0%
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
cli.js 7.88 MB 0%

code-with-jov and others added 30 commits March 1, 2026 03:46
…n, and tests

- Phase 1: Add `payPeriodsEnabled` FeatureFlag, `PayPeriodConfig` type, and
  `showPayPeriods`/`payPeriodFrequency`/`payPeriodStartDate` to SyncedPrefs
- Phase 2: Create `pay-periods.ts` with `isPayPeriod`, `generatePayPeriods`
  (memoized, ≤87-period assertion), `getPayPeriodFromDate` (year-boundary aware),
  `nextPayPeriod`, `prevPayPeriod`, `addPayPeriods`, `generatePayPeriodRange`,
  and `getPayPeriodLabel`
- Phase 3: Extend `months.ts` `_parse`, `bounds`, `prevMonth`, `nextMonth`,
  `addMonths`, `monthFromDate`, `currentMonth`, `rangeInclusive`, `nameForMonth`,
  `isBefore`, and `isAfter` with optional `PayPeriodConfig` parameter;
  pay period IDs dispatch to period-aware logic; calendar callers unchanged
- Phase 4: Add `pay-periods.test.ts` covering all engine functions and
  months.ts integration; all 546 existing tests continue to pass

https://claude.ai/code/session_015cHkMuws4XaokUtBqa46bv
…gine integration

Phase 5 — Server Preferences and Config Loading:
- Add `loadPayPeriodConfig()` to `preferences/app.ts` — reads
  showPayPeriods/payPeriodFrequency/payPeriodStartDate from DB
- Store config in `sheet.get().meta().payPeriodConfig` at budget-open time
  and reload it whenever a pay period pref is saved via `saveSyncedPrefs`
- Pass config to `createAllBudgets(config)` in the budget-open flow

Phase 6 — Server Budget Engine Integration:
- `getBudgetRange`: period-aware when enabled — uses `addPayPeriods` for
  3-period/12-period buffers and `generatePayPeriodRange` for the range
- `createBudget`: passes config to `bounds` and `prevMonth`; also passes
  config into `envelopeBudget.createBudget` so the blank-sheet calculation
  uses `prevPayPeriod` instead of calendar `prevMonth`
- `createAllBudgets`: passes config through to `getBudgetRange`/`createBudget`
- `triggerBudgetChanges`: reads `payPeriodConfig` from sheet meta and passes
  it to `handleTransactionChange` (period-aware routing) and
  `envelopeBudget.handleCategoryChange` (config-aware category setup)
- `envelope.ts`: thread config through `getBlankSheet`, `createBlankCategory`,
  `createBlankMonth`, `createBudget`, and `handleCategoryChange`
- Integration tests: `createBudget` with period IDs creates correct sheet names
  and SQL date bounds; period 1 does not capture period 2 transactions

All 554 tests pass.

https://claude.ai/code/session_015cHkMuws4XaokUtBqa46bv
Phase 7 — Context and Hook:
- Create PayPeriodContext/PayPeriodProvider and usePayPeriodConfig() hook
  in components/budget/PayPeriodContext.tsx
- Thread PayPeriodConfig into budget/index.tsx by reading showPayPeriods,
  payPeriodFrequency, payPeriodStartDate prefs and payPeriodsEnabled flag;
  wrap budget tree in <PayPeriodProvider>
- Add payPeriodsEnabled: false default in useFeatureFlag.ts

Phase 8 — Budget Page Navigation:
- budget/index.tsx: use monthUtils.currentMonth(config) for startMonth
  default; pass config to prewarmAllMonths; use addMonths(month, ±n, config)
  for prewarm direction heuristic
- MonthsContext.tsx: use usePayPeriodConfig() in MonthsProvider so
  addMonths and rangeInclusive are period-aware
- util.ts: prewarmAllMonths accepts optional PayPeriodConfig; replaces
  subMonths with addMonths(-1, config)
- MonthPicker.tsx: all navigation (prev/next/today) and range calculations
  use config-aware month utils

Phase 9 — Budget Column Display:
- MonthPicker shows 'PP N' short labels via getPayPeriodLabel when a period
  ID is in the range; falls back to 'MMM' for calendar months
- Envelope and tracking BudgetSummary: displayMonth uses getPayPeriodLabel
  (long format) for period IDs; prevMonthName uses short format
- EnvelopeBudgetContext and TrackingBudgetContext: currentMonth uses
  monthUtils.currentMonth(config) for period-aware highlighting

Phase 10 — Settings UI:
- Create PayPeriodSettings component with enable toggle, frequency selector
  (weekly/biweekly/monthly), start date input, validation (start date and
  frequency required before enabling), and frequency-change warning
- Add to settings/index.tsx gated on payPeriodsEnabled feature flag

https://claude.ai/code/session_015cHkMuws4XaokUtBqa46bv
- Add e2e/pay-periods.test.ts with 11 tests covering all user-facing
  pay period scenarios from pay-period-ui spec (settings visibility,
  frequency selector, validation, enable/disable, navigation, labels)
- Add data-testid="budget-month-header" to both BudgetSummary components
  and update headers to show PP N labels for pay period IDs
- Add aria-labels to MonthPicker nav buttons for Playwright targeting:
  "Go to current period", "Previous period", "Next period"
- Wrap PayPeriodSettings in <View data-testid="pay-period-settings">
  for reliable test targeting; use Select component consistent with app
- Add payPeriodsEnabled FeatureToggle in Experimental.tsx so the flag
  can be enabled via settings UI in tests

https://claude.ai/code/session_015cHkMuws4XaokUtBqa46bv
When pay periods are enabled, createAllBudgets passes a calendar date
(from the earliest transaction) as the start argument while currentMonth
returns a pay period ID. The old condition required BOTH start AND end
to be pay period IDs, so a calendar start would fall through to the
calendar path which called monthUtils.addMonths on a pay period ID,
triggering the '_parse requires PayPeriodConfig' error.

Fix by normalising any calendar date/month to a pay period ID at the
top of the pay-period branch, before the buffer arithmetic.

https://claude.ai/code/session_015cHkMuws4XaokUtBqa46bv
Three classes of bugs discovered during smoke testing and fixed:

1. subMonths missing config param (months.ts) — unlike addMonths/prevMonth/
   nextMonth, subMonths had no config? parameter, causing _parse to throw
   whenever a pay period ID was passed. Added config? and dispatches to
   addPayPeriods(month, -n, config) when isPayPeriod(month).

2. BudgetSummaries and DynamicBudgetTable not consuming PayPeriodConfig —
   both components called subMonths/addMonths/prevMonth/nextMonth with
   period IDs but without config. Added usePayPeriodConfig() to each and
   propagated config to all call sites (including the 0/←/→ hotkeys).

3. onShowActivity drill-through crash (Value.tsx RangeError) — clicking
   Spent passed a pay period ID as a month-equality filter, which
   subfieldFromFilter misidentified as a calendar month (both are length-7
   strings), causing parseISO to produce Invalid Date. Now branches on
   isPayPeriod: pay period path emits gte/lte date-range conditions using
   bounds(month, config); calendar path is unchanged.

OpenSpec updates: added subMonths to engine spec affected-functions list
with scenarios; added two new requirements to UI spec (internal components
must consume config, drill-through must use date-range filter); added tasks
3.5a, 9.4, 9.5, 9.6 to tasks.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add OpenSpec proposal for pay-period-label-improvements
…ase-4-o7tge

Implement pay-periods phases 1–4: types, engine, months.ts integratio…
…ate-range

- Change `getPayPeriodLabel` third parameter from `short: boolean` to
  `format: 'picker' | 'summary'` with optional `locale?: Locale`
- Picker format produces `{monthLetter}{withinMonthCount}` (e.g. J1, F2)
  using the locale-aware first char of the start month's MMM abbreviation
- Summary format produces `{startDate} - {endDate} (PP{n})` (e.g. Jan 4 - Jan 17 (PP1))
- Update `nameForMonth` in months.ts to forward locale and use new format strings
- Update both BudgetSummary components to use `format='summary'` with locale
- Update unit tests with new expected label formats and add additional coverage
- Update E2E tests to match new summary label regex

https://claude.ai/code/session_018hZh1bX4qEexFQAWDPb1xy
…facts

Includes pay-period engine, UI components, budget integration, settings,
openspec specs/design files, and opsx command updates that were already
present but unstaged.

https://claude.ai/code/session_018hZh1bX4qEexFQAWDPb1xy
Proposal, design, and tasks for bringing the mobile budget page
(BudgetPage, CategoryPage, CategoryTransactions) in line with the
desktop pay-period-aware implementation.

https://claude.ai/code/session_01C6Li6m7S81FfFebW26gi35
- BudgetPage: read pay period prefs, build PayPeriodConfig, wrap in
  PayPeriodProvider; fix currentMonth, prev/next navigation, today button
  visibility, notes modal label to all pass/use config
- MonthSelector: accept payPeriodConfig prop; use getPayPeriodLabel for
  period display; fix prev/next bounds via period start-date integer
  comparison against calendar bounds
- CategoryPage: build PayPeriodConfig, wrap in PayPeriodProvider, fix
  header label for pay period IDs
- CategoryTransactions: use usePayPeriodConfig() from context; switch
  getCategoryMonthFilter to date-range filter for pay period IDs

https://claude.ai/code/session_01C6Li6m7S81FfFebW26gi35
Catch up to latest changes
Catch up to latest changes
…el-improvements-ZsC5e

Claude/apply pay period label improvements zs c5e
@netlify
Copy link

netlify bot commented Mar 17, 2026

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 150c82b
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget/deploys/69be13b15ecbc20008869fbc
😎 Deploy Preview https://deploy-preview-7226.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Contributor

👋 Hello contributor!

We would love to review your PR! Before we can do that, please make sure:

  • ✅ All CI checks pass
  • ✅ The PR is moved from draft to open (if applicable)
  • ✅ The "[WIP]" prefix is removed from the PR title
  • ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.

We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.

For more information, please see our Contributing Guide.

@netlify
Copy link

netlify bot commented Mar 17, 2026

Deploy Preview for actualbudget-storybook ready!

Name Link
🔨 Latest commit 150c82b
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-storybook/deploys/69be13b1f04d1c000819f643
😎 Deploy Preview https://deploy-preview-7226--actualbudget-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Mar 17, 2026

Deploy Preview for actualbudget-website ready!

Name Link
🔨 Latest commit 150c82b
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-website/deploys/69be13b1e883b3000810c7a4
😎 Deploy Preview https://deploy-preview-7226.www.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@code-with-jov code-with-jov changed the title Enable budget planning by pay periods [WIP] Enable budget planning by pay periods Mar 17, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link
Contributor

VRT tests ❌ failed. View the test report.

To update the VRT screenshots, comment /update-vrt on this PR. The VRT update operation takes about 50 minutes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants