slack5-1770529463/.sdlc/features/user-preferences/tasks.md
rdev-worker 8e69a17587
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /breakdown-feature user-preferences
2026-02-08 05:58:54 +00:00

9.1 KiB

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/