slack5-1770544098/.sdlc/features/user-preferences/tasks.md
rdev-worker e0b6dc03eb
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /breakdown-feature user-preferences
2026-02-08 10:02:29 +00:00

9.1 KiB

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