build: /breakdown-feature user-preferences
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
rdev-worker 2026-02-08 18:24:58 +00:00
parent 8e08dbd822
commit 493f96a9fd
2 changed files with 169 additions and 1 deletions

View File

@ -25,5 +25,31 @@ artifacts:
status: draft
path: spec.md
tasks:
status: pending
status: draft
path: tasks.md
total: 8
tasks:
- id: task-001
title: Domain layer - preference types, validation, defaults, and errors
status: pending
- id: task-002
title: Port layer - PreferenceRepository interface
status: pending
- id: task-003
title: Adapter layer - in-memory PreferenceRepository for tests
status: pending
- id: task-004
title: Adapter layer - PostgreSQL PreferenceRepository with schema creation
status: pending
- id: task-005
title: Service layer - PreferenceService with business logic and tests
status: pending
- id: task-006
title: Handler layer - GET and PUT preference handlers with tests
status: pending
- id: task-007
title: Routes, OpenAPI spec, and main.go wiring
status: pending
- id: task-008
title: Remove example scaffold and verify clean build
status: pending

View File

@ -0,0 +1,142 @@
# 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