You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

16 KiB

Budget Categories and CSPF-Inspired Budget Page Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add first-class expense category management and evolve the Budget page toward the CS Personal Finance Budget tab model: monthly budget lines, yearly expenses converted to monthly amounts, bank-account transfer summaries, and planned-vs-actual statistics.

Architecture: Keep expense categories user-owned and reusable across expenses and budgets. Treat each monthly budget row as a budget line with item name, amount, account, category, and type instead of only one amount per category. Yearly expenses are separate user-owned records that the API converts into automatic monthly budget rows and summary totals.

Tech Stack: Angular 21 standalone components, Angular Material table/dialog/form controls, NestJS, Prisma, PostgreSQL, Nx, Jest, Ghostfolio DataService, Ghostfolio permissions.

Reference: CS Personal Finance Budget guide: https://guide.cspersonalfinance.io/summary-tabs/budget


What We Can Bring From CS Personal Finance

The source Budget tab has five useful concepts:

  • Monthly budget table: item name, monthly amount, bank account, and category.
  • Yearly expenses table: annual or irregular expenses converted into equivalent monthly cost.
  • Automatic bank transfers: grouped view showing how much money needs to be sent to each bank account each month.
  • Graphs and statistics: planned spend, actual spend, monthly savings, yearly savings, and estimated savings rate.
  • Settings overview: read-only summary of assumptions used for calculations.

Ghostfolio can support these, but with app-native data models instead of spreadsheet cells:

  • Expense categories become managed entities with create, edit, delete, color, and duplicate-name validation.
  • Monthly budget rows become editable records in the Budget page.
  • Yearly expenses become separate records and are displayed as automatic budget rows.
  • Actual spend comes from the Expense table, using monthly totals and 6-month average actual spend.
  • Account transfer summaries group monthly budget rows by accountId.

Current State

  • ExpenseCategory, Expense, and Budget Prisma models exist on this branch.
  • Budget UI can create and edit monthly category budgets.
  • Budget dialog now fetches existing categories, but there is no UI to create or manage categories.
  • Budget rows are currently category-level only, so they cannot fully represent CS Personal Finance item-level budget rows yet.

Build this in three slices:

  1. Category management inside the Budget page.
  2. Budget line upgrade: item name, account, category, amount, and budget type.
  3. CSPF-inspired yearly expense and statistics sections.

This keeps each slice testable and usable without waiting for the full spreadsheet-style experience.


File Structure

  • Modify prisma/schema.prisma: extend Budget, add BudgetType enum, add YearlyExpense.
  • Modify libs/common/src/lib/dtos/: add yearly expense DTOs and extend budget DTOs with name, accountId, and type.
  • Modify libs/common/src/lib/interfaces/responses/: extend budget responses and add yearly expense/budget summary interfaces.
  • Modify apps/api/src/app/budgets/: add category CRUD, budget line support, yearly expense support, and summary calculations.
  • Modify libs/ui/src/lib/services/data.service.ts: add category CRUD and yearly expense methods.
  • Modify apps/client/src/app/pages/portfolio/budget/: add category management dialog, yearly expense dialog, transfer summary, and statistics panels.

Task 1: Add Category Management API

Files:

  • Modify: apps/api/src/app/budgets/budgets.controller.ts

  • Modify: apps/api/src/app/budgets/budgets.service.ts

  • Modify: apps/api/src/app/budgets/budgets.service.spec.ts

  • Modify: libs/ui/src/lib/services/data.service.ts

  • Modify: libs/ui/src/lib/services/data.service.spec.ts

  • Step 1: Add failing service tests

Add tests for:

it('creates an expense category for the current user');
it('rejects duplicate category names for the current user');
it('updates only categories owned by the current user');
it('deletes only categories owned by the current user');

Expected Prisma mocks:

expenseCategory.create;
expenseCategory.delete;
expenseCategory.findFirst;
expenseCategory.findMany;
expenseCategory.update;
  • Step 2: Add service methods

Implement these methods in BudgetsService:

createCategory({ data, userId }: { data: CreateExpenseCategoryDto; userId: string })
updateCategory({ data, id, userId }: { data: UpdateExpenseCategoryDto; id: string; userId: string })
deleteCategory({ id, userId }: { id: string; userId: string })

Use ConflictException when a category name already exists for the same user. Use ForbiddenException when the category does not belong to the user.

  • Step 3: Add controller endpoints

Add:

POST /api/v1/budgets/categories
PUT /api/v1/budgets/categories/:id
DELETE /api/v1/budgets/categories/:id

Permissions:

createExpenseCategory;
updateExpenseCategory;
deleteExpenseCategory;
  • Step 4: Add client service methods

Add to DataService:

createExpenseCategory(category: CreateExpenseCategoryDto)
updateExpenseCategory({ category, id }: { category: UpdateExpenseCategoryDto; id: string })
deleteExpenseCategory(id: string)
  • Step 5: Verify

