slate-test-1770505673/.sdlc/features/user-preferences/tasks.md
rdev-worker 2c87b1b618
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /breakdown-feature user-preferences
2026-02-07 23:34:51 +00:00

8.2 KiB

Tasks: User Preferences API

Task Order (dependency sequence)

T1 → T2 → T3 → T4 → T5 → T6

All tasks are sequential — each builds on the previous layer of the hexagonal architecture.


T1: Remove example scaffold code (task-001)

  • Scope: Delete all example-related files from the preferences-api service. This clears the scaffold to make room for preference-specific domain logic. The health handler, config, Makefile, Dockerfile, and component.yaml are preserved.
  • Files:
    • Delete internal/domain/example.go
    • Delete internal/domain/errors.go
    • Delete internal/port/example.go
    • Delete internal/service/example.go
    • Delete internal/service/example_test.go
    • Delete internal/adapter/memory/example.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
    • internal/api/handlers/health.go is untouched
    • internal/config/config.go is untouched
    • Code compiles after removing example references from routes.go and main.go (temporary stubs or comment-outs are acceptable since later tasks will replace them)

T2: Implement domain layer - preference types, validation, and errors (task-002)

  • Scope: Create the preference domain model with Preference type, AllowedKeys map, key/value validation functions, and domain error types. Include unit tests for all validation logic.
  • Files:
    • Create internal/domain/preference.goPreference struct, AllowedKeys map, Validate(), ValidateKey(), ValidateValue() functions
    • Create internal/domain/errors.goErrUnknownKey, ErrInvalidValue, ErrForbidden
    • Create internal/domain/preference_test.go — Unit tests for validation
  • Depends on: T1
  • Acceptance criteria:
    • AllowedKeys includes theme (light, dark, system), language (ISO 639-1 regex ^[a-z]{2}$), notifications_enabled (true, false)
    • ValidateKey("theme") returns nil; ValidateKey("unknown") returns ErrUnknownKey
    • ValidateValue("theme", "dark") returns nil; ValidateValue("theme", "blue") returns ErrInvalidValue
    • ValidateValue("language", "en") returns nil; ValidateValue("language", "english") returns ErrInvalidValue
    • ValidateValue("notifications_enabled", "true") returns nil; ValidateValue("notifications_enabled", "yes") returns ErrInvalidValue
    • Domain errors use errors.New and are exported for use in error mapping
    • All tests pass: go test -v ./internal/domain/...

T3: Implement port interface and PostgreSQL adapter with migration (task-003)

  • Scope: Define the PreferenceRepository port interface and implement the PostgreSQL adapter. Create the SQL migration for the user_preferences table.
  • Files:
    • Create internal/port/preference.goPreferenceRepository interface with GetByUserID and Upsert methods
    • Create internal/adapter/postgres/preference.go — PostgreSQL implementation using sqlx
    • Create migrations/001_create_user_preferences.sql — Table DDL with composite PK and index
  • Depends on: T2
  • Acceptance criteria:
    • PreferenceRepository interface defines GetByUserID(ctx, userID) (map[string]string, error) and Upsert(ctx, userID, prefs map[string]string) error
    • PostgreSQL adapter GetByUserID returns empty map (not nil) when no rows exist
    • PostgreSQL adapter Upsert uses INSERT ... ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW() within a transaction
    • Migration creates user_preferences table with composite PK (user_id, key) and index on user_id
    • All queries use parameterized statements (no SQL injection risk)
    • Code compiles successfully

T4: Implement service layer with business logic and tests (task-004)

  • Scope: Create the PreferenceService that orchestrates get/upsert operations, performs domain validation, and delegates persistence to the repository port. Write comprehensive service-layer tests using a mock repository.
  • Files:
    • Create internal/service/preference.goPreferenceService with Get and Upsert methods, constructor with DI
    • Create internal/service/preference_test.go — Table-driven tests with mock repository
  • Depends on: T3
  • Acceptance criteria:
    • Get(ctx, userID) delegates to repository GetByUserID and returns the result
    • Upsert(ctx, userID, prefs) validates all keys/values using domain validation before persisting
    • Upsert with an unknown key returns an error wrapping ErrUnknownKey
    • Upsert with an invalid value returns an error wrapping ErrInvalidValue
    • Upsert returns the full preference set after the update (calls GetByUserID after successful upsert)
    • Service accepts PreferenceRepository interface (not concrete type) for testability
    • Tests use a mock repository implementing PreferenceRepository
    • Tests cover: successful get, get empty, successful upsert, upsert with unknown key, upsert with invalid value, repository error propagation
    • All tests pass: go test -v ./internal/service/...

T5: Implement HTTP handlers with auth ownership check and tests (task-005)

  • Scope: Create GET and PUT preference handlers with UUID validation, JWT ownership check, request binding, domain-to-HTTP error mapping, and comprehensive handler tests.
  • Files:
    • Create internal/api/handlers/preference.goPreferenceHandler with Get and Update methods, mapDomainError helper
    • Create internal/api/handlers/preference_test.go — Handler tests covering success, validation, auth, and ownership cases
  • Depends on: T4
  • Acceptance criteria:
    • GET handler extracts user_id from URL path, validates UUID format, checks ownership against JWT subject, returns preferences via httpresponse.OK
    • PUT handler binds JSON body to map[string]string, validates UUID, checks ownership, calls service Upsert, returns updated preferences via httpresponse.OK
    • Invalid UUID returns httperror.BadRequest("invalid user ID format")
    • JWT subject mismatch returns httperror.Forbidden("cannot access preferences for another user")
    • Empty PUT body returns httperror.BadRequest("request body is required")
    • ErrUnknownKey maps to 400; ErrInvalidValue maps to 400 with descriptive message
    • All handlers return error and are compatible with app.Wrap()
    • Tests cover: successful GET, GET empty prefs, GET invalid UUID, GET forbidden, successful PUT, PUT invalid key, PUT invalid value, PUT empty body, PUT forbidden
    • All tests pass: go test -v ./internal/api/handlers/...

T6: Wire routes, OpenAPI spec, and main.go integration (task-006)

  • Scope: Update route registration to mount preference endpoints with mandatory auth, replace the OpenAPI spec with preference endpoint documentation, and update main.go to initialize DB pool, run migrations, and wire all dependencies.
  • Files:
    • Modify internal/api/routes.go — Replace example routes with preference routes under auth middleware group
    • Replace internal/api/spec.go — OpenAPI docs for GET/PUT /preferences/{user_id} with request/response schemas
    • Modify cmd/server/main.go — Add database pool initialization, migration runner, wire PreferenceService with PostgreSQL adapter
  • Depends on: T5
  • Acceptance criteria:
    • Routes mount GET /api/preferences-api/preferences/{user_id} and PUT /api/preferences-api/preferences/{user_id} under auth middleware
    • Auth middleware is mandatory for preference routes (not conditional on AUTH_ENABLED)
    • Health endpoint remains unprotected
    • OpenAPI spec documents both endpoints with request/response schemas, security requirements, and error responses (400, 401, 403)
    • main.go initializes database pool using pkg/database, runs embedded migrations, creates PostgreSQL adapter, wires into service
    • Application starts and routes are accessible (compile + basic wiring test)
    • Full test suite passes: cd services/preferences-api && go test -v ./...