slate-v3-1770514618/.sdlc/features/user-preferences/tasks.md
rdev-worker 86e2ae36ec
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /breakdown-feature user-preferences
2026-02-08 01:49:39 +00:00

9.0 KiB

Tasks: User Preferences API

Task Order (dependency sequence)

T1: Remove example scaffold (task-001)

  • Scope: Delete all example entity files across every layer (domain, port, adapter, service, handlers, tests). This clears the path for preferences code without merge conflicts.
  • Files:
    • Delete internal/domain/example.go
    • Delete internal/domain/errors.go
    • Delete internal/port/example.go
    • Delete internal/adapter/memory/example.go
    • Delete internal/service/example.go
    • Delete internal/service/example_test.go
    • Delete internal/api/handlers/example.go
    • Delete internal/api/handlers/example_test.go
  • Depends on: None
  • Acceptance criteria:
    • All 8 example files are deleted
    • No references to Example, ExampleID, ExampleRepository, or ExampleService remain in the codebase (routes.go, spec.go, and main.go will be updated in later tasks)
    • Service still compiles with routes.go/spec.go/main.go temporarily stubbed or commented

T2: Domain layer — preferences entity, validation, merge logic (task-002)

  • Scope: Create the core domain types (Preferences, NotificationSettings, PreferencesUpdate, NotificationSettingsUpdate, UserID), validation logic (Validate()), merge logic (MergeFrom()), default factory (NewDefaultPreferences()), and domain errors.
  • Files:
    • Create internal/domain/preferences.go
    • Create internal/domain/errors.go
  • Depends on: T1
  • Acceptance criteria:
    • UserID type with String() and IsZero() methods
    • Preferences struct with UserID, Theme, Language, Notifications, UpdatedAt fields
    • NotificationSettings struct with Email, Push, Digest fields
    • PreferencesUpdate struct with pointer fields (*string, *bool) to distinguish provided vs. absent
    • NotificationSettingsUpdate struct with pointer fields
    • NewDefaultPreferences(userID) returns preferences with all defaults: theme=system, language=en, email=true, push=true, digest=weekly
    • Validate() rejects invalid theme (not in light/dark/system), invalid language (not matching ^[a-z]{2}$), invalid digest (not in daily/weekly/never)
    • MergeFrom() only overwrites fields where update pointer is non-nil; notifications sub-fields merged individually
    • Domain errors defined: ErrPreferencesNotFound, ErrInvalidTheme, ErrInvalidLanguage, ErrInvalidDigest, ErrInvalidPreferences
    • Unit tests cover validation (valid + each invalid case), merge (partial updates, full updates, notifications sub-field merge), and defaults

T3: Port interface — PreferencesRepository (task-003)

  • Scope: Define the PreferencesRepository interface in the port layer with Get and Upsert methods.
  • Files:
    • Create internal/port/preferences.go
  • Depends on: T2 (uses domain types)
  • Acceptance criteria:
    • PreferencesRepository interface with Get(ctx context.Context, userID domain.UserID) (*domain.Preferences, error)
    • PreferencesRepository interface with Upsert(ctx context.Context, userID domain.UserID, prefs *domain.Preferences) error
    • Interface is in port package, imports only domain and standard library

T4: In-memory adapter — thread-safe map implementation (task-004)

  • Scope: Implement PreferencesRepository using a sync.RWMutex-protected map[domain.UserID]*domain.Preferences. Returns copies to prevent aliasing.
  • Files:
    • Create internal/adapter/memory/preferences.go
  • Depends on: T3 (implements port interface)
  • Acceptance criteria:
    • NewPreferencesRepository() constructor returns port.PreferencesRepository
    • Get() returns a copy of stored preferences, or domain.ErrPreferencesNotFound if absent
    • Upsert() stores a copy of the provided preferences (insert or replace)
    • Concurrent reads are safe (RWMutex read lock)
    • Writes are serialized (RWMutex write lock)
    • Unit tests cover: get existing, get missing (returns error), upsert new, upsert existing (overwrites)