Run:

./node_modules/.bin/nx test api --testFile=budgets.service.spec.ts --runInBand
./node_modules/.bin/nx test ui --testFile=data.service.spec.ts --runInBand
  • Step 6: Commit
git add apps/api/src/app/budgets libs/ui/src/lib/services/data.service.ts libs/ui/src/lib/services/data.service.spec.ts
git commit -m "feat: add budget category management api"

Task 2: Add Category Management UI

Files:

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.component.ts

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.html

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts

  • Create: apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.ts

  • Create: apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.html

  • Create: apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.scss

  • Create: apps/client/src/app/pages/portfolio/budget/manage-budget-categories-dialog/manage-budget-categories-dialog.component.spec.ts

  • Step 1: Add a Manage Categories button

Place a secondary button near Create budget:

<button mat-stroked-button type="button" (click)="onManageCategories()">
  Manage categories
</button>
  • Step 2: Build the dialog

The dialog should show:

  • Category name input.
  • Optional color input.
  • Create button.
  • Table/list of existing categories.
  • Edit and delete icon buttons.

Use MatDialogModule, MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule, and MatTableModule.

  • Step 3: Refresh budget dialog category list

After categories are created or edited, closing the category dialog with { refresh: true } should allow the next Budget dialog open to fetch the latest categories.

  • Step 4: Verify

Run:

./node_modules/.bin/nx test client --testFile=manage-budget-categories-dialog.component.spec.ts --runInBand
./node_modules/.bin/nx test client --testFile=budget-page.component.spec.ts --runInBand
  • Step 5: Commit
git add apps/client/src/app/pages/portfolio/budget
git commit -m "feat: add budget category management ui"

Task 3: Upgrade Budget Rows Into Monthly Budget Lines

