build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
9c45486bef
commit
a167ae7c25
@ -22,7 +22,7 @@ artifacts:
|
||||
status: pending
|
||||
path: review.md
|
||||
spec:
|
||||
status: pending
|
||||
status: draft
|
||||
path: spec.md
|
||||
tasks:
|
||||
status: pending
|
||||
|
||||
180
.sdlc/features/user-preferences/spec.md
Normal file
180
.sdlc/features/user-preferences/spec.md
Normal file
@ -0,0 +1,180 @@
|
||||
# Feature: User Preferences API
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Users need a way to persist and retrieve their application preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently there is no preferences storage — the `preferences-api` service exists as a skeleton with only the example CRUD scaffold. This feature replaces the example entity with a real user preferences system.
|
||||
|
||||
## User Stories
|
||||
|
||||
- As an authenticated user, I want to save my theme preference so that the UI renders in my chosen theme across sessions.
|
||||
- As an authenticated user, I want to save my language preference so that content is displayed in my preferred language.
|
||||
- As an authenticated user, I want to configure my notification settings so that I only receive the alerts I care about.
|
||||
- As an authenticated user, I want to retrieve all my preferences in a single call so that the frontend can initialize quickly.
|
||||
- As an API consumer, I want to update individual preference keys without overwriting unrelated settings.
|
||||
|
||||
## API Design
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|------|------|-------------|
|
||||
| GET | `/api/preferences-api/preferences/{user_id}` | Required | Retrieve all preferences for a user |
|
||||
| PUT | `/api/preferences-api/preferences/{user_id}` | Required | Create or update preferences for a user (merge semantics) |
|
||||
|
||||
### Request / Response
|
||||
|
||||
**GET /api/preferences-api/preferences/{user_id}**
|
||||
|
||||
Response `200 OK`:
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"user_id": "uuid",
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"push": false,
|
||||
"digest": "weekly"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-02-08T00:00:00Z"
|
||||
},
|
||||
"meta": { "request_id": "...", "timestamp": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
Response `404 Not Found` (no preferences saved yet):
|
||||
```json
|
||||
{
|
||||
"error": { "code": "NOT_FOUND", "message": "preferences not found" },
|
||||
"meta": { "request_id": "...", "timestamp": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
**PUT /api/preferences-api/preferences/{user_id}**
|
||||
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"preferences": {
|
||||
"theme": "light",
|
||||
"notifications": {
|
||||
"email": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response `200 OK` (returns merged result):
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"user_id": "uuid",
|
||||
"preferences": {
|
||||
"theme": "light",
|
||||
"language": "en",
|
||||
"notifications": {
|
||||
"email": false,
|
||||
"push": false,
|
||||
"digest": "weekly"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-02-08T00:00:01Z"
|
||||
},
|
||||
"meta": { "request_id": "...", "timestamp": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
### Merge Semantics
|
||||
|
||||
PUT performs a **shallow merge** at the top-level keys (`theme`, `language`, `notifications`). Nested objects like `notifications` are replaced entirely when provided. This keeps behavior predictable without requiring JSON Patch complexity.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] GET `/api/preferences-api/preferences/{user_id}` returns `200` with stored preferences
|
||||
- [ ] GET returns `404` when no preferences exist for the user
|
||||
- [ ] PUT `/api/preferences-api/preferences/{user_id}` creates preferences if none exist (upsert)
|
||||
- [ ] PUT merges provided keys with existing preferences (shallow merge)
|
||||
- [ ] PUT returns `200` with the full merged preference set
|
||||
- [ ] PUT validates that `preferences` field is present and is a JSON object
|
||||
- [ ] PUT rejects unknown top-level preference keys with `400 Bad Request`
|
||||
- [ ] Both endpoints require authentication via `auth.Middleware()`
|
||||
- [ ] Authenticated user can only access their own preferences (user_id in path must match token subject), unless they have an admin role
|
||||
- [ ] `user_id` path parameter is validated as a UUID
|
||||
- [ ] Preferences are persisted in-memory via the existing adapter pattern (database adapter deferred)
|
||||
- [ ] OpenAPI spec documents both endpoints with schemas, examples, and error responses
|
||||
- [ ] Domain model defines allowed preference keys and validation rules
|
||||
- [ ] Handler tests cover success paths, validation errors, auth failures, and not-found cases
|
||||
- [ ] Service tests cover merge logic, create-on-first-PUT, and authorization checks
|
||||
- [ ] All existing `example` scaffold code is removed and replaced with preferences code
|
||||
|
||||
## Data Model
|
||||
|
||||
### Domain Entity: `Preference`
|
||||
|
||||
```go
|
||||
type UserID string
|
||||
|
||||
type Preferences struct {
|
||||
UserID UserID
|
||||
Theme string // "light", "dark", "system"
|
||||
Language string // ISO 639-1 code: "en", "es", "fr", etc.
|
||||
Notifications NotificationSettings
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type NotificationSettings struct {
|
||||
Email bool
|
||||
Push bool
|
||||
Digest string // "daily", "weekly", "never"
|
||||
}
|
||||
```
|
||||
|
||||
### Allowed Values
|
||||
|
||||
| Key | Type | Allowed Values | Default |
|
||||
|-----|------|----------------|---------|
|
||||
| `theme` | string | `light`, `dark`, `system` | `system` |
|
||||
| `language` | string | ISO 639-1 codes | `en` |
|
||||
| `notifications.email` | bool | `true`, `false` | `true` |
|
||||
| `notifications.push` | bool | `true`, `false` | `true` |
|
||||
| `notifications.digest` | string | `daily`, `weekly`, `never` | `weekly` |
|
||||
|
||||
## Technical Constraints
|
||||
|
||||
- Must follow the existing hexagonal architecture: domain → service → port → adapter
|
||||
- Must use `app.Wrap()`, `app.BindAndValidate()`, `httpresponse.*`, `httperror.*` patterns
|
||||
- Must use `auth.Middleware()` for protected routes
|
||||
- In-memory adapter for initial implementation (matches existing pattern); database migration deferred to a follow-up feature
|
||||
- Preference values must be validated against allowed values in the domain layer
|
||||
- OpenAPI spec must be updated to replace example endpoints with preference endpoints
|
||||
- Route base path remains `/api/preferences-api`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `pkg/app` — handler wrapping, request binding
|
||||
- `pkg/auth` — JWT middleware, context user extraction
|
||||
- `pkg/httperror` — typed HTTP errors
|
||||
- `pkg/httpresponse` — response envelope
|
||||
- `pkg/httpvalidation` — struct validation
|
||||
- `pkg/openapi` — spec builder
|
||||
- `pkg/logging` — structured logging
|
||||
- Existing `preferences-api` service skeleton (to be modified in-place)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Database persistence (PostgreSQL adapter) — separate follow-up feature
|
||||
- Bulk preference operations across multiple users
|
||||
- Preference history / audit log
|
||||
- Preference defaults management API (defaults are hardcoded in domain)
|
||||
- Frontend integration (consuming the API from apps/)
|
||||
- Rate limiting
|
||||
- Preference change webhooks / event publishing
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Authorization model**: Should any user be able to read another user's preferences, or is it strictly own-user-only? The spec assumes own-user + admin override, but this needs confirmation.
|
||||
2. **Additional preference keys**: Are `theme`, `language`, and `notifications` the complete set, or should the schema be extensible for future keys without code changes?
|
||||
3. **Default preferences**: When GET returns 404, should we instead return a `200` with default values? This simplifies the frontend but changes the semantic contract.
|
||||
Loading…
Reference in New Issue
Block a user