build: /breakdown-feature user-preferences
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
8e08dbd822
commit
493f96a9fd
@ -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
|
||||
|
||||
142
.sdlc/features/user-preferences/tasks.md
Normal file
142
.sdlc/features/user-preferences/tasks.md
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user