T5: Service layer — PreferencesService with Get and Upsert (task-005)

  • Scope: Implement PreferencesService with Get (delegates to repo) and Upsert (fetch-or-create-defaults → merge → validate → persist) methods.
  • Files:
    • Create internal/service/preferences.go
    • Create internal/service/preferences_test.go
  • Depends on: T3 (uses port interface), T4 (test with in-memory adapter or mock)
  • Acceptance criteria:
    • NewPreferencesService(repo port.PreferencesRepository, logger *slog.Logger) constructor
    • Get(ctx, userID) returns preferences or propagates ErrPreferencesNotFound
    • Upsert(ctx, userID, update) fetches existing or creates defaults if not found
    • Upsert applies MergeFrom() then Validate() then repo.Upsert()
    • Upsert sets UpdatedAt to current time before persisting
    • Returns validation errors from domain layer unchanged (handler maps them)
    • Tests use mock repository implementing port.PreferencesRepository
    • Tests cover: get success, get not-found, upsert creates new with defaults + merge, upsert updates existing with merge, upsert rejects invalid values (validation error propagated)

T6: HTTP handlers — GET and PUT with auth, validation, error mapping (task-006)

  • Scope: Implement PreferencesHandler with Get and Update methods. Includes UUID validation of user_id path param, authorization check (own-user or admin), request binding with strict unknown-field rejection, domain-to-HTTP error mapping, and response envelope formatting.
  • Files:
    • Create internal/api/handlers/preferences.go
    • Create internal/api/handlers/preferences_test.go
  • Depends on: T5 (uses service layer)
  • Acceptance criteria:
    • NewPreferencesHandler(svc *service.PreferencesService) constructor
    • Get(w, r) error — extracts user_id from chi URL param, validates UUID format, checks authorization, calls service, returns httpresponse.OK with envelope
    • Update(w, r) error — extracts user_id, validates UUID, checks authorization, binds request body with app.BindAndValidate, rejects unknown top-level preference keys, calls service Upsert, returns httpresponse.OK with merged result
    • mapDomainError() maps ErrPreferencesNotFound → 404, ErrInvalidTheme/Language/Digest/Preferences → 400
    • Authorization: extracts user from auth.GetUser(ctx), compares with user_id path param, allows if match or user.HasRole("admin"); returns httperror.Forbidden otherwise
    • Response types: PreferencesResponse, PreferencesDataResponse, NotificationSettingsResponse as defined in design
    • Request types: UpdatePreferencesRequest with PreferencesPayload using pointer fields and validate:"required" tag
    • Tests cover: GET success, GET not found, GET forbidden (wrong user), PUT success (create), PUT success (merge), PUT bad request (invalid values), PUT bad request (unknown keys), PUT bad request (missing preferences), PUT forbidden

T7: Routes and OpenAPI spec — wire endpoints and document API (task-007)

  • Scope: Update routes.go to mount GET and PUT /preferences/{user_id} under auth middleware. Update spec.go to document both endpoints with schemas, examples, and error responses. Remove all example endpoint references.
  • Files:
    • Modify internal/api/routes.go
    • Modify internal/api/spec.go
  • Depends on: T6 (uses handler methods)
  • Acceptance criteria:
    • All example routes removed from routes.go
    • GET /api/preferences-api/preferences/{user_id} route registered with app.Wrap(handler.Get)
    • PUT /api/preferences-api/preferences/{user_id} route registered with app.Wrap(handler.Update)
    • Both preference routes are inside an auth.Middleware() group
    • Health endpoint remains at /api/preferences-api/health (unchanged)
    • OpenAPI spec defines Preferences, PreferencesUpdate, NotificationSettings schemas
    • OpenAPI spec documents GET and PUT endpoints with parameters, request bodies, responses (200, 400, 403, 404)
    • All example schemas and paths removed from spec

T8: Wire main.go and integration — connect all layers (task-008)

  • Scope: Update main.go to instantiate memory.NewPreferencesRepository(), service.NewPreferencesService(), and pass them through route registration. Verify the full service starts and endpoints respond correctly.
  • Files:
    • Modify cmd/server/main.go
  • Depends on: T7 (routes ready to accept handler)
  • Acceptance criteria:
    • main.go creates memory.NewPreferencesRepository()
    • main.go creates service.NewPreferencesService(repo, logger)
    • main.go passes service to route registration (handler created in routes or passed through)
    • All references to ExampleRepository and ExampleService removed
    • Service compiles: go build ./... succeeds
    • All tests pass: go test ./... succeeds
    • Service starts without error and health endpoint responds