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
2da48d43f8
commit
8e69a17587
@ -25,5 +25,31 @@ artifacts:
|
||||
status: draft
|
||||
path: spec.md
|
||||
tasks:
|
||||
status: pending
|
||||
status: draft
|
||||
path: tasks.md
|
||||
total: 8
|
||||
tasks:
|
||||
- id: task-001
|
||||
title: Domain layer - preference types, validation, defaults, and domain errors
|
||||
status: pending
|
||||
- id: task-002
|
||||
title: Port layer - PreferenceRepository interface and row type
|
||||
status: pending
|
||||
- id: task-003
|
||||
title: Service layer - PreferenceService with get, update, validation logic and unit tests
|
||||
status: pending
|
||||
- id: task-004
|
||||
title: Database migration - create user_preferences table
|
||||
status: pending
|
||||
- id: task-005
|
||||
title: PostgreSQL adapter - implement PreferenceRepository with sqlx
|
||||
status: pending
|
||||
- id: task-006
|
||||
title: Handler layer - GET and PUT preference handlers with error mapping and handler tests
|
||||
status: pending
|
||||
- id: task-007
|
||||
title: Routes, OpenAPI spec, and main.go wiring
|
||||
status: pending
|
||||
- id: task-008
|
||||
title: Cleanup - remove example scaffold files
|
||||
status: pending
|
||||
|
||||
127
.sdlc/features/user-preferences/tasks.md
Normal file
127
.sdlc/features/user-preferences/tasks.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Tasks: User Preferences API
|
||||
|
||||
## Task Order (dependency sequence)
|
||||
|
||||
### T1: Domain layer - preference types, validation, defaults, and domain errors
|
||||
- **Scope:** Create the core domain model: `PreferenceKey` constants, `PreferenceDefinition` registry with default values and per-key validators, `UserPreferences` aggregate, helper functions (`DefaultPreferences`, `ValidateKey`, `ValidateValue`, `MergeWithDefaults`, `SerializeForResponse`), and domain errors (`ErrUnknownPreferenceKey`, `ErrInvalidPreferenceValue`, `ErrInvalidUserID`). Also implement `ValidateUserID` (UUID format check).
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/domain/preference.go`
|
||||
- Modify `services/preferences-api/internal/domain/errors.go` (replace example errors)
|
||||
- Delete `services/preferences-api/internal/domain/example.go`
|
||||
- **Depends on:** None
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `PreferenceKey` constants defined for `theme`, `language`, `notifications_enabled`
|
||||
- [ ] `DefaultPreferences()` returns all 3 keys with correct defaults (`system`, `en`, `true`)
|
||||
- [ ] `ValidateKey()` rejects unknown keys, accepts known keys
|
||||
- [ ] `ValidateValue()` enforces: theme in {light, dark, system}, language matches BCP 47 regex, notifications_enabled in {true, false}
|
||||
- [ ] `MergeWithDefaults()` fills missing keys with defaults, preserves stored values
|
||||
- [ ] `SerializeForResponse()` converts `"true"`/`"false"` to boolean for `notifications_enabled`
|
||||
- [ ] Domain errors are sentinel errors with descriptive messages
|
||||
- [ ] `ValidateUserID()` accepts valid UUIDs, rejects non-UUID strings
|
||||
|
||||
### T2: Port layer - PreferenceRepository interface and row type
|
||||
- **Scope:** Define the `PreferenceRepository` port interface with `GetByUserID` and `Upsert` methods, plus the `PreferenceRow` data transfer type.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/port/preference.go`
|
||||
- Delete `services/preferences-api/internal/port/example.go`
|
||||
- **Depends on:** T1
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `PreferenceRepository` interface defined with `GetByUserID(ctx, userID) ([]PreferenceRow, error)` and `Upsert(ctx, userID, key, value) error`
|
||||
- [ ] `PreferenceRow` struct has fields: `UserID`, `Key`, `Value`, `CreatedAt`, `UpdatedAt`
|
||||
- [ ] Interface is minimal (no delete, no list-all-users)
|
||||
|
||||
### T3: Service layer - PreferenceService with get, update, validation logic and unit tests
|
||||
- **Scope:** Implement `PreferenceService` with `GetPreferences` and `UpdatePreferences` methods. `GetPreferences` fetches stored rows, merges with defaults, serializes for response. `UpdatePreferences` validates keys/values, upserts each, then returns full merged result. Write comprehensive unit tests with a mock repository.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/service/preference.go`
|
||||
- Create `services/preferences-api/internal/service/preference_test.go`
|
||||
- Delete `services/preferences-api/internal/service/example.go`
|
||||
- Delete `services/preferences-api/internal/service/example_test.go`
|
||||
- **Depends on:** T1, T2
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `GetPreferences` returns all defaults when no rows stored
|
||||
- [ ] `GetPreferences` merges stored values with defaults for missing keys
|
||||
- [ ] `GetPreferences` returns `ErrInvalidUserID` for non-UUID user_id
|
||||
- [ ] `UpdatePreferences` rejects unknown keys with `ErrUnknownPreferenceKey`
|
||||
- [ ] `UpdatePreferences` rejects invalid values with `ErrInvalidPreferenceValue`
|
||||
- [ ] `UpdatePreferences` calls `Upsert` for each provided key
|
||||
- [ ] `UpdatePreferences` returns full merged preferences after upsert
|
||||
- [ ] `UpdatePreferences` handles boolean input (converts `true`/`false` to string)
|
||||
- [ ] Unit tests cover: defaults-only, partial stored, full stored, unknown key, invalid value, invalid user_id
|
||||
- [ ] Tests use mock repository (no database dependency)
|
||||
|
||||
### T4: Database migration - create user_preferences table
|
||||
- **Scope:** Write the SQL migration to create the `user_preferences` table with composite primary key `(user_id, key)` and an index on `user_id`.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/migrations/001_create_user_preferences.sql`
|
||||
- **Depends on:** None
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Table `user_preferences` created with columns: `user_id` (UUID NOT NULL), `key` (VARCHAR(64) NOT NULL), `value` (TEXT NOT NULL), `created_at` (TIMESTAMPTZ DEFAULT NOW()), `updated_at` (TIMESTAMPTZ DEFAULT NOW())
|
||||
- [ ] Primary key is `(user_id, key)`
|
||||
- [ ] Index `idx_user_preferences_user_id` created on `user_id`
|
||||
- [ ] Migration is idempotent-safe (CREATE TABLE, not CREATE TABLE IF NOT EXISTS — rely on migration runner)
|
||||
|
||||
### T5: PostgreSQL adapter - implement PreferenceRepository with sqlx
|
||||
- **Scope:** Implement `PostgresPreferenceRepository` that satisfies the `PreferenceRepository` interface using sqlx queries. `GetByUserID` selects all rows for a user. `Upsert` uses `INSERT ... ON CONFLICT DO UPDATE`.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/adapter/postgres/preference.go`
|
||||
- Delete `services/preferences-api/internal/adapter/memory/example.go`
|
||||
- **Depends on:** T2, T4
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `PostgresPreferenceRepository` struct holds a `*sqlx.DB` (or pool from `pkg/database`)
|
||||
- [ ] `GetByUserID` executes `SELECT user_id, key, value, created_at, updated_at FROM user_preferences WHERE user_id = $1`
|
||||
- [ ] `Upsert` executes `INSERT INTO user_preferences ... ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`
|
||||
- [ ] Constructor `NewPreferenceRepository(db)` returns a new instance
|
||||
- [ ] Implements `port.PreferenceRepository` interface (compile-time check)
|
||||
|
||||
### T6: Handler layer - GET and PUT preference handlers with error mapping and handler tests
|
||||
- **Scope:** Implement `PreferenceHandler` with `GetPreferences` and `UpdatePreferences` HTTP handlers. GET extracts `user_id` from URL, calls service, maps errors. PUT also binds request body via `app.Bind`. Write handler tests using chi test router with a mock service or mock repository.
|
||||
- **Files:**
|
||||
- Create `services/preferences-api/internal/api/handlers/preference.go`
|
||||
- Create `services/preferences-api/internal/api/handlers/preference_test.go`
|
||||
- Delete `services/preferences-api/internal/api/handlers/example.go`
|
||||
- Delete `services/preferences-api/internal/api/handlers/example_test.go`
|
||||
- **Depends on:** T3
|
||||
- **Acceptance criteria:**
|
||||
- [ ] GET handler extracts `user_id` via `chi.URLParam(r, "user_id")`
|
||||
- [ ] GET handler returns 200 with `{data, meta}` envelope containing `user_id` and `preferences`
|
||||
- [ ] GET handler returns 400 for invalid `user_id`
|
||||
- [ ] PUT handler binds request body via `app.Bind()`
|
||||
- [ ] PUT handler returns 200 with full merged preferences after update
|
||||
- [ ] PUT handler returns 400 for unknown key, invalid value, or invalid user_id
|
||||
- [ ] Both handlers return `error` wrapped with `app.Wrap()`
|
||||
- [ ] Handler tests cover: successful GET, GET with defaults, successful PUT, PUT unknown key, PUT invalid value, invalid user_id for both endpoints
|
||||
- [ ] Response shape matches spec: `{"data": {"user_id": "...", "preferences": {...}}, "meta": {...}}`
|
||||
|
||||
### T7: Routes, OpenAPI spec, and main.go wiring
|
||||
- **Scope:** Update `routes.go` to register new preference routes (replacing example routes). Update `spec.go` to document the new endpoints with schemas. Update `main.go` to wire database connection, run migrations, create PostgreSQL adapter, and inject into the service.
|
||||
- **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:** T5, T6
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Routes: `GET /api/preferences-api/preferences/{user_id}` registered
|
||||
- [ ] Routes: `PUT /api/preferences-api/preferences/{user_id}` registered
|
||||
- [ ] Routes: Old `/examples` routes removed
|
||||
- [ ] Routes: Preference routes in auth-protectable group (conditional on `AUTH_ENABLED`)
|
||||
- [ ] Routes: Health endpoint unchanged
|
||||
- [ ] OpenAPI spec documents both endpoints with request/response schemas
|
||||
- [ ] OpenAPI spec includes `UserPreferences` and `UpdatePreferencesRequest` schemas
|
||||
- [ ] `main.go` connects to database via `pkg/database`
|
||||
- [ ] `main.go` runs migrations on startup
|
||||
- [ ] `main.go` creates `PostgresPreferenceRepository` and injects into service
|
||||
- [ ] `main.go` adds DB pool shutdown hook
|
||||
|
||||
### T8: Cleanup - remove example scaffold files
|
||||
- **Scope:** Delete all remaining example scaffold files that were not already replaced in previous tasks. Verify no references to `example` or `Example` remain in the codebase under `services/preferences-api/`. Ensure the service compiles and all tests pass.
|
||||
- **Files:**
|
||||
- Delete any remaining `example*.go` files
|
||||
- Delete `services/preferences-api/internal/adapter/memory/` directory (if not already removed)
|
||||
- **Depends on:** T7
|
||||
- **Acceptance criteria:**
|
||||
- [ ] No files named `example*.go` exist under `services/preferences-api/`
|
||||
- [ ] No references to `Example`, `example`, or `memory.New` in `services/preferences-api/`
|
||||
- [ ] `go build ./...` succeeds from `services/preferences-api/`
|
||||
- [ ] `go test ./...` passes from `services/preferences-api/`
|
||||
- [ ] `go vet ./...` passes from `services/preferences-api/`
|
||||
Loading…
Reference in New Issue
Block a user