mirror of https://github.com/ghostfolio/ghostfolio
1 changed files with 516 additions and 0 deletions
@ -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 |
|||
<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: |
|||
|
|||
```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. |
|||
Loading…
Reference in new issue