284 lines
11 KiB
Markdown
284 lines
11 KiB
Markdown
---
|
|
phase: 06-preset-data-first-run-detection-and-db-safety
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/data/presets.ts
|
|
- src/i18n/en.json
|
|
- src/i18n/de.json
|
|
autonomous: true
|
|
requirements:
|
|
- SETUP-02
|
|
|
|
must_haves:
|
|
truths:
|
|
- "src/data/presets.ts exports a PRESETS array of exactly 19 typed items"
|
|
- "PRESETS covers all 6 category types with the agreed distribution (4+4+5+2+2+2)"
|
|
- "Every preset item has a slug, type (valid CategoryType), defaultAmount (round number EUR), and item_tier (fixed or variable)"
|
|
- "en.json has a top-level presets key with all 19 slugs translated to English"
|
|
- "de.json has a top-level presets key with all 19 slugs translated to German"
|
|
artifacts:
|
|
- path: "src/data/presets.ts"
|
|
provides: "PresetItem interface and PRESETS array"
|
|
exports: ["PresetItem", "PRESETS"]
|
|
- path: "src/i18n/en.json"
|
|
provides: "English preset translations under presets.* key"
|
|
contains: "presets"
|
|
- path: "src/i18n/de.json"
|
|
provides: "German preset translations under presets.* key"
|
|
contains: "presets"
|
|
key_links:
|
|
- from: "src/data/presets.ts"
|
|
to: "src/lib/types.ts"
|
|
via: "import CategoryType"
|
|
pattern: "import.*CategoryType.*from.*types"
|
|
- from: "src/i18n/en.json"
|
|
to: "presets.{type}.{slug}"
|
|
via: "react-i18next t() dot-path"
|
|
pattern: "\"presets\":"
|
|
---
|
|
|
|
<objective>
|
|
Create the static preset budget item library and its i18n translations.
|
|
|
|
`src/data/presets.ts` exports `PresetItem` interface and `PRESETS` array with 19 items across 6 category types (4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment). Both `src/i18n/en.json` and `src/i18n/de.json` get a new top-level `"presets"` key containing all 19 English/German display names.
|
|
|
|
Purpose: This is the curated item library the Phase 7 wizard shows to new users for one-click budget template setup. All amounts are plain EUR numbers — the wizard reads currency from `profiles.currency`, not from this file.
|
|
Output: `src/data/presets.ts`, updated `en.json`, updated `de.json`.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-CONTEXT.md
|
|
@.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md
|
|
|
|
<interfaces>
|
|
From src/lib/types.ts:
|
|
```typescript
|
|
export type CategoryType =
|
|
| "income"
|
|
| "bill"
|
|
| "variable_expense"
|
|
| "debt"
|
|
| "saving"
|
|
| "investment"
|
|
|
|
export type ItemTier = "fixed" | "variable" | "one_off"
|
|
```
|
|
|
|
From src/i18n/en.json — existing top-level structure (add "presets" alongside these):
|
|
"app", "nav", "auth", "categories", "template", "budget", "settings", "common"
|
|
i18n library: react-i18next — uses t('dot.path.key') syntax confirmed.
|
|
|
|
NOTE: Do NOT hardcode currency symbols in preset display names or amounts.
|
|
Amounts are plain numbers (EUR value). The wizard (Phase 7) will format them.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create src/data/presets.ts — 19-item preset library</name>
|
|
<files>src/data/presets.ts</files>
|
|
<read_first>
|
|
- src/lib/types.ts
|
|
- .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 4: Preset Data File Shape)
|
|
</read_first>
|
|
<action>
|
|
Create `src/data/presets.ts`. The file must NOT import from Supabase or React — it is a pure static data module.
|
|
|
|
```typescript
|
|
import type { CategoryType } from "@/lib/types"
|
|
|
|
export interface PresetItem {
|
|
slug: string
|
|
type: CategoryType
|
|
defaultAmount: number // EUR, round number — do NOT suffix with currency symbol
|
|
item_tier: "fixed" | "variable"
|
|
}
|
|
|
|
export const PRESETS: PresetItem[] = [
|
|
// income (4)
|
|
{ slug: "salary", type: "income", defaultAmount: 3000, item_tier: "fixed" },
|
|
{ slug: "freelance", type: "income", defaultAmount: 500, item_tier: "variable" },
|
|
{ slug: "rental_income", type: "income", defaultAmount: 800, item_tier: "fixed" },
|
|
{ slug: "other_income", type: "income", defaultAmount: 200, item_tier: "variable" },
|
|
// bill (4)
|
|
{ slug: "rent", type: "bill", defaultAmount: 1000, item_tier: "fixed" },
|
|
{ slug: "electricity", type: "bill", defaultAmount: 80, item_tier: "fixed" },
|
|
{ slug: "internet", type: "bill", defaultAmount: 40, item_tier: "fixed" },
|
|
{ slug: "phone", type: "bill", defaultAmount: 30, item_tier: "fixed" },
|
|
// variable_expense (5)
|
|
{ slug: "groceries", type: "variable_expense", defaultAmount: 400, item_tier: "variable" },
|
|
{ slug: "transport", type: "variable_expense", defaultAmount: 100, item_tier: "variable" },
|
|
{ slug: "dining_out", type: "variable_expense", defaultAmount: 150, item_tier: "variable" },
|
|
{ slug: "health", type: "variable_expense", defaultAmount: 50, item_tier: "variable" },
|
|
{ slug: "clothing", type: "variable_expense", defaultAmount: 100, item_tier: "variable" },
|
|
// debt (2)
|
|
{ slug: "loan_repayment", type: "debt", defaultAmount: 200, item_tier: "fixed" },
|
|
{ slug: "credit_card", type: "debt", defaultAmount: 100, item_tier: "fixed" },
|
|
// saving (2)
|
|
{ slug: "emergency_fund", type: "saving", defaultAmount: 200, item_tier: "fixed" },
|
|
{ slug: "vacation", type: "saving", defaultAmount: 100, item_tier: "fixed" },
|
|
// investment (2)
|
|
{ slug: "etf", type: "investment", defaultAmount: 200, item_tier: "fixed" },
|
|
{ slug: "pension", type: "investment", defaultAmount: 100, item_tier: "fixed" },
|
|
]
|
|
```
|
|
|
|
The `item_tier` for all items must be either `"fixed"` or `"variable"` — never `"one_off"` (which is excluded from template_items by the DB check constraint).
|
|
</action>
|
|
<verify>
|
|
<automated>grep -c "slug:" src/data/presets.ts</automated>
|
|
</verify>
|
|
<done>File exists. Contains exactly 19 objects (grep -c "slug:" returns 19). All `type` values are valid CategoryType strings. No `item_tier: "one_off"` present. `tsc --noEmit` passes.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add preset translations to en.json and de.json</name>
|
|
<files>src/i18n/en.json, src/i18n/de.json</files>
|
|
<read_first>
|
|
- src/i18n/en.json
|
|
- src/i18n/de.json
|
|
</read_first>
|
|
<action>
|
|
Add a top-level `"presets"` key to both i18n files. The key structure is `presets.{category_type}.{slug}` — matching the `type` and `slug` fields in PRESETS.
|
|
|
|
For `src/i18n/en.json`, add after the last existing top-level key:
|
|
```json
|
|
"presets": {
|
|
"income": {
|
|
"salary": "Salary",
|
|
"freelance": "Freelance Income",
|
|
"rental_income": "Rental Income",
|
|
"other_income": "Other Income"
|
|
},
|
|
"bill": {
|
|
"rent": "Rent",
|
|
"electricity": "Electricity",
|
|
"internet": "Internet",
|
|
"phone": "Phone"
|
|
},
|
|
"variable_expense": {
|
|
"groceries": "Groceries",
|
|
"transport": "Transport",
|
|
"dining_out": "Dining Out",
|
|
"health": "Health & Pharmacy",
|
|
"clothing": "Clothing"
|
|
},
|
|
"debt": {
|
|
"loan_repayment": "Loan Repayment",
|
|
"credit_card": "Credit Card"
|
|
},
|
|
"saving": {
|
|
"emergency_fund": "Emergency Fund",
|
|
"vacation": "Vacation Fund"
|
|
},
|
|
"investment": {
|
|
"etf": "ETF / Index Fund",
|
|
"pension": "Pension"
|
|
}
|
|
}
|
|
```
|
|
|
|
For `src/i18n/de.json`, add the same structure with German translations:
|
|
```json
|
|
"presets": {
|
|
"income": {
|
|
"salary": "Gehalt",
|
|
"freelance": "Freelance-Einkommen",
|
|
"rental_income": "Mieteinnahmen",
|
|
"other_income": "Sonstiges Einkommen"
|
|
},
|
|
"bill": {
|
|
"rent": "Miete",
|
|
"electricity": "Strom",
|
|
"internet": "Internet",
|
|
"phone": "Telefon"
|
|
},
|
|
"variable_expense": {
|
|
"groceries": "Lebensmittel",
|
|
"transport": "Transport",
|
|
"dining_out": "Auswärts essen",
|
|
"health": "Gesundheit & Apotheke",
|
|
"clothing": "Kleidung"
|
|
},
|
|
"debt": {
|
|
"loan_repayment": "Kreditrückzahlung",
|
|
"credit_card": "Kreditkarte"
|
|
},
|
|
"saving": {
|
|
"emergency_fund": "Notfallfonds",
|
|
"vacation": "Urlaubskasse"
|
|
},
|
|
"investment": {
|
|
"etf": "ETF / Indexfonds",
|
|
"pension": "Altersvorsorge"
|
|
}
|
|
}
|
|
```
|
|
|
|
Both files must remain valid JSON after the edit. Add the `"presets"` key as the last entry in each JSON object (before the closing `}`), preceded by a comma on the previous last key.
|
|
</action>
|
|
<verify>
|
|
<automated>node -e "JSON.parse(require('fs').readFileSync('src/i18n/en.json','utf8')); JSON.parse(require('fs').readFileSync('src/i18n/de.json','utf8')); console.log('JSON valid')" && grep -c '"slug_key"' src/i18n/en.json || grep -c '"salary"' src/i18n/en.json</automated>
|
|
</verify>
|
|
<done>Both JSON files are valid (node JSON.parse succeeds). `en.json` and `de.json` each contain a top-level `"presets"` key. `grep '"salary"' src/i18n/en.json` returns 1 match. `grep '"Gehalt"' src/i18n/de.json` returns 1 match.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| presets.ts → wizard UI | Static read-only data — no user input, no network calls |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-06-04 | Information Disclosure | presets.ts amounts | accept | Amounts are generic EUR defaults, not user-specific data; public knowledge |
|
|
| T-06-05 | Tampering | i18n JSON malformed after edit | mitigate | Verify both JSON files parse cleanly after edit (`node -e "JSON.parse(...)"`) before committing |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
```bash
|
|
# Confirm presets file exists with 19 items
|
|
grep -c "slug:" src/data/presets.ts
|
|
|
|
# Confirm no one_off tier
|
|
grep "one_off" src/data/presets.ts || echo "no one_off found (correct)"
|
|
|
|
# Confirm i18n JSON files are valid
|
|
node -e "JSON.parse(require('fs').readFileSync('src/i18n/en.json','utf8')); console.log('en.json valid')"
|
|
node -e "JSON.parse(require('fs').readFileSync('src/i18n/de.json','utf8')); console.log('de.json valid')"
|
|
|
|
# Confirm presets key present in both
|
|
grep '"presets"' src/i18n/en.json src/i18n/de.json
|
|
|
|
# TypeScript clean
|
|
npx tsc --noEmit
|
|
```
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `src/data/presets.ts` exports `PresetItem` interface and `PRESETS` array with exactly 19 items
|
|
- Distribution: 4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment
|
|
- All `item_tier` values are `"fixed"` or `"variable"` only
|
|
- `en.json` and `de.json` both contain a valid `"presets"` nested object with all 19 slugs translated
|
|
- Both JSON files remain valid (parseable) after edits
|
|
- `tsc --noEmit` passes
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-02-SUMMARY.md`
|
|
</output>
|