build: /breakdown-feature user-preferences
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
rdev-worker 2026-02-08 05:58:54 +00:00
parent 2da48d43f8
commit 8e69a17587
2 changed files with 154 additions and 1 deletions

View File

@ -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

View 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/`