Files
SimpleFinanceDash/.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-01-PLAN.md

257 lines
10 KiB
Markdown

---
phase: 06-preset-data-first-run-detection-and-db-safety
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- supabase/migrations/006_uniqueness_constraints.sql
- supabase/migrations/007_setup_completed.sql
- src/lib/types.ts
autonomous: true
requirements:
- AUTO-01
- AUTO-03
- SETUP-01
must_haves:
truths:
- "Migration 006 adds a unique constraint on budgets(user_id, start_date) with safe deduplication"
- "Migration 006 adds a unique constraint on categories(user_id, name) with safe deduplication"
- "Migration 007 adds setup_completed boolean NOT NULL DEFAULT false to profiles"
- "Migration 007 backfills setup_completed = true for all users with existing categories"
- "Profile TypeScript interface includes setup_completed: boolean"
artifacts:
- path: "supabase/migrations/006_uniqueness_constraints.sql"
provides: "Atomic deduplication + unique constraint DDL for budgets and categories"
- path: "supabase/migrations/007_setup_completed.sql"
provides: "ALTER TABLE profiles ADD COLUMN setup_completed + backfill UPDATE"
- path: "src/lib/types.ts"
provides: "Updated Profile interface with setup_completed field"
contains: "setup_completed: boolean"
key_links:
- from: "supabase/migrations/007_setup_completed.sql"
to: "profiles table"
via: "ALTER TABLE profiles ADD COLUMN"
pattern: "setup_completed boolean NOT NULL DEFAULT false"
- from: "src/lib/types.ts"
to: "Profile interface"
via: "TypeScript field"
pattern: "setup_completed: boolean"
---
<objective>
Write two Supabase migration files and update the Profile TypeScript type.
Migration 006 makes duplicate budget/category creation impossible at the DB level by first deduplicating any existing rows then adding UNIQUE constraints. Migration 007 adds the `setup_completed` boolean column to `profiles` and backfills existing users who have categories to `true`. The TypeScript `Profile` interface in `src/lib/types.ts` is updated to include `setup_completed: boolean`.
Purpose: The DB layer must enforce uniqueness atomically (prevents race conditions) and must know which users have already completed setup (prevents existing v1.0 users from seeing the wizard on first v2.0 login).
Output: 2 `.sql` migration files + 1 updated TypeScript type file.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.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>
<!-- Existing migration chain: 001-005. New files must be named 006_... and 007_... for correct lexicographic ordering. -->
From supabase/migrations/001_profiles.sql — profiles table columns:
id, display_name, locale, currency, created_at, updated_at
(NO setup_completed yet)
From supabase/migrations/002_categories.sql — categories table:
id, user_id, name, type, icon, sort_order, created_at, updated_at
Existing index: categories_user_id_idx
(NO unique constraint on (user_id, name) yet)
From supabase/migrations/004_budgets.sql — budgets table:
id, user_id, start_date, end_date, currency, carryover_amount, created_at, updated_at
Existing index: budgets_user_id_idx
(NO unique constraint on (user_id, start_date) yet)
From src/lib/types.ts — Profile interface (before this plan):
export interface Profile {
id: string
display_name: string | null
locale: string
currency: string
created_at: string
updated_at: string
}
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Write migration 006 — uniqueness constraints with safe deduplication</name>
<files>supabase/migrations/006_uniqueness_constraints.sql</files>
<read_first>
- supabase/migrations/002_categories.sql
- supabase/migrations/004_budgets.sql
- .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 1: Safe Deduplication)
</read_first>
<action>
Create `supabase/migrations/006_uniqueness_constraints.sql` with this exact content — a single transaction that deduplicates then constrains:
```sql
-- Migration 006: Add uniqueness constraints to budgets and categories
-- Safe deduplication runs first inside the transaction before each constraint.
BEGIN;
-- Deduplicate budgets: keep the oldest row per (user_id, start_date)
DELETE FROM budgets
WHERE id NOT IN (
SELECT DISTINCT ON (user_id, start_date) id
FROM budgets
ORDER BY user_id, start_date, created_at ASC
);
ALTER TABLE budgets
ADD CONSTRAINT budgets_user_month_unique UNIQUE (user_id, start_date);
-- Deduplicate categories: keep the oldest row per (user_id, name)
DELETE FROM categories
WHERE id NOT IN (
SELECT DISTINCT ON (user_id, name) id
FROM categories
ORDER BY user_id, name, created_at ASC
);
ALTER TABLE categories
ADD CONSTRAINT categories_user_name_unique UNIQUE (user_id, name);
COMMIT;
```
Key: DISTINCT ON requires ORDER BY on the same leading columns — already satisfied above. Wrapping both operations in a single transaction means if constraint ADD fails, cleanup rolls back too.
</action>
<verify>
<automated>grep -c "ADD CONSTRAINT" supabase/migrations/006_uniqueness_constraints.sql && grep -c "BEGIN" supabase/migrations/006_uniqueness_constraints.sql && grep -c "COMMIT" supabase/migrations/006_uniqueness_constraints.sql</automated>
</verify>
<done>File exists. Contains exactly 2 ADD CONSTRAINT statements, 1 BEGIN, 1 COMMIT. Both constraints named: budgets_user_month_unique and categories_user_name_unique.</done>
</task>
<task type="auto">
<name>Task 2: Write migration 007 — setup_completed column + backfill</name>
<files>supabase/migrations/007_setup_completed.sql</files>
<read_first>
- supabase/migrations/001_profiles.sql
- .planning/phases/06-preset-data-first-run-detection-and-db-safety/06-RESEARCH.md (Pattern 2: ADD COLUMN with Default + Backfill, Pitfall 3)
</read_first>
<action>
Create `supabase/migrations/007_setup_completed.sql` with this exact content:
```sql
-- Migration 007: Add setup_completed to profiles
-- New signups default to false (not set up).
-- Existing users who have any categories are backfilled to true (already set up).
-- Wider backfill also includes users with template items to protect against
-- edge case where user created template items but skipped category creation.
ALTER TABLE profiles
ADD COLUMN setup_completed boolean NOT NULL DEFAULT false;
-- Backfill: users with categories OR template items are considered set up
UPDATE profiles
SET setup_completed = true
WHERE id IN (
SELECT DISTINCT user_id FROM categories
UNION
SELECT t.user_id FROM templates t
INNER JOIN template_items ti ON ti.template_id = t.id
);
```
Note: The wider backfill UNION (categories OR template items) matches Pitfall 3 guidance from RESEARCH.md — protects v1.0 users who may have template items but no categories.
</action>
<verify>
<automated>grep -c "ADD COLUMN setup_completed" supabase/migrations/007_setup_completed.sql && grep -c "UPDATE profiles" supabase/migrations/007_setup_completed.sql</automated>
</verify>
<done>File exists. Contains ADD COLUMN statement with `boolean NOT NULL DEFAULT false` and UPDATE statement with UNION backfill covering both categories and template_items.</done>
</task>
<task type="auto">
<name>Task 3: Update Profile TypeScript interface to include setup_completed</name>
<files>src/lib/types.ts</files>
<read_first>
- src/lib/types.ts
</read_first>
<action>
In `src/lib/types.ts`, add `setup_completed: boolean` to the `Profile` interface. The field goes after `currency` and before `created_at`:
```typescript
export interface Profile {
id: string
display_name: string | null
locale: string
currency: string
setup_completed: boolean // <-- ADD THIS LINE
created_at: string
updated_at: string
}
```
No other changes to types.ts.
</action>
<verify>
<automated>grep -n "setup_completed: boolean" src/lib/types.ts</automated>
</verify>
<done>`src/lib/types.ts` contains `setup_completed: boolean` inside the Profile interface. `tsc --noEmit` passes with no new errors.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Migration SQL → Supabase DB | DDL runs with superuser privileges via Supabase CLI — only run locally or in controlled CI |
| Client → profiles RLS | Client can UPDATE own profile row including setup_completed — acceptable since it's a UX flag, not a security gate |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06-01 | Tampering | budgets table — duplicate row creation via concurrent requests | mitigate | UNIQUE constraint on (user_id, start_date) enforced at DB level; second INSERT returns PostgREST error 23505 |
| T-06-02 | Tampering | categories table — duplicate name creation | mitigate | UNIQUE constraint on (user_id, name) enforced at DB level; second INSERT returns PostgREST error 23505 |
| T-06-03 | Elevation of privilege | profiles.setup_completed — client sets own flag to false to force wizard re-display | accept | setup_completed is a UX routing flag only; no data is gated behind it; RLS allows own-row update |
</threat_model>
<verification>
After all 3 tasks complete:
```bash
# Confirm both migration files exist
ls supabase/migrations/006_uniqueness_constraints.sql supabase/migrations/007_setup_completed.sql
# Confirm TypeScript type updated
grep "setup_completed: boolean" src/lib/types.ts
# Confirm TypeScript compiles cleanly
npx tsc --noEmit
```
</verification>
<success_criteria>
- `supabase/migrations/006_uniqueness_constraints.sql` exists, contains BEGIN/COMMIT, two DISTINCT ON dedup DELETEs, two ADD CONSTRAINT statements
- `supabase/migrations/007_setup_completed.sql` exists, adds column with `boolean NOT NULL DEFAULT false`, backfills with UNION covering categories and template_items
- `src/lib/types.ts` Profile interface has `setup_completed: boolean`
- `tsc --noEmit` passes with no errors
</success_criteria>
<output>
After completion, create `.planning/phases/06-preset-data-first-run-detection-and-db-safety/06-01-SUMMARY.md`
</output>