diff --git a/.sdlc/features/user-preferences/manifest.yaml b/.sdlc/features/user-preferences/manifest.yaml index b528272..70914e2 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..836c923 --- /dev/null +++ b/.sdlc/features/user-preferences/spec.md @@ -0,0 +1,154 @@ +# Feature: User Preferences API + +## Problem Statement + +Users of the platform have no way to persist their personal settings (theme, language, notification preferences). Without a preferences API, every client must manage settings locally, leading to inconsistent experiences across devices and sessions. The `preferences-api` service currently contains only scaffold/example code and needs to be replaced with real preference management functionality. + +## User Stories + +- As a user, I want to save my preferred theme so that the UI looks the same every time I log in. +- As a user, I want to set my preferred language so that content is displayed in my language across devices. +- As a user, I want to configure my notification settings so that I only receive the alerts I care about. +- As a frontend developer, I want a simple GET/PUT API to read and write user preferences so that I can build settings pages without managing local storage. + +## Acceptance Criteria + +- [ ] `GET /api/preferences-api/preferences/{user_id}` returns all preferences for a user as key-value pairs +- [ ] `GET` returns a default set of preferences (`theme`, `language`, `notifications`) when the user has no saved preferences +- [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or updates preferences for a user (upsert semantics) +- [ ] `PUT` validates that `theme` is one of: `light`, `dark`, `system` +- [ ] `PUT` validates that `language` is a valid BCP-47 language tag (e.g., `en`, `fr`, `es`, `de`, `ja`) +- [ ] `PUT` validates that `notifications` is an object with boolean fields: `email`, `push`, `in_app` +- [ ] `PUT` supports partial updates — omitted keys retain their current values +- [ ] All responses follow the `{data, meta}` envelope pattern +- [ ] Preferences are persisted in PostgreSQL (not in-memory) +- [ ] Database migration creates the preferences table +- [ ] `user_id` is validated as a UUID format +- [ ] `PUT` on protected route group (requires auth when `AUTH_ENABLED=true`) +- [ ] `GET` on protected route group (requires auth when `AUTH_ENABLED=true`) +- [ ] Handler tests cover success, validation errors, not-found defaults, and invalid user ID +- [ ] Service-layer tests cover business logic with mock repository +- [ ] Domain model enforces preference value constraints +- [ ] OpenAPI spec documents both endpoints with request/response schemas +- [ ] Existing Example CRUD scaffold code is removed + +## API Design + +### GET /api/preferences-api/preferences/{user_id} + +**Response 200:** +```json +{ + "data": { + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "preferences": { + "theme": "dark", + "language": "en", + "notifications": { + "email": true, + "push": false, + "in_app": true + } + }, + "updated_at": "2026-01-15T10:30:00Z" + }, + "meta": { + "request_id": "...", + "timestamp": "..." + } +} +``` + +### PUT /api/preferences-api/preferences/{user_id} + +**Request body (partial update):** +```json +{ + "theme": "dark", + "notifications": { + "email": false + } +} +``` + +**Response 200:** +```json +{ + "data": { + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "preferences": { + "theme": "dark", + "language": "en", + "notifications": { + "email": false, + "push": true, + "in_app": true + } + }, + "updated_at": "2026-02-08T12:00:00Z" + }, + "meta": { + "request_id": "...", + "timestamp": "..." + } +} +``` + +## Default Preference Values + +| Key | Default Value | +|-----------------|---------------| +| `theme` | `system` | +| `language` | `en` | +| `notifications.email` | `true` | +| `notifications.push` | `true` | +| `notifications.in_app` | `true` | + +## Technical Constraints + +- **Database:** PostgreSQL via `pkg/database` (connection pool, migrations, transactions) +- **Architecture:** Hexagonal — domain, service, port (interface), adapter (postgres implementation) +- **Router:** chi with `{param}` brace syntax for URL parameters +- **Validation:** `pkg/httpvalidation` with `go-playground/validator` struct tags +- **Error handling:** Domain errors mapped to HTTP errors via `httperror.*` factories +- **Response format:** All responses wrapped via `httpresponse.OK`, `httpresponse.Created`, etc. +- **Auth:** Opt-in via `auth.Middleware()` in route group, controlled by `AUTH_ENABLED` env var +- **Migrations:** SQL files in `services/preferences-api/migrations/` using `pkg/database.RunMigrations` +- **Port:** Service runs on port 8001 (existing allocation) + +## Database Schema + +```sql +CREATE TABLE IF NOT EXISTS user_preferences ( + user_id UUID PRIMARY KEY, + theme VARCHAR(10) NOT NULL DEFAULT 'system', + language VARCHAR(10) NOT NULL DEFAULT 'en', + notif_email BOOLEAN NOT NULL DEFAULT true, + notif_push BOOLEAN NOT NULL DEFAULT true, + notif_in_app BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +## Dependencies + +- PostgreSQL database available (connection string via `DATABASE_URL` env var) +- `pkg/database` package for connection pooling and migrations +- `pkg/app`, `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` packages (all exist) +- `pkg/auth` for JWT middleware (exists) + +## Out of Scope + +- User creation/management (user IDs come from an external auth system) +- Preference history/audit log +- Preference schemas beyond `theme`, `language`, `notifications` +- Admin endpoints to manage preferences for other users +- Real-time preference sync via WebSocket +- Preference import/export + +## Open Questions + +1. **Authorization model:** Should `GET /preferences/{user_id}` be restricted so users can only read their own preferences, or can any authenticated user read any user's preferences? (Spec assumes auth-protected but no ownership check — simplest to start.) +2. **Extensible keys:** Should the API support arbitrary preference keys beyond the three defined, or should it be a fixed schema? (Spec assumes fixed schema for type safety and validation.) +3. **Rate limiting:** Should PUT be rate-limited to prevent abuse? (Spec assumes no — can be added later via middleware.)