build: /breakdown-feature user-preferences
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
96af8d3c07
commit
e0b6dc03eb
@ -25,5 +25,34 @@ artifacts:
|
||||
status: draft
|
||||
path: spec.md
|
||||
tasks:
|
||||
status: pending
|
||||
status: draft
|
||||
path: tasks.md
|
||||
total: 9
|
||||
tasks:
|
||||
- id: task-001
|
||||
title: Domain layer - preferences entity, validation, and errors
|
||||
status: pending
|
||||
- id: task-002
|
||||
title: Port layer - PreferencesRepository interface
|
||||
status: pending
|
||||
- id: task-003
|
||||
title: Database migration and PostgreSQL adapter
|
||||
status: pending
|
||||
- id: task-004
|
||||
title: Service layer - PreferencesService with Get and Update
|
||||
status: pending
|
||||
- id: task-005
|
||||
title: Service layer unit tests
|
||||
status: pending
|
||||
- id: task-006
|
||||
title: HTTP handlers - Get and Update preferences
|
||||
status: pending
|
||||
- id: task-007
|
||||
title: Handler integration tests
|
||||
status: pending
|
||||
- id: task-008
|
||||
title: Routes, OpenAPI spec, and main.go wiring
|
||||
status: pending
|
||||
- id: task-009
|
||||
title: Remove Example scaffold code
|
||||
status: pending
|
||||
|
||||
145
.sdlc/features/user-preferences/tasks.md
Normal file
145
.sdlc/features/user-preferences/tasks.md
Normal file
@ -0,0 +1,145 @@
|
||||
# Tasks: User Preferences API
|
||||
|
||||
## Task Order (dependency sequence)
|
||||
|
||||
### T1: Domain layer - preferences entity, validation, and errors
|
||||
- **Scope:** Create `UserPreferences` struct, preference key/value validation functions, and domain error definitions. Implement `ValidatePreferences`, `ValidatePreferenceKey`, and `ValidatePreferenceValue` with the closed key set (`theme`, `language`, `notifications_enabled`) and per-key value rules.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/domain/preferences.go`
|
||||
- Replace `services/preferences-api/internal/domain/errors.go`
|
||||
- **Depends on:** None
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `UserPreferences` struct has `UserID`, `Preferences`, `CreatedAt`, `UpdatedAt` fields
|
||||
- [ ] `ValidatePreferences` rejects unknown keys with `ErrInvalidPreferenceKey`
|
||||
- [ ] `ValidatePreferenceValue` validates `theme` accepts only `"light"` and `"dark"`
|
||||
- [ ] `ValidatePreferenceValue` validates `language` matches `^[a-z]{2}$`
|
||||
- [ ] `ValidatePreferenceValue` validates `notifications_enabled` is a boolean
|
||||
- [ ] Error messages include the offending key/value for debuggability
|
||||
- [ ] `ErrInvalidPreferenceKey` and `ErrInvalidPreferenceValue` sentinel errors defined
|
||||
|
||||
### T2: Port layer - PreferencesRepository interface
|
||||
- **Scope:** Define the `PreferencesRepository` interface with `Get` and `Upsert` methods. This is a thin interface file replacing the Example port.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/port/preferences.go`
|
||||
- **Depends on:** T1
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `PreferencesRepository` interface defined with `Get(ctx, userID) (*UserPreferences, error)` and `Upsert(ctx, userID, prefs) (*UserPreferences, error)`
|
||||
- [ ] Uses domain types from `internal/domain`
|
||||
- [ ] Context parameter on all methods for cancellation/timeout support
|
||||
|
||||
### T3: Database migration and PostgreSQL adapter
|
||||
- **Scope:** Create the SQL migration for the `user_preferences` table and implement the PostgreSQL repository adapter that satisfies the `PreferencesRepository` port.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/migrations/001_create_user_preferences.sql`
|
||||
- Create `services/preferences-api/internal/adapter/postgres/preferences.go`
|
||||
- **Depends on:** T2
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Migration creates `user_preferences` table with `user_id UUID PRIMARY KEY`, `preferences JSONB NOT NULL DEFAULT '{}'`, `created_at TIMESTAMPTZ`, `updated_at TIMESTAMPTZ`
|
||||
- [ ] Migration uses `IF NOT EXISTS` for idempotency
|
||||
- [ ] `Get` returns `nil` (not error) when no row found, so handler returns empty preferences
|
||||
- [ ] `Upsert` uses `INSERT ... ON CONFLICT (user_id) DO UPDATE` with JSONB merge (`preferences || $2`)
|
||||
- [ ] `Upsert` returns the full merged row after upsert
|
||||
- [ ] All queries use parameterized statements (no SQL injection)
|
||||
- [ ] Repository struct accepts `*sql.DB` or pool via constructor
|
||||
|
||||
### T4: Service layer - PreferencesService with Get and Update
|
||||
- **Scope:** Implement `PreferencesService` with `Get` and `Update` methods. Get retrieves preferences (returning empty object for new users). Update validates input via domain layer, then delegates to repository upsert.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/service/preferences.go`
|
||||
- **Depends on:** T1, T2
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `Get(ctx, userID)` returns `*UserPreferences` — empty preferences struct for new users (not nil, not error)
|
||||
- [ ] `Update(ctx, userID, prefs)` calls `domain.ValidatePreferences` before persisting
|
||||
- [ ] `Update` returns the full merged preferences after upsert
|
||||
- [ ] Validation errors from domain layer propagate to caller unchanged
|
||||
- [ ] Repository errors propagate to caller unchanged (will become 500s)
|
||||
- [ ] Structured logging with user_id context on operations
|
||||
|
||||
### T5: Service layer unit tests
|
||||
- **Scope:** Write table-driven unit tests for `PreferencesService` with a mock repository. Cover valid operations, validation failures, and repository errors.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/service/preferences_test.go`
|
||||
- **Depends on:** T4
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Tests use mock repository implementing `port.PreferencesRepository`
|
||||
- [ ] Test `Get` for existing user (returns preferences) and new user (returns empty)
|
||||
- [ ] Test `Update` with valid preferences succeeds
|
||||
- [ ] Test `Update` with unknown key returns `ErrInvalidPreferenceKey`
|
||||
- [ ] Test `Update` with invalid theme value returns `ErrInvalidPreferenceValue`
|
||||
- [ ] Test `Update` with invalid language format returns `ErrInvalidPreferenceValue`
|
||||
- [ ] Test `Update` with non-boolean `notifications_enabled` returns `ErrInvalidPreferenceValue`
|
||||
- [ ] Tests use `logging.Nop()` for no-op logger
|
||||
- [ ] All tests pass with `go test -v ./internal/service/...`
|
||||
|
||||
### T6: HTTP handlers - Get and Update preferences
|
||||
- **Scope:** Implement `Preferences` handler struct with `Get` and `Update` methods. Include UUID validation on path params, ownership check against JWT user, request binding, domain error mapping, and response envelope formatting.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/api/handlers/preferences.go`
|
||||
- **Depends on:** T4
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `Get` extracts `user_id` from `chi.URLParam`, validates UUID format
|
||||
- [ ] `Get` checks ownership via `auth.MustGetUser(ctx)` comparison
|
||||
- [ ] `Get` returns 200 with `{data, meta}` envelope via `httpresponse.OK`
|
||||
- [ ] `Update` binds request with `app.BindAndValidate`
|
||||
- [ ] `Update` checks ownership before calling service
|
||||
- [ ] `Update` returns 200 with full merged preferences
|
||||
- [ ] Invalid UUID returns 400 via `httperror.BadRequest`
|
||||
- [ ] Ownership mismatch returns 403 via `httperror.Forbidden`
|
||||
- [ ] Domain errors mapped: `ErrInvalidPreferenceKey` → 400, `ErrInvalidPreferenceValue` → 400
|
||||
- [ ] Unhandled errors bubble up as 500 via `app.Wrap`
|
||||
- [ ] `PreferencesResponse` DTO with `user_id`, `preferences`, `updated_at` (nullable)
|
||||
|
||||
### T7: Handler integration tests
|
||||
- **Scope:** Write HTTP-level integration tests for the preferences handlers using `httptest` and chi router. Mock the repository at the port layer. Test all status codes, response shapes, and error cases.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/api/handlers/preferences_test.go`
|
||||
- **Depends on:** T6
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Test `GET` returns 200 with preferences for existing user
|
||||
- [ ] Test `GET` returns 200 with empty preferences for new user
|
||||
- [ ] Test `GET` returns 400 for invalid UUID
|
||||
- [ ] Test `PUT` returns 200 with merged preferences on success
|
||||
- [ ] Test `PUT` returns 400 for unknown preference keys
|
||||
- [ ] Test `PUT` returns 400 for invalid preference values
|
||||
- [ ] Test `PUT` returns 400 for missing `preferences` field
|
||||
- [ ] All responses use `{data, meta}` envelope structure
|
||||
- [ ] Tests use table-driven pattern with subtests
|
||||
- [ ] All tests pass with `go test -v ./internal/api/handlers/...`
|
||||
|
||||
### T8: Routes, OpenAPI spec, and main.go wiring
|
||||
- **Scope:** Update route registration to mount preferences endpoints under auth middleware, update OpenAPI spec with the two new endpoints, and wire the PostgreSQL adapter + service in `main.go` (DB connect, migrations, shutdown hook).
|
||||
- **Files:**
|
||||
- Modify `services/preferences-api/internal/api/routes.go`
|
||||
- Modify `services/preferences-api/internal/api/spec.go`
|
||||
- Modify `services/preferences-api/cmd/server/main.go`
|
||||
- **Depends on:** T3, T6
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Routes: `GET /api/preferences-api/preferences/{user_id}` and `PUT /api/preferences-api/preferences/{user_id}` registered
|
||||
- [ ] Both preference routes wrapped in `auth.Middleware()` group
|
||||
- [ ] URL parameters use `{user_id}` brace syntax (not colon)
|
||||
- [ ] Handlers wrapped with `app.Wrap()`
|
||||
- [ ] OpenAPI spec defines both endpoints with request/response schemas, security requirements, and error codes
|
||||
- [ ] `main.go` connects to PostgreSQL via `database.MustConnect()` with `DatabaseConfig`
|
||||
- [ ] `main.go` runs migrations via `database.MustRunMigrations()`
|
||||
- [ ] `main.go` creates `postgres.PreferencesRepository` and `PreferencesService`
|
||||
- [ ] `main.go` registers DB pool shutdown hook
|
||||
- [ ] Health endpoint remains functional
|
||||
|
||||
### T9: Remove Example scaffold code
|
||||
- **Scope:** Delete all Example scaffold files that have been replaced by preferences code. Ensure no references to Example types remain in routes, spec, main, or tests.
|
||||
- **Files:**
|
||||
- Delete `services/preferences-api/internal/domain/example.go`
|
||||
- Delete `services/preferences-api/internal/port/example.go`
|
||||
- Delete `services/preferences-api/internal/adapter/memory/example.go` (and `memory/` directory)
|
||||
- Delete `services/preferences-api/internal/service/example.go`
|
||||
- Delete `services/preferences-api/internal/service/example_test.go`
|
||||
- Delete `services/preferences-api/internal/api/handlers/example.go`
|
||||
- Delete `services/preferences-api/internal/api/handlers/example_test.go`
|
||||
- **Depends on:** T8
|
||||
- **Acceptance criteria:**
|
||||
- [ ] All Example files deleted
|
||||
- [ ] No remaining imports of Example types in any file
|
||||
- [ ] No remaining references to `/examples` routes
|
||||
- [ ] `go build ./...` succeeds with no compilation errors
|
||||
- [ ] `go test ./...` passes with no failures
|
||||
- [ ] Health endpoint still works
|
||||
Loading…
Reference in New Issue
Block a user