docs(05): create phase plan
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user