slack5-1770606136/.sdlc/features/user-preferences/tasks.md
rdev-worker c675a6ca7e
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /breakdown-feature user-preferences
2026-02-09 03:18:13 +00:00

8.5 KiB

Tasks: User Preferences API

Task Order (dependency sequence)

T1: Domain layer - UserPreferences model and domain errors

  • Scope: Create the UserPreferences domain model and preference-specific domain errors. The model uses map[string]any for flexible key-value storage. Add ErrInvalidUserID and ErrInvalidPreferenceValue sentinel errors.
  • Files:
    • Create services/preferences-api/internal/domain/preference.go
    • Modify services/preferences-api/internal/domain/errors.go (replace example errors with preference errors)
  • Depends on: None
  • Acceptance criteria:
    • UserPreferences struct exists with fields: UserID string, Preferences map[string]any, CreatedAt time.Time, UpdatedAt time.Time
    • ErrInvalidUserID sentinel error defined
    • ErrInvalidPreferenceValue sentinel error defined
    • Example domain errors removed
    • Package compiles without errors

T2: Port layer - PreferenceRepository interface

  • Scope: Define the PreferenceRepository interface with Get and Upsert methods. Get returns nil, nil for non-existent users (not an error). Upsert handles both create and update atomically.
  • Files:
    • Create services/preferences-api/internal/port/preference.go
  • Depends on: T1
  • Acceptance criteria:
    • PreferenceRepository interface defined with Get(ctx context.Context, userID string) (*domain.UserPreferences, error) method
    • PreferenceRepository interface has Upsert(ctx context.Context, prefs *domain.UserPreferences) error method
    • Method signatures document expected nil behavior for Get (nil, nil when no row exists)
    • Package compiles without errors

T3: Service layer - PreferenceService with validation logic and tests

  • Scope: Implement PreferenceService with Get and Upsert methods. The Upsert method validates known preference keys (theme, language, notifications_enabled), merges incoming preferences with existing ones, and delegates to the repository. Includes a custom ValidationError type that wraps domain.ErrInvalidPreferenceValue and carries per-field error details. 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
  • Depends on: T1, T2
  • Acceptance criteria:
    • PreferenceService struct with constructor NewPreferenceService(repo, logger)
    • Get method delegates to repository and returns result
    • Upsert method validates theme against ["light", "dark", "system"]
    • Upsert method validates language as a valid BCP-47 tag
    • Upsert method validates notifications_enabled as boolean
    • Unknown preference keys accepted without validation
    • Multiple validation errors collected and returned together in ValidationError
    • ValidationError type implements error and Unwrap() returning domain.ErrInvalidPreferenceValue
    • ValidationError.Details is map[string]string with per-field messages
    • Merge strategy: incoming keys overwrite existing, unmentioned keys preserved
    • Tests cover: valid preferences, invalid theme, invalid language, invalid notifications_enabled, multiple errors, unknown keys accepted, merge behavior
    • All tests pass

T4: Database migration and PostgreSQL adapter

  • Scope: Create the SQL migration for the preferences table and implement the PostgreSQL adapter for PreferenceRepository. Uses sqlx with parameterized queries. Get returns nil, nil on sql.ErrNoRows. Upsert uses ON CONFLICT for atomic create-or-update.
  • Files:
    • Create services/preferences-api/migrations/001_create_preferences.sql
    • Create services/preferences-api/internal/adapter/postgres/preference.go
  • Depends on: T1, T2
  • Acceptance criteria:
    • Migration creates preferences table with columns: user_id UUID PRIMARY KEY, preferences JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ
    • Migration creates index idx_preferences_updated_at on updated_at
    • Migration uses CREATE TABLE IF NOT EXISTS for idempotency
    • PostgreSQL adapter implements PreferenceRepository interface (compile-time check)
    • Get uses parameterized query and returns nil, nil for sql.ErrNoRows
    • Upsert uses INSERT ... ON CONFLICT ... DO UPDATE with parameterized values
    • JSONB marshaling/unmarshaling handled correctly for map[string]any
    • Package compiles without errors

T5: HTTP handlers - GET and PUT preference endpoints with tests

  • Scope: Implement preference handlers for GET and PUT. GET extracts user_id from URL, validates UUID format, calls service, and returns response in {data, meta} envelope. PUT additionally binds the request body and maps validation errors to structured HTTP error responses. Write handler tests using httptest with a mock service/repository.
  • Files:
    • Create services/preferences-api/internal/api/handlers/preference.go
    • Create services/preferences-api/internal/api/handlers/preference_test.go
  • Depends on: T1, T2, T3
  • Acceptance criteria:
    • Preference handler struct with constructor NewPreference(svc, logger)
    • Get handler extracts user_id via chi.URLParam, validates UUID, calls service, returns httpresponse.OK
    • Get handler returns 200 with empty preferences {} when service returns nil
    • Get handler returns 400 for invalid UUID format
    • Upsert handler binds request body with app.Bind(), validates preferences field is not nil
    • Upsert handler calls service and returns httpresponse.OK with updated preferences
    • Upsert handler maps ValidationError to httperror with details
    • PreferenceResponse struct has json tags for user_id, preferences, updated_at
    • mapDomainError function handles ErrInvalidPreferenceValue with details
    • Tests cover: GET success, GET empty preferences, GET invalid UUID, PUT success, PUT validation error, PUT missing body
    • All tests pass

T6: Routes, OpenAPI spec, and main.go wiring

  • Scope: Update routes to register preference endpoints (replacing example routes). Update OpenAPI spec to document preference schemas and endpoints (removing example schemas). Update main.go to wire PostgreSQL connection, run migrations, create postgres adapter, and inject into the service. Add golang.org/x/text/language dependency for BCP-47 validation.
  • 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
    • Modify services/preferences-api/go.mod (add golang.org/x/text dependency)
  • Depends on: T3, T4, T5
  • Acceptance criteria:
    • Routes register GET /preferences/{user_id} and PUT /preferences/{user_id} under /api/preferences-api
    • Example routes removed
    • Both routes wrapped with app.Wrap()
    • OpenAPI spec defines UserPreferences schema and UpdatePreferencesRequest schema
    • OpenAPI spec documents GET and PUT endpoints with parameters, request bodies, and response schemas
    • Example schemas removed from spec
    • main.go connects to PostgreSQL via database.MustConnect
    • main.go runs migrations via database.MustRunMigrations
    • main.go creates postgres.NewPreferenceRepository and service.NewPreferenceService
    • main.go defers pool.Close()
    • golang.org/x/text/language dependency added
    • Service compiles and starts successfully

T7: Cleanup - Remove example scaffolding files

  • Scope: Delete all example-related files that have been replaced by preference implementations. This is done last to ensure nothing breaks.
  • Files:
    • Delete services/preferences-api/internal/domain/example.go
    • Delete services/preferences-api/internal/service/example.go
    • Delete services/preferences-api/internal/service/example_test.go
    • Delete services/preferences-api/internal/port/example.go
    • Delete services/preferences-api/internal/adapter/memory/example.go
    • Delete services/preferences-api/internal/api/handlers/example.go
    • Delete services/preferences-api/internal/api/handlers/example_test.go
  • Depends on: T6
  • Acceptance criteria:
    • All example files deleted
    • No imports reference example types
    • go test ./... passes with no compilation errors
    • Service compiles cleanly