Files
SimpleFinanceDash/.planning/phases/05-template-data-model-and-api/05-02-PLAN.md

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
05-template-data-model-and-api 02 execute 2
05-01
backend/internal/api/handlers.go
backend/internal/api/router.go
true
TMPL-01
TMPL-02
TMPL-04
truths artifacts key_links
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)
path provides contains
backend/internal/api/handlers.go Template handlers, Generate handler, updated budget item handlers GetTemplate
path provides contains
backend/internal/api/router.go /api/template routes and /api/budgets/generate route template
from to via pattern
backend/internal/api/handlers.go backend/internal/db/queries.go h.queries.GetTemplate, h.queries.GenerateBudgetFromTemplate queries.(GetTemplate|GenerateBudgetFromTemplate|CreateTemplateItem)
from to via pattern
backend/internal/api/router.go backend/internal/api/handlers.go route registration to handler methods h.(GetTemplate|GenerateBudget)
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.

<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>

@.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

From backend/internal/models/models.go (after Plan 01):

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):

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):

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):

// Protected routes use r.Group with auth.Middleware
// Route groups: r.Route("/api/path", func(r chi.Router) { ... })
Task 1: Update budget item handlers and add template handlers backend/internal/api/handlers.go 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. cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/backend && go vet ./... All handler methods compile. CreateBudgetItem/UpdateBudgetItem pass item_tier. Template CRUD handlers validate input. GenerateBudget returns 201/409 correctly. go vet passes.

Task 2: Wire routes and verify full compilation backend/internal/api/router.go Update `backend/internal/api/router.go` inside the protected routes group (r.Group with auth.Middleware):
  1. Add template route group:
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)
})
  1. Add generate endpoint inside the existing /api/budgets route group, BEFORE the /{id} routes (so "generate" isn't treated as an {id}):
r.Post("/generate", h.GenerateBudget)

Place this line right after r.Post("/", h.CreateBudget) inside the /api/budgets route group.

  1. Run go build ./... from backend directory to verify full compilation of the entire application. cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/backend && go build ./... && go vet ./... 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.
- `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

<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>
After completion, create `.planning/phases/05-template-data-model-and-api/05-02-SUMMARY.md`