diff --git a/.sdlc/features/user-preferences/manifest.yaml b/.sdlc/features/user-preferences/manifest.yaml index a3abbca..4cf2549 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..d00353a --- /dev/null +++ b/.sdlc/features/user-preferences/spec.md @@ -0,0 +1,128 @@ +# Feature: User Preferences API + +## Problem Statement + +Users need the ability to persist and retrieve personal preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently, the `preferences-api` service exists as scaffolding with only an example CRUD resource — there is no actual preferences domain model or persistence. + +## User Stories + +- As a **user**, I want to save my theme preference so that the UI renders in my chosen theme across sessions. +- As a **user**, I want to save my language preference so that the application displays content in my preferred language. +- As a **user**, I want to configure notification settings so that I only receive the notifications I care about. +- As a **frontend application**, I want to retrieve all preferences for a user in a single API call so that I can initialize the UI efficiently. +- As a **backend service**, I want to read a user's preferences to personalize behavior (e.g., notification delivery channel). + +## API Design + +### Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| `GET` | `/api/preferences-api/preferences/{user_id}` | Required | Get all preferences for a user | +| `PUT` | `/api/preferences-api/preferences/{user_id}` | Required | Create or replace all preferences for a user | + +### Request: `PUT /api/preferences-api/preferences/{user_id}` + +```json +{ + "preferences": { + "theme": "dark", + "language": "en", + "notifications": { + "email": true, + "push": true, + "sms": false + } + } +} +``` + +### Response: `GET /api/preferences-api/preferences/{user_id}` + +```json +{ + "data": { + "user_id": "usr_abc123", + "preferences": { + "theme": "dark", + "language": "en", + "notifications": { + "email": true, + "push": true, + "sms": false + } + }, + "updated_at": "2026-02-08T10:30:00Z" + }, + "meta": { + "request_id": "req_xyz", + "timestamp": "2026-02-08T10:30:01Z" + } +} +``` + +### Storage Model + +Preferences are stored as a JSON document per user (not individual key-value rows). This simplifies reads (single query) and writes (single upsert), and avoids N+1 patterns for users with many preferences. + +### Known Preference Keys + +| Key | Type | Default | Validation | +|-----|------|---------|------------| +| `theme` | string | `"system"` | One of: `"light"`, `"dark"`, `"system"` | +| `language` | string | `"en"` | BCP-47 language tag, max 10 chars | +| `notifications.email` | bool | `true` | — | +| `notifications.push` | bool | `true` | — | +| `notifications.sms` | bool | `false` | — | + +The schema should be extensible — unknown keys are preserved but not validated, allowing new preference keys to be introduced without schema migrations. + +## Acceptance Criteria + +- [ ] `GET /api/preferences-api/preferences/{user_id}` returns the user's preferences with a `200` response in the standard `{data, meta}` envelope. +- [ ] `GET` returns default preferences (`theme: "system"`, `language: "en"`, `notifications: {email: true, push: true, sms: false}`) when no preferences have been saved for the user. +- [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or fully replaces the user's preferences, returning `200`. +- [ ] `PUT` validates known preference keys: rejects invalid `theme` values, rejects `language` values exceeding 10 characters. +- [ ] `PUT` with invalid input returns `400 Bad Request` with a descriptive error in the standard error envelope. +- [ ] Both endpoints require authentication via `auth.Middleware()`. +- [ ] The authenticated user can only access their own preferences (the `user_id` in the path must match the authenticated user's ID), returning `403 Forbidden` otherwise. +- [ ] Preferences are persisted to PostgreSQL and survive service restarts. +- [ ] The database schema uses an upsert pattern — `PUT` to a new `user_id` creates the record, `PUT` to an existing `user_id` replaces it. +- [ ] OpenAPI spec is updated with both endpoints, request/response schemas, and examples. +- [ ] Handler, service, and domain layers follow existing hexagonal architecture patterns. +- [ ] Unit tests cover: handler request/response mapping, service business logic (defaults, validation, authorization), domain validation rules. +- [ ] URL parameters use brace syntax `{user_id}` (not colon syntax). + +## Technical Constraints + +- **Architecture**: Must follow the existing hexagonal architecture (domain → service → port → adapter) established in the preferences-api service. +- **Database**: PostgreSQL via `pkg/database`. Migrations embedded with `//go:embed`. Single table with JSONB column for preferences. +- **Auth**: Endpoints protected by `auth.Middleware()`. User ID extracted from JWT claims via `auth.GetUser(ctx)`. +- **Response format**: All responses use `httpresponse.OK/Created/NoContent` helpers for the `{data, meta}` envelope. +- **Error handling**: Domain errors mapped to HTTP errors in handlers via `mapDomainError()` pattern. Use `httperror.BadRequest`, `httperror.NotFound`, `httperror.Forbidden`. +- **Request binding**: Use `app.BindAndValidate()` for PUT request body. +- **Router**: chi router with `{param}` brace syntax for URL parameters. +- **Port**: Service runs on port 8001 (already configured). + +## Dependencies + +- `pkg/database` — PostgreSQL connection and migration support (already exists). +- `pkg/auth` — JWT middleware and user extraction (already exists). +- `pkg/app`, `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` — HTTP framework (already exists). +- A running PostgreSQL instance for persistence (managed via `DATABASE_URL` env var). + +## Out of Scope + +- **Bulk preferences for multiple users** — Only single-user GET/PUT in this iteration. +- **PATCH (partial update)** — Full replacement via PUT only. Partial updates may be added later. +- **DELETE endpoint** — No need to delete preferences (they reset to defaults if the row is removed manually). +- **Preference history/audit log** — No tracking of preference changes over time. +- **Admin override** — No admin endpoint to modify another user's preferences. +- **Frontend integration** — API only; frontend changes are a separate feature. +- **Rate limiting** — Handled at the infrastructure layer, not in this feature. + +## Open Questions + +1. **Authorization model**: Should the authenticated user's ID come from the JWT `sub` claim, or is there a separate `user_id` field in the token? (Needs alignment with `auth.GetUser()` contract.) +2. **Unknown preference keys**: Should unknown keys be silently accepted and stored, or rejected with a validation error? (Spec currently assumes they are preserved.) +3. **Preference size limit**: Should there be a maximum size for the preferences JSON document to prevent abuse? (e.g., 10KB limit.)