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
5a6d2dc3a9
commit
86e2ae36ec
@ -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: Remove example scaffold - delete all example entity files
|
||||
status: pending
|
||||
- id: task-002
|
||||
title: Domain layer - preferences entity, validation, merge logic
|
||||
status: pending
|
||||
- id: task-003
|
||||
title: Port interface - PreferencesRepository with Get and Upsert
|
||||
status: pending
|
||||
- id: task-004
|
||||
title: In-memory adapter - thread-safe map implementation
|
||||
status: pending
|
||||
- id: task-005
|
||||
title: Service layer - PreferencesService with Get and Upsert merge logic
|
||||
status: pending
|
||||
- id: task-006
|
||||
title: HTTP handlers - GET and PUT with auth, validation, error mapping
|
||||
status: pending
|
||||
- id: task-007
|
||||
title: Routes and OpenAPI spec - wire endpoints and document API
|
||||
status: pending
|
||||
- id: task-008
|
||||
title: Wire main.go and integration - connect all layers
|
||||
status: pending
|
||||
|
||||
123
.sdlc/features/user-preferences/tasks.md
Normal file
123
.sdlc/features/user-preferences/tasks.md
Normal file
@ -0,0 +1,123 @@
|
||||
# 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
|
||||
Loading…
Reference in New Issue
Block a user