docs(05): create phase plan
This commit is contained in:
@@ -78,7 +78,7 @@ Plans:
|
||||
|
||||
---
|
||||
|
||||
### 🚧 v1.1 Usability and Templates (In Progress)
|
||||
### v1.1 Usability and Templates (In Progress)
|
||||
|
||||
**Milestone Goal:** Replace the blunt "copy from previous month" workflow with a smart template system that understands fixed, variable, and one-off expenses. Add a quick-add library for saved one-off categories. Rethink the dashboard layout for denser, spreadsheet-like data presentation.
|
||||
|
||||
@@ -91,11 +91,11 @@ Plans:
|
||||
2. A user's template can be created and fetched via the API — it contains fixed items (with amounts) and variable items (category only, no amount)
|
||||
3. One-off items are excluded from template contents — the API never includes them in template responses
|
||||
4. The template API endpoints are documented and return correct data for all three item tiers
|
||||
**Plans**: TBD
|
||||
**Plans:** 2 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 05-01: DB migration — add item_type column to budget_items, create templates and template_items tables
|
||||
- [ ] 05-02: Go handlers — template CRUD endpoints and budget-from-template generation endpoint
|
||||
- [ ] 05-01-PLAN.md — DB migration (item_tier enum, templates/template_items tables) + Go models + query functions
|
||||
- [ ] 05-02-PLAN.md — HTTP handlers for template CRUD, budget generation endpoint, route wiring
|
||||
|
||||
#### Phase 6: Template Frontend and Workflow Replacement
|
||||
**Goal**: Users can manage their template on a dedicated page, navigate to any month and get a budget auto-generated from their template, and the old "copy from previous month" flow is gone
|
||||
@@ -145,7 +145,7 @@ Plans:
|
||||
## Progress
|
||||
|
||||
**Execution Order:**
|
||||
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
|
||||
Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8
|
||||
|
||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||
|-------|-----------|----------------|--------|-----------|
|
||||
@@ -153,7 +153,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
|
||||
| 2. Layout and Brand Identity | v1.0 | 2/2 | Complete | 2026-03-12 |
|
||||
| 3. Interaction Quality and Completeness | v1.0 | 4/4 | Complete | 2026-03-12 |
|
||||
| 4. Chart Polish and Bug Fixes | v1.0 | 2/2 | Complete | 2026-03-12 |
|
||||
| 5. Template Data Model and API | v1.1 | 0/2 | Not started | - |
|
||||
| 5. Template Data Model and API | v1.1 | 0/2 | Planning | - |
|
||||
| 6. Template Frontend and Workflow Replacement | v1.1 | 0/3 | Not started | - |
|
||||
| 7. Quick-Add Library | v1.1 | 0/2 | Not started | - |
|
||||
| 8. Layout and Density Rethink | v1.1 | 0/2 | Not started | - |
|
||||
|
||||
204
.planning/phases/05-template-data-model-and-api/05-01-PLAN.md
Normal file
204
.planning/phases/05-template-data-model-and-api/05-01-PLAN.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
phase: 05-template-data-model-and-api
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- backend/migrations/002_templates.sql
|
||||
- backend/internal/models/models.go
|
||||
- backend/internal/db/queries.go
|
||||
autonomous: true
|
||||
requirements: [TMPL-01, TMPL-02, TMPL-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "budget_items table has an item_tier column with values fixed, variable, one_off"
|
||||
- "templates table exists with one-per-user constraint"
|
||||
- "template_items table excludes one_off via CHECK constraint"
|
||||
- "Template and TemplateItem Go structs exist with JSON tags"
|
||||
- "Query functions exist for template CRUD and budget generation from template"
|
||||
- "GetBudgetWithItems returns item_tier in budget item rows"
|
||||
- "CreateBudgetItem and UpdateBudgetItem accept item_tier parameter"
|
||||
artifacts:
|
||||
- path: "backend/migrations/002_templates.sql"
|
||||
provides: "item_tier enum, templates table, template_items table, item_tier column on budget_items"
|
||||
contains: "CREATE TYPE item_tier"
|
||||
- path: "backend/internal/models/models.go"
|
||||
provides: "ItemTier type, Template struct, TemplateItem struct, updated BudgetItem"
|
||||
contains: "ItemTier"
|
||||
- path: "backend/internal/db/queries.go"
|
||||
provides: "Template query functions, updated budget item queries with item_tier"
|
||||
contains: "GetTemplate"
|
||||
key_links:
|
||||
- from: "backend/internal/db/queries.go"
|
||||
to: "backend/internal/models/models.go"
|
||||
via: "Template/TemplateItem struct usage in query returns"
|
||||
pattern: "models\\.Template"
|
||||
- from: "backend/migrations/002_templates.sql"
|
||||
to: "backend/internal/db/queries.go"
|
||||
via: "SQL column names match Scan field order"
|
||||
pattern: "item_tier"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the database migration for the template system (item_tier enum, templates table, template_items table, item_tier column on budget_items) and implement all Go model types and database query functions.
|
||||
|
||||
Purpose: Establish the data layer foundation that handlers (Plan 02) will call.
|
||||
Output: Migration SQL, updated models, complete query functions for templates and updated budget item queries.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/05-template-data-model-and-api/05-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing code contracts the executor needs -->
|
||||
|
||||
From backend/internal/models/models.go:
|
||||
```go
|
||||
type CategoryType string
|
||||
// Constants: CategoryBill, CategoryVariableExpense, CategoryDebt, CategorySaving, CategoryInvestment, CategoryIncome
|
||||
|
||||
type BudgetItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
BudgetID uuid.UUID `json:"budget_id"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name,omitempty"`
|
||||
CategoryType CategoryType `json:"category_type,omitempty"`
|
||||
BudgetedAmount decimal.Decimal `json:"budgeted_amount"`
|
||||
ActualAmount decimal.Decimal `json:"actual_amount"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BudgetDetail struct {
|
||||
Budget
|
||||
Items []BudgetItem `json:"items"`
|
||||
Totals BudgetTotals `json:"totals"`
|
||||
}
|
||||
```
|
||||
|
||||
From backend/internal/db/queries.go:
|
||||
```go
|
||||
type Queries struct { pool *pgxpool.Pool }
|
||||
func NewQueries(pool *pgxpool.Pool) *Queries
|
||||
func (q *Queries) CreateBudgetItem(ctx context.Context, budgetID, categoryID uuid.UUID, budgeted, actual decimal.Decimal, notes string) (*models.BudgetItem, error)
|
||||
func (q *Queries) UpdateBudgetItem(ctx context.Context, id, budgetID uuid.UUID, budgeted, actual decimal.Decimal, notes string) (*models.BudgetItem, error)
|
||||
func (q *Queries) GetBudgetWithItems(ctx context.Context, id, userID uuid.UUID) (*models.BudgetDetail, error)
|
||||
func (q *Queries) CopyBudgetItems(ctx context.Context, targetBudgetID, sourceBudgetID, userID uuid.UUID) error
|
||||
```
|
||||
|
||||
From backend/migrations/001_initial.sql:
|
||||
```sql
|
||||
-- budget_items has: id, budget_id, category_id, budgeted_amount, actual_amount, notes, created_at, updated_at
|
||||
-- No item_type or item_tier column exists yet
|
||||
-- category_type ENUM already exists as a pattern to follow
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migration SQL and Go model types</name>
|
||||
<files>backend/migrations/002_templates.sql, backend/internal/models/models.go</files>
|
||||
<action>
|
||||
1. Create `backend/migrations/002_templates.sql` with:
|
||||
- CREATE TYPE item_tier AS ENUM ('fixed', 'variable', 'one_off')
|
||||
- ALTER TABLE budget_items ADD COLUMN item_tier item_tier NOT NULL DEFAULT 'fixed'
|
||||
(default 'fixed' because existing items were created via copy-from-previous, treating all as recurring)
|
||||
- CREATE TABLE templates: id UUID PK DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL DEFAULT 'My Template', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
- CREATE UNIQUE INDEX idx_templates_user_id ON templates (user_id)
|
||||
- CREATE TABLE template_items: id UUID PK DEFAULT uuid_generate_v4(), template_id UUID NOT NULL REFERENCES templates(id) ON DELETE CASCADE, category_id UUID NOT NULL REFERENCES categories(id) ON DELETE RESTRICT, item_tier item_tier NOT NULL, budgeted_amount NUMERIC(12,2), sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
- CHECK constraint on template_items: item_tier IN ('fixed', 'variable') — one_off cannot exist in templates (DB-level enforcement per TMPL-04)
|
||||
- CREATE INDEX idx_template_items_template_id ON template_items (template_id)
|
||||
|
||||
2. Update `backend/internal/models/models.go`:
|
||||
- Add ItemTier type (string) with constants: ItemTierFixed = "fixed", ItemTierVariable = "variable", ItemTierOneOff = "one_off"
|
||||
- Add ItemTier field to BudgetItem struct: `ItemTier ItemTier json:"item_tier"` — place it after CategoryType field
|
||||
- Add Template struct: ID uuid.UUID, UserID uuid.UUID, Name string, CreatedAt time.Time, UpdatedAt time.Time (all with json tags)
|
||||
- Add TemplateItem struct: ID uuid.UUID, TemplateID uuid.UUID, CategoryID uuid.UUID, CategoryName string (omitempty), CategoryType CategoryType (omitempty), CategoryIcon string (omitempty), ItemTier ItemTier, BudgetedAmount *decimal.Decimal (pointer for nullable), SortOrder int, CreatedAt time.Time, UpdatedAt time.Time (all with json tags)
|
||||
- Add TemplateDetail struct: Template embedded + Items []TemplateItem json:"items"
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/backend && go vet ./internal/models/...</automated>
|
||||
</verify>
|
||||
<done>Migration file exists with item_tier enum, budget_items ALTER, templates table, template_items table with CHECK constraint. Models compile with ItemTier type, Template, TemplateItem, TemplateDetail structs.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Database query functions for templates and updated budget item queries</name>
|
||||
<files>backend/internal/db/queries.go</files>
|
||||
<action>
|
||||
Update `backend/internal/db/queries.go` with the following changes:
|
||||
|
||||
**Update existing budget item queries to include item_tier:**
|
||||
|
||||
1. `CreateBudgetItem` — add `itemTier models.ItemTier` parameter (after notes). Update INSERT to include item_tier column. Update RETURNING to include item_tier. Update Scan to include `&i.ItemTier`. Default: if itemTier is empty string, use "one_off" (per context decision: new items default to one_off).
|
||||
|
||||
2. `UpdateBudgetItem` — add `itemTier models.ItemTier` parameter. Update SET to include item_tier. Update RETURNING and Scan to include item_tier.
|
||||
|
||||
3. `GetBudgetWithItems` — update SELECT to include `bi.item_tier` after `c.type`. Update Scan to include `&i.ItemTier` (between CategoryType and BudgetedAmount).
|
||||
|
||||
4. `CopyBudgetItems` — update INSERT...SELECT to copy item_tier column from source items.
|
||||
|
||||
**Add new template query functions:**
|
||||
|
||||
5. `GetTemplate(ctx, userID) (*models.TemplateDetail, error)` — SELECT template by user_id. If no rows, return `&models.TemplateDetail{Items: []models.TemplateItem{}}` with zero-value Template (ID will be uuid.Nil). If found, query template_items JOIN categories (get c.name, c.type, c.icon) ORDER BY sort_order, return TemplateDetail with items. This handles the "no template yet returns empty" case.
|
||||
|
||||
6. `UpdateTemplateName(ctx, userID uuid.UUID, name string) (*models.Template, error)` — UPDATE templates SET name WHERE user_id. Return error if no template exists.
|
||||
|
||||
7. `CreateTemplateItem(ctx, userID uuid.UUID, categoryID uuid.UUID, itemTier models.ItemTier, budgetedAmount *decimal.Decimal, sortOrder int) (*models.TemplateItem, error)` — First ensure template exists: INSERT INTO templates (user_id) VALUES ($1) ON CONFLICT (user_id) DO UPDATE SET updated_at = now() RETURNING id. Then INSERT INTO template_items using the template_id. RETURNING with JOIN to get category details. This implements lazy template creation.
|
||||
|
||||
8. `UpdateTemplateItem(ctx, userID, itemID uuid.UUID, itemTier models.ItemTier, budgetedAmount *decimal.Decimal, sortOrder int) (*models.TemplateItem, error)` — UPDATE template_items SET ... WHERE id = $1 AND template_id = (SELECT id FROM templates WHERE user_id = $2). Return with joined category details.
|
||||
|
||||
9. `DeleteTemplateItem(ctx, userID, itemID uuid.UUID) error` — DELETE FROM template_items WHERE id = $1 AND template_id = (SELECT id FROM templates WHERE user_id = $2).
|
||||
|
||||
10. `ReorderTemplateItems(ctx, userID uuid.UUID, itemOrders []struct{ID uuid.UUID; SortOrder int}) error` — Batch update sort_order for multiple items. Use a transaction. Verify each item belongs to user's template.
|
||||
|
||||
11. `GenerateBudgetFromTemplate(ctx, userID uuid.UUID, month string, currency string) (*models.BudgetDetail, error)` — Single transaction:
|
||||
a. Parse month string ("2026-04") to compute startDate (first of month) and endDate (last of month).
|
||||
b. Check if budget with overlapping dates exists for user. If yes, return a sentinel error (define `var ErrBudgetExists = fmt.Errorf("budget already exists")`) and include the existing budget ID.
|
||||
c. Get user's preferred_locale from users table to format budget name (use a simple map: "de" -> German month names, "en" -> English month names, default to English). Budget name = "MonthName Year" (e.g. "April 2026" or "April 2026" in German).
|
||||
d. Create budget with computed name, dates, currency, zero carryover.
|
||||
e. Get template items for user (if no template or no items, return the empty budget — no error per context decision).
|
||||
f. For each template item: INSERT INTO budget_items with budget_id, category_id, item_tier, budgeted_amount (use template amount for fixed, 0 for variable), actual_amount = 0, notes = ''.
|
||||
g. Return full BudgetDetail (reuse GetBudgetWithItems pattern).
|
||||
|
||||
For the ErrBudgetExists sentinel, also create a BudgetExistsError struct type that holds the existing budget_id so the handler can include it in the 409 response.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/backend && go vet ./...</automated>
|
||||
</verify>
|
||||
<done>All query functions compile. CreateBudgetItem/UpdateBudgetItem accept item_tier. GetBudgetWithItems returns item_tier. Template CRUD queries exist. GenerateBudgetFromTemplate creates budget + items from template in a transaction. ErrBudgetExists sentinel defined.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `go vet ./...` passes with no errors from the backend directory
|
||||
- Models file contains ItemTier type with 3 constants, Template, TemplateItem, TemplateDetail structs
|
||||
- Queries file contains all 7 new template functions plus updated budget item functions
|
||||
- Migration SQL is syntactically valid (item_tier enum, ALTER budget_items, CREATE templates, CREATE template_items with CHECK)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Migration 002 creates item_tier type, adds column to budget_items, creates templates and template_items tables
|
||||
- BudgetItem struct includes ItemTier field returned in JSON
|
||||
- Template CRUD queries handle lazy creation and user scoping
|
||||
- GenerateBudgetFromTemplate handles empty template, duplicate month (409), and normal generation
|
||||
- `go vet ./...` passes cleanly
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-template-data-model-and-api/05-01-SUMMARY.md`
|
||||
</output>
|
||||
261
.planning/phases/05-template-data-model-and-api/05-02-PLAN.md
Normal file
261
.planning/phases/05-template-data-model-and-api/05-02-PLAN.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
phase: 05-template-data-model-and-api
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [05-01]
|
||||
files_modified:
|
||||
- backend/internal/api/handlers.go
|
||||
- backend/internal/api/router.go
|
||||
autonomous: true
|
||||
requirements: [TMPL-01, TMPL-02, TMPL-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "GET /api/template returns template with items (or empty if none exists)"
|
||||
- "PUT /api/template updates template name"
|
||||
- "POST /api/template/items adds item and auto-creates template"
|
||||
- "PUT /api/template/items/{itemId} updates a template item"
|
||||
- "DELETE /api/template/items/{itemId} removes a template item"
|
||||
- "PUT /api/template/items/reorder batch-updates sort order"
|
||||
- "POST /api/budgets/generate creates budget from template or returns 409 if exists"
|
||||
- "POST /api/budgets/{id}/items accepts optional item_tier field"
|
||||
- "PUT /api/budgets/{id}/items/{itemId} accepts optional item_tier field"
|
||||
- "GET /api/budgets/{id} returns item_tier in each budget item"
|
||||
- "One-off items cannot be added to templates (rejected by DB CHECK)"
|
||||
artifacts:
|
||||
- path: "backend/internal/api/handlers.go"
|
||||
provides: "Template handlers, Generate handler, updated budget item handlers"
|
||||
contains: "GetTemplate"
|
||||
- path: "backend/internal/api/router.go"
|
||||
provides: "/api/template routes and /api/budgets/generate route"
|
||||
contains: "template"
|
||||
key_links:
|
||||
- from: "backend/internal/api/handlers.go"
|
||||
to: "backend/internal/db/queries.go"
|
||||
via: "h.queries.GetTemplate, h.queries.GenerateBudgetFromTemplate"
|
||||
pattern: "queries\\.(GetTemplate|GenerateBudgetFromTemplate|CreateTemplateItem)"
|
||||
- from: "backend/internal/api/router.go"
|
||||
to: "backend/internal/api/handlers.go"
|
||||
via: "route registration to handler methods"
|
||||
pattern: "h\\.(GetTemplate|GenerateBudget)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire HTTP handlers and routes for the template API and budget generation endpoint. Update existing budget item handlers to pass item_tier through to query functions.
|
||||
|
||||
Purpose: Expose the data layer (from Plan 01) via REST API so the frontend (Phase 6) can manage templates and generate budgets.
|
||||
Output: Complete, working API endpoints for template CRUD, reorder, and budget-from-template generation.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/05-template-data-model-and-api/05-CONTEXT.md
|
||||
@.planning/phases/05-template-data-model-and-api/05-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts created by Plan 01 that this plan consumes -->
|
||||
|
||||
From backend/internal/models/models.go (after Plan 01):
|
||||
```go
|
||||
type ItemTier string
|
||||
const (
|
||||
ItemTierFixed ItemTier = "fixed"
|
||||
ItemTierVariable ItemTier = "variable"
|
||||
ItemTierOneOff ItemTier = "one_off"
|
||||
)
|
||||
|
||||
type BudgetItem struct {
|
||||
// ... existing fields ...
|
||||
ItemTier ItemTier `json:"item_tier"` // NEW
|
||||
// ...
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TemplateItem struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TemplateID uuid.UUID `json:"template_id"`
|
||||
CategoryID uuid.UUID `json:"category_id"`
|
||||
CategoryName string `json:"category_name,omitempty"`
|
||||
CategoryType CategoryType `json:"category_type,omitempty"`
|
||||
CategoryIcon string `json:"category_icon,omitempty"`
|
||||
ItemTier ItemTier `json:"item_tier"`
|
||||
BudgetedAmount *decimal.Decimal `json:"budgeted_amount"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TemplateDetail struct {
|
||||
Template
|
||||
Items []TemplateItem `json:"items"`
|
||||
}
|
||||
```
|
||||
|
||||
From backend/internal/db/queries.go (after Plan 01):
|
||||
```go
|
||||
func (q *Queries) GetTemplate(ctx context.Context, userID uuid.UUID) (*models.TemplateDetail, error)
|
||||
func (q *Queries) UpdateTemplateName(ctx context.Context, userID uuid.UUID, name string) (*models.Template, error)
|
||||
func (q *Queries) CreateTemplateItem(ctx context.Context, userID, categoryID uuid.UUID, itemTier models.ItemTier, budgetedAmount *decimal.Decimal, sortOrder int) (*models.TemplateItem, error)
|
||||
func (q *Queries) UpdateTemplateItem(ctx context.Context, userID, itemID uuid.UUID, itemTier models.ItemTier, budgetedAmount *decimal.Decimal, sortOrder int) (*models.TemplateItem, error)
|
||||
func (q *Queries) DeleteTemplateItem(ctx context.Context, userID, itemID uuid.UUID) error
|
||||
func (q *Queries) ReorderTemplateItems(ctx context.Context, userID uuid.UUID, itemOrders []struct{ID uuid.UUID; SortOrder int}) error
|
||||
func (q *Queries) GenerateBudgetFromTemplate(ctx context.Context, userID uuid.UUID, month, currency string) (*models.BudgetDetail, error)
|
||||
// BudgetExistsError type with BudgetID field for 409 responses
|
||||
func (q *Queries) CreateBudgetItem(ctx context.Context, budgetID, categoryID uuid.UUID, budgeted, actual decimal.Decimal, notes string, itemTier models.ItemTier) (*models.BudgetItem, error)
|
||||
func (q *Queries) UpdateBudgetItem(ctx context.Context, id, budgetID uuid.UUID, budgeted, actual decimal.Decimal, notes string, itemTier models.ItemTier) (*models.BudgetItem, error)
|
||||
```
|
||||
|
||||
From backend/internal/api/handlers.go (existing patterns):
|
||||
```go
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{})
|
||||
func writeError(w http.ResponseWriter, status int, msg string)
|
||||
func decodeJSON(r *http.Request, v interface{}) error
|
||||
func parseUUID(s string) (uuid.UUID, error)
|
||||
// userID from context: auth.UserIDFromContext(r.Context())
|
||||
```
|
||||
|
||||
From backend/internal/api/router.go (existing patterns):
|
||||
```go
|
||||
// Protected routes use r.Group with auth.Middleware
|
||||
// Route groups: r.Route("/api/path", func(r chi.Router) { ... })
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Update budget item handlers and add template handlers</name>
|
||||
<files>backend/internal/api/handlers.go</files>
|
||||
<action>
|
||||
Update `backend/internal/api/handlers.go`:
|
||||
|
||||
**1. Update CreateBudgetItem handler:**
|
||||
Add `ItemTier models.ItemTier` field (json:"item_tier") to the request struct. After decoding, if req.ItemTier is empty string, default to models.ItemTierOneOff (per context decision). Pass req.ItemTier as the new parameter to h.queries.CreateBudgetItem.
|
||||
|
||||
**2. Update UpdateBudgetItem handler:**
|
||||
Add `ItemTier models.ItemTier` field to request struct. If empty, default to models.ItemTierOneOff. Pass to h.queries.UpdateBudgetItem.
|
||||
|
||||
**3. Add GetTemplate handler:**
|
||||
```
|
||||
func (h *Handlers) GetTemplate(w, r)
|
||||
```
|
||||
Get userID from context. Call h.queries.GetTemplate(ctx, userID). On error, 500. On success, writeJSON 200 with the TemplateDetail. The query already handles no-template case by returning empty.
|
||||
|
||||
**4. Add UpdateTemplateName handler:**
|
||||
```
|
||||
func (h *Handlers) UpdateTemplateName(w, r)
|
||||
```
|
||||
Decode request with Name field. Call h.queries.UpdateTemplateName(ctx, userID, name). On pgx.ErrNoRows (no template exists), return 404 "no template found". On success, writeJSON 200.
|
||||
|
||||
**5. Add CreateTemplateItem handler:**
|
||||
```
|
||||
func (h *Handlers) CreateTemplateItem(w, r)
|
||||
```
|
||||
Decode request struct: CategoryID uuid.UUID, ItemTier models.ItemTier, BudgetedAmount *decimal.Decimal, SortOrder int. Validate: ItemTier must be "fixed" or "variable" (reject "one_off" at handler level with 400 "one-off items cannot be added to templates" — defense in depth, DB CHECK also enforces). If ItemTier is "fixed" and BudgetedAmount is nil, return 400 "fixed items require budgeted_amount". Call h.queries.CreateTemplateItem. On success, writeJSON 201.
|
||||
|
||||
**6. Add UpdateTemplateItem handler:**
|
||||
```
|
||||
func (h *Handlers) UpdateTemplateItem(w, r)
|
||||
```
|
||||
Parse itemId from URL. Decode same fields as create (minus CategoryID — category cannot change). Same validation. Call h.queries.UpdateTemplateItem. On error 404, on success 200.
|
||||
|
||||
**7. Add DeleteTemplateItem handler:**
|
||||
```
|
||||
func (h *Handlers) DeleteTemplateItem(w, r)
|
||||
```
|
||||
Parse itemId from URL. Call h.queries.DeleteTemplateItem. On success, 204 No Content.
|
||||
|
||||
**8. Add ReorderTemplateItems handler:**
|
||||
```
|
||||
func (h *Handlers) ReorderTemplateItems(w, r)
|
||||
```
|
||||
Decode request: Items []struct{ ID uuid.UUID json:"id"; SortOrder int json:"sort_order" }. Call h.queries.ReorderTemplateItems. On success, 204 No Content.
|
||||
|
||||
**9. Add GenerateBudget handler:**
|
||||
```
|
||||
func (h *Handlers) GenerateBudget(w, r)
|
||||
```
|
||||
Decode request: Month string json:"month", Currency string json:"currency". Validate month format (YYYY-MM regex or time.Parse "2006-01"). If Currency empty, default "EUR". Call h.queries.GenerateBudgetFromTemplate(ctx, userID, month, currency).
|
||||
- On BudgetExistsError: writeJSON 409 with `{"error": "budget already exists", "budget_id": err.BudgetID.String()}`.
|
||||
- On other error: 500.
|
||||
- On success: writeJSON 201 with the BudgetDetail.
|
||||
|
||||
Use errors.As to check for BudgetExistsError type.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/backend && go vet ./...</automated>
|
||||
</verify>
|
||||
<done>All handler methods compile. CreateBudgetItem/UpdateBudgetItem pass item_tier. Template CRUD handlers validate input. GenerateBudget returns 201/409 correctly. go vet passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire routes and verify full compilation</name>
|
||||
<files>backend/internal/api/router.go</files>
|
||||
<action>
|
||||
Update `backend/internal/api/router.go` inside the protected routes group (r.Group with auth.Middleware):
|
||||
|
||||
1. Add template route group:
|
||||
```go
|
||||
r.Route("/api/template", func(r chi.Router) {
|
||||
r.Get("/", h.GetTemplate)
|
||||
r.Put("/", h.UpdateTemplateName)
|
||||
r.Post("/items", h.CreateTemplateItem)
|
||||
r.Put("/items/{itemId}", h.UpdateTemplateItem)
|
||||
r.Delete("/items/{itemId}", h.DeleteTemplateItem)
|
||||
r.Put("/items/reorder", h.ReorderTemplateItems)
|
||||
})
|
||||
```
|
||||
|
||||
2. Add generate endpoint inside the existing /api/budgets route group, BEFORE the /{id} routes (so "generate" isn't treated as an {id}):
|
||||
```go
|
||||
r.Post("/generate", h.GenerateBudget)
|
||||
```
|
||||
Place this line right after `r.Post("/", h.CreateBudget)` inside the /api/budgets route group.
|
||||
|
||||
3. Run `go build ./...` from backend directory to verify full compilation of the entire application.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/backend && go build ./... && go vet ./...</automated>
|
||||
</verify>
|
||||
<done>All routes registered. /api/template group serves GET, PUT, POST items, PUT/DELETE items/{itemId}, PUT items/reorder. /api/budgets/generate serves POST. Full backend compiles and vets clean.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `go build ./...` succeeds from backend directory — entire application compiles
|
||||
- `go vet ./...` reports no issues
|
||||
- router.go contains /api/template route group and /api/budgets/generate endpoint
|
||||
- handlers.go contains 7 new handler methods plus 2 updated ones
|
||||
- Template GET returns empty items array when no template exists (not an error)
|
||||
- GenerateBudget returns 409 with budget_id when month already has a budget
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All template API endpoints are routed and handled: GET/PUT /api/template, POST/PUT/DELETE /api/template/items, PUT /api/template/items/reorder
|
||||
- POST /api/budgets/generate creates budget from template or returns 409
|
||||
- Budget item create/update endpoints accept item_tier field
|
||||
- GET budget detail returns item_tier in each item
|
||||
- One-off items rejected at handler level when adding to template
|
||||
- Full backend compiles with `go build ./...`
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-template-data-model-and-api/05-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user