From a167ae7c25c972013a7a9c19e5b32a1941ddff2f Mon Sep 17 00:00:00 2001 From: rdev-worker Date: Sun, 8 Feb 2026 01:43:39 +0000 Subject: [PATCH] build: /spec-feature user-preferences --requirements 'CRUD API for user pref... --- .sdlc/features/user-preferences/manifest.yaml | 2 +- .sdlc/features/user-preferences/spec.md | 180 ++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 .sdlc/features/user-preferences/spec.md diff --git a/.sdlc/features/user-preferences/manifest.yaml b/.sdlc/features/user-preferences/manifest.yaml index b3633ef..35682f3 100644 --- a/.sdlc/features/user-preferences/manifest.yaml +++ b/.sdlc/features/user-preferences/manifest.yaml @@ -22,7 +22,7 @@ artifacts: status: pending path: review.md spec: - status: pending + status: draft path: spec.md tasks: status: pending diff --git a/.sdlc/features/user-preferences/spec.md b/.sdlc/features/user-preferences/spec.md new file mode 100644 index 0000000..751d17c --- /dev/null +++ b/.sdlc/features/user-preferences/spec.md @@ -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.