From 0d64b8a03138b91bf5ec9253471734531f994d6b Mon Sep 17 00:00:00 2001 From: Yip Shing him Date: Tue, 23 Jun 2026 23:37:33 +0100 Subject: [PATCH] docs: add budget categories cspf plan --- ...06-23-budget-categories-and-cspf-budget.md | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-23-budget-categories-and-cspf-budget.md diff --git a/docs/superpowers/plans/2026-06-23-budget-categories-and-cspf-budget.md b/docs/superpowers/plans/2026-06-23-budget-categories-and-cspf-budget.md new file mode 100644 index 000000000..d135ba615 --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-budget-categories-and-cspf-budget.md @@ -0,0 +1,516 @@ +# 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. + +## Recommended Scope + +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: + +```ts +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: + +```ts +expenseCategory.create; +expenseCategory.delete; +expenseCategory.findFirst; +expenseCategory.findMany; +expenseCategory.update; +``` + +- [ ] **Step 2: Add service methods** + +Implement these methods in `BudgetsService`: + +```ts +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: + +```ts +POST /api/v1/budgets/categories +PUT /api/v1/budgets/categories/:id +DELETE /api/v1/budgets/categories/:id +``` + +Permissions: + +```ts +createExpenseCategory; +updateExpenseCategory; +deleteExpenseCategory; +``` + +- [ ] **Step 4: Add client service methods** + +Add to `DataService`: + +```ts +createExpenseCategory(category: CreateExpenseCategoryDto) +updateExpenseCategory({ category, id }: { category: UpdateExpenseCategoryDto; id: string }) +deleteExpenseCategory(id: string) +``` + +- [ ] **Step 5: Verify** + +Run: + +```bash +./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** + +```bash +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`: + +```html + +``` + +- [ ] **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: + +```bash +./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** + +```bash +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: + +```prisma +enum BudgetType { + EXPENSE + CASH_SAVINGS + INVESTMENT_SAVINGS + YEARLY_EXPENSE_AUTOMATIC + LIABILITY_AUTOMATIC +} +``` + +Extend `Budget`: + +```prisma +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: + +```prisma +@@index([accountId]) +@@index([type]) +``` + +Keep the existing indexes on `categoryId`, `month`, and `userId`. + +- [ ] **Step 2: Update contracts** + +Add to create/update DTOs: + +```ts +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: + +```ts +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: + +```ts +[ + '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: + +```bash +./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** + +```bash +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** + +```prisma +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: + +```ts +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: + +```ts +readBudgets; +createBudget; +updateBudget; +deleteBudget; +``` + +- [ ] **Step 3: Include automatic rows in `getBudgets()`** + +For each yearly expense, emit an automatic budget row: + +```ts +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: + +```ts +['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: + +```bash +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: + +```ts +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: + +```ts +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: + +```ts +['account', 'amount']; +``` + +- [ ] **Step 5: Verify and commit** + +Run: + +```bash +./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** + +```bash +./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** + +```bash +./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.