262 lines
12 KiB
Markdown
262 lines
12 KiB
Markdown
---
|
|
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>
|