Files:

  • Modify: prisma/schema.prisma

  • Modify: libs/common/src/lib/dtos/create-budget.dto.ts

  • Modify: libs/common/src/lib/dtos/update-budget.dto.ts

  • Modify: libs/common/src/lib/interfaces/responses/budget-response.interface.ts

  • Modify: apps/api/src/app/budgets/budgets.service.ts

  • Modify: apps/api/src/app/budgets/budgets.service.spec.ts

  • Modify: apps/client/src/app/pages/portfolio/budget/create-or-update-budget-dialog/*

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.*

  • Step 1: Extend the Prisma model

Add:

enum BudgetType {
  EXPENSE
  CASH_SAVINGS
  INVESTMENT_SAVINGS
  YEARLY_EXPENSE_AUTOMATIC
  LIABILITY_AUTOMATIC
}

Extend Budget:

account       Account?   @relation(fields: [accountId, accountUserId], references: [id, userId])
accountId     String?
accountUserId String?
name          String
type          BudgetType @default(EXPENSE)

Replace the current unique category/month constraint with:

@@index([accountId])
@@index([type])

Keep the existing indexes on categoryId, month, and userId.

  • Step 2: Update contracts

Add to create/update DTOs:

accountId?: string;
name: string;
type: 'EXPENSE' | 'CASH_SAVINGS' | 'INVESTMENT_SAVINGS';

Do not allow clients to submit automatic types directly.

  • Step 3: Update API calculations

getBudgets() should return manual monthly budget lines plus automatic yearly expense rows from Task 4. Totals should calculate:

totalBudgeted = sum(all row amounts)
totalPlannedSpend = sum(EXPENSE + YEARLY_EXPENSE_AUTOMATIC + LIABILITY_AUTOMATIC)
totalMonthlySavings = sum(CASH_SAVINGS + INVESTMENT_SAVINGS)
totalRemaining = totalBudgeted - totalSpent
  • Step 4: Update UI table columns

Columns should become:

[
  'name',
  'category',
  'account',
  'type',
  'amount',
  'spent',
  'remaining',
  'progress',
  'actions'
];

The dialog should ask for item name, account, category, type, month, and amount.

  • Step 5: Verify

Run:

./node_modules/.bin/nx test api --testFile=budgets.service.spec.ts --runInBand
./node_modules/.bin/nx test client --testFile=create-or-update-budget-dialog.component.spec.ts --runInBand
./node_modules/.bin/nx test client --testFile=budget-page.component.spec.ts --runInBand
./node_modules/.bin/nx run api:build
./node_modules/.bin/nx run client:build:development-en
  • Step 6: Commit
git add prisma/schema.prisma libs/common apps/api/src/app/budgets apps/client/src/app/pages/portfolio/budget
git commit -m "feat: support monthly budget lines"

Task 4: Add Yearly Expenses

Files:

  • Modify: prisma/schema.prisma

  • Create: libs/common/src/lib/dtos/create-yearly-expense.dto.ts

  • Create: libs/common/src/lib/dtos/update-yearly-expense.dto.ts

  • Modify: libs/common/src/lib/dtos/index.ts

  • Create: libs/common/src/lib/interfaces/responses/yearly-expense-response.interface.ts

  • Modify: libs/common/src/lib/interfaces/index.ts

  • Modify: apps/api/src/app/budgets/budgets.controller.ts

  • Modify: apps/api/src/app/budgets/budgets.service.ts

  • Modify: apps/api/src/app/budgets/budgets.service.spec.ts

  • Modify: libs/ui/src/lib/services/data.service.ts

  • Modify: apps/client/src/app/pages/portfolio/budget/*

  • Step 1: Add Prisma model

model YearlyExpense {
  account       Account?        @relation(fields: [accountId, accountUserId], references: [id, userId])
  accountId     String?
  accountUserId String?
  category      ExpenseCategory @relation(fields: [categoryId], onDelete: Cascade, references: [id])
  categoryId    String
  createdAt     DateTime        @default(now())
  currency      String
  id            String          @id @default(uuid())
  name          String
  updatedAt     DateTime        @updatedAt
  user          User            @relation(fields: [userId], onDelete: Cascade, references: [id])
  userId        String
  yearlyCost    Float

  @@index([accountId])
  @@index([categoryId])
  @@index([userId])
}
  • Step 2: Add yearly expense endpoints

Add:

GET /api/v1/budgets/yearly-expenses
POST /api/v1/budgets/yearly-expenses
PUT /api/v1/budgets/yearly-expenses/:id
DELETE /api/v1/budgets/yearly-expenses/:id

Use budget permissions for now:

readBudgets;
createBudget;
updateBudget;
deleteBudget;
  • Step 3: Include automatic rows in getBudgets()

For each yearly expense, emit an automatic budget row:

amount = yearlyCost / 12;
type = 'YEARLY_EXPENSE_AUTOMATIC';
name = `${yearlyExpense.name} (yearly)`;

These rows should not be editable from the monthly budget table. They are edited in the Yearly Expenses table.

  • Step 4: Add Yearly Expenses UI section

Below the monthly table, add a compact table:

['name', 'category', 'account', 'yearlyCost', 'monthlyEquivalent', 'actions'];

Add create/edit dialog with fields:

  • Name

  • Category

  • Account

  • Yearly cost

  • Step 5: Verify and commit

Run the same API/client tests and builds from Task 3, then commit:

git commit -m "feat: add yearly budget expenses"

Task 5: Add Transfer Summary and Budget Statistics

Files:

  • Modify: libs/common/src/lib/interfaces/responses/budget-response.interface.ts

  • Modify: apps/api/src/app/budgets/budgets.service.ts

  • Modify: apps/api/src/app/budgets/budgets.service.spec.ts

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.component.ts

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.html

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.scss

  • Modify: apps/client/src/app/pages/portfolio/budget/budget-page.component.spec.ts

  • Step 1: Extend BudgetsResponse

Add:

actualSpendAverage: number;
estimatedSavingsRate: number;
totalMonthlySavings: number;
totalPlannedSpend: number;
totalYearlySavings: number;
transfers: Array<{
  accountId?: string;
  accountName: string;
  amount: number;
}>;
  • Step 2: Calculate 6-month actual spend

In BudgetsService, calculate actual spend as the average monthly sum of Expense.amount over the six months ending with the selected month.

  • Step 3: Calculate transfer rows

Group all monthly and yearly automatic budget rows by account:

transfers = rows.reduce((byAccount, row) => {
  byAccount[row.accountId ?? 'unassigned'] += row.amount;
  return byAccount;
}, {});
  • Step 4: Render stats and transfer summary

Add top summary metrics:

  • Planned spend
  • Actual spend average
  • Monthly savings
  • Yearly savings
  • Estimated savings rate

Add a compact transfer table:

['account', 'amount'];
  • Step 5: Verify and commit

Run:

./node_modules/.bin/nx test api --testFile=budgets.service.spec.ts --runInBand
./node_modules/.bin/nx test client --testFile=budget-page.component.spec.ts --runInBand
./node_modules/.bin/nx run client:build:development-en
git commit -m "feat: add budget transfer summary and statistics"

Task 6: Final Verification

  • Run focused tests
./node_modules/.bin/nx test api --testFile=budgets.service.spec.ts --runInBand
./node_modules/.bin/nx test ui --testFile=data.service.spec.ts --runInBand
./node_modules/.bin/nx test client --testFile=budget-page.component.spec.ts --runInBand
./node_modules/.bin/nx test client --testFile=create-or-update-budget-dialog.component.spec.ts --runInBand
  • Run builds
./node_modules/.bin/nx run api:build
./node_modules/.bin/nx run client:build:development-en
  • Manual browser check

Start the dev server and verify:

  • Budget page loads.
  • Manage categories opens.
  • A category can be created.
  • A monthly budget line can be created using that category.
  • A yearly expense appears as monthly equivalent.
  • Transfer summary groups rows by account.
  • Stats update when changing month.