slack5-1770574304/.sdlc/features/user-preferences/tasks.md
rdev-worker 493f96a9fd
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /breakdown-feature user-preferences
2026-02-08 18:24:58 +00:00

9.7 KiB

Tasks: User Preferences API

Task Order (dependency sequence)

T1: Domain layer - preference types, validation, defaults, and errors

  • ID: task-001
  • Scope: Create the pure domain model for user preferences. Define UserPreferences struct with typed fields (Theme, Language, NotificationPreferences), enum types (Theme, DigestFrequency) with constants, a DefaultPreferences() factory, a Validate() method with domain-level validation, and domain error sentinels (ErrInvalidTheme, ErrInvalidLanguage, ErrInvalidDigest).
  • Files:
    • services/preferences-api/internal/domain/preferences.go (create)
    • services/preferences-api/internal/domain/errors.go (replace contents)
  • Depends on: None
  • Acceptance criteria:
    • UserPreferences struct has fields: UserID, Theme, Language, Notifications, UpdatedAt
    • Theme type with constants: ThemeLight, ThemeDark, ThemeSystem
    • DigestFrequency type with constants: DigestNone, DigestDaily, DigestWeekly
    • NotificationPreferences struct with Email (bool), Push (bool), Digest (DigestFrequency)
    • DefaultPreferences(userID) returns correct defaults (theme=system, language=en, email=true, push=true, digest=weekly)
    • Validate() returns ErrInvalidTheme for invalid theme values
    • Validate() returns ErrInvalidLanguage for invalid language values (only en, fr, es, de, ja)
    • Validate() returns ErrInvalidDigest for invalid digest values
    • Domain errors defined as package-level sentinel errors
    • No external dependencies (pure Go, no framework imports)

T2: Port layer - PreferenceRepository interface

  • ID: task-002
  • Scope: Define the PreferenceRepository port interface with Get(ctx, userID) and Upsert(ctx, prefs) methods. This is the hexagonal architecture boundary between the service layer and storage adapters.
  • Files:
    • services/preferences-api/internal/port/preferences.go (create)
  • Depends on: T1
  • Acceptance criteria:
    • PreferenceRepository interface defined with Get(ctx context.Context, userID string) (*domain.UserPreferences, error) method
    • PreferenceRepository interface includes Upsert(ctx context.Context, prefs *domain.UserPreferences) error method
    • Get documents that it returns nil, nil when no preferences exist (service applies defaults)
    • Interface uses domain types only (no adapter-specific types)

T3: Adapter layer - in-memory PreferenceRepository for tests

  • ID: task-003
  • Scope: Implement a thread-safe in-memory PreferenceRepository adapter for use in unit and handler tests. Uses a map[string]*domain.UserPreferences with sync.RWMutex.
  • Files:
    • services/preferences-api/internal/adapter/memory/preferences.go (create)
  • Depends on: T1, T2
  • Acceptance criteria:
    • Implements port.PreferenceRepository interface (compile-time verification)
    • Get() returns nil, nil for non-existent user (not an error)
    • Get() returns a defensive copy (mutation-safe)
    • Upsert() stores preferences, overwriting any existing entry
    • Thread-safe via sync.RWMutex
    • Constructor NewPreferenceRepository() initializes empty map

T4: Adapter layer - PostgreSQL PreferenceRepository with schema creation

  • ID: task-004
  • Scope: Implement the PostgreSQL adapter for PreferenceRepository. Includes EnsureSchema() for idempotent table creation via CREATE TABLE IF NOT EXISTS, Get() with single-row PK lookup, and Upsert() with INSERT ... ON CONFLICT UPDATE. Uses database/sql with parameterized queries.
  • Files:
    • services/preferences-api/internal/adapter/postgres/preferences.go (create)
  • Depends on: T1, T2
  • Acceptance criteria:
    • Implements port.PreferenceRepository interface (compile-time verification)
    • Constructor accepts *sql.DB and calls EnsureSchema() to create the user_preferences table
    • EnsureSchema() uses CREATE TABLE IF NOT EXISTS with correct column types (TEXT PK, TEXT, BOOLEAN, TIMESTAMPTZ)
    • Get() returns nil, nil when sql.ErrNoRows (not an error, service applies defaults)
    • Get() maps flat columns back to domain.UserPreferences struct
    • Upsert() uses INSERT ... ON CONFLICT (user_id) DO UPDATE SET ... for atomic upsert
    • All queries use parameterized placeholders ($1, $2, ...) - no SQL interpolation
    • updated_at set to NOW() on upsert

T5: Service layer - PreferenceService with business logic and tests

  • ID: task-005
  • Scope: Implement PreferenceService with GetPreferences(ctx, userID) and UpdatePreferences(ctx, userID, prefs). GET applies defaults when repository returns nil. UPDATE validates via domain Validate() before persisting. Write comprehensive unit tests using the in-memory adapter.
  • Files:
    • services/preferences-api/internal/service/preferences.go (create)
    • services/preferences-api/internal/service/preferences_test.go (create)
  • Depends on: T1, T2, T3
  • Acceptance criteria:
    • GetPreferences() returns default preferences when repository returns nil
    • GetPreferences() returns stored preferences when they exist
    • UpdatePreferences() calls Validate() and returns domain errors on invalid input
    • UpdatePreferences() sets UserID and UpdatedAt before upserting
    • UpdatePreferences() delegates to repository Upsert() after validation
    • Constructor accepts port.PreferenceRepository and *logging.Logger
    • Unit tests cover: get defaults, get existing, update valid, update invalid theme/language/digest
    • Tests use logging.Nop() for no-op logger

T6: Handler layer - GET and PUT preference handlers with tests

  • ID: task-006
  • Scope: Implement PreferenceHandler with Get(w, r) error and Update(w, r) error. GET extracts {user_id} from URL, checks authorization (self-access or admin read), delegates to service, returns response envelope. PUT extracts user_id, checks self-only auth, binds/validates request body strictly, delegates to service. Write handler tests using httptest with the in-memory adapter.
  • Files:
    • services/preferences-api/internal/api/handlers/preferences.go (create)
    • services/preferences-api/internal/api/handlers/preferences_test.go (create)
  • Depends on: T1, T2, T3, T5
  • Acceptance criteria:
    • Get() extracts user_id from chi URL param using brace syntax
    • Get() returns 403 Forbidden when JWT user ID != URL user_id (and not admin)
    • Get() allows admin role to read any user's preferences
    • Get() returns 200 with {data, meta} envelope via httpresponse.OK()
    • Update() returns 403 for any non-self access (even admin)
    • Update() uses strict binding to reject unknown JSON fields with 400
    • Update() returns 400 with validation errors for invalid values
    • Update() returns 200 with updated preferences on success
    • Request DTOs use struct validation tags (oneof=light dark system, etc.)
    • Response DTO maps domain types to JSON representation
    • Domain errors mapped to appropriate HTTP errors (400, 403)
    • Tests cover: self-access GET, admin GET, forbidden GET, self PUT, forbidden PUT, invalid body, unknown fields, valid update

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

  • ID: task-007
  • Scope: Update routes.go to register preference endpoints (GET and PUT under auth middleware). Update spec.go to document preference endpoints with schemas, examples, and error responses. Update main.go to wire PostgreSQL adapter (via DatabaseConfig), create PreferenceService, and pass to route registration.
  • Files:
    • 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/internal/config/config.go (modify if needed)
  • Depends on: T4, T5, T6
  • Acceptance criteria:
    • GET /api/preferences-api/preferences/{user_id} registered with auth middleware
    • PUT /api/preferences-api/preferences/{user_id} registered with auth middleware
    • Health endpoint preserved at /api/preferences-api/health
    • OpenAPI spec documents both preference endpoints with request/response schemas
    • OpenAPI spec includes UserPreferences schema, UpdatePreferencesRequest schema
    • OpenAPI spec includes error responses (400, 401, 403)
    • main.go opens PostgreSQL connection via sql.Open with DatabaseConfig.DSN()
    • main.go creates PostgreSQL adapter, PreferenceService, passes to RegisterRoutes
    • Docs mounted via application.EnableDocs(spec)

T8: Remove example scaffold and verify clean build

  • ID: task-008
  • Scope: Delete all example scaffold files that have been replaced by preference equivalents. Verify the service compiles, all tests pass, and no dead code remains.
  • Files:
    • services/preferences-api/internal/domain/example.go (delete)
    • services/preferences-api/internal/port/example.go (delete)
    • services/preferences-api/internal/adapter/memory/example.go (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 (delete)
  • Depends on: T7
  • Acceptance criteria:
    • All example files deleted (7 files listed above)
    • No remaining imports of example types in any file
    • go build ./... succeeds with zero errors
    • go test ./... passes all tests
    • go vet ./... reports no issues
    • No dead code or unused imports remain