From ba23b2797486c91f70a4603cb8faa0d3f913cfdc Mon Sep 17 00:00:00 2001 From: rdev-worker Date: Mon, 9 Feb 2026 03:10:22 +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 | 167 ++++++++++++++++++ 2 files changed, 168 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 fd78109..0bbc71d 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..126592c --- /dev/null +++ b/.sdlc/features/user-preferences/spec.md @@ -0,0 +1,167 @@ +# Feature: User Preferences API + +## Problem Statement + +Users need the ability to store and retrieve personal preferences (theme, language, notification settings) so that their experience is personalized and consistent across sessions. Currently, the preferences-api service has only scaffolded example endpoints with no real preference management functionality. + +## User Stories + +- As an application user, I want to retrieve my preferences so that the UI reflects my chosen theme, language, and notification settings. +- As an application user, I want to update my preferences so that changes persist across sessions. +- As a frontend application, I want to fetch preferences by user ID on login so that I can render the correct theme and locale immediately. +- As a system administrator, I want preferences stored as structured key-value pairs so that new preference types can be added without schema changes. + +## Acceptance Criteria + +- [ ] `GET /api/preferences-api/preferences/{user_id}` returns all preferences for a user as key-value pairs +- [ ] `GET /api/preferences-api/preferences/{user_id}` returns `200` with empty preferences object when user has no stored preferences +- [ ] `GET /api/preferences-api/preferences/{user_id}` returns `404` only if user_id format is invalid (not a valid UUID) +- [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or updates preferences (upsert behavior) +- [ ] `PUT /api/preferences-api/preferences/{user_id}` accepts a JSON body with a `preferences` object containing key-value pairs +- [ ] `PUT /api/preferences-api/preferences/{user_id}` returns `200` with the updated preferences +- [ ] `PUT /api/preferences-api/preferences/{user_id}` validates known preference keys against allowed values: + - `theme`: `"light"`, `"dark"`, `"system"` + - `language`: valid BCP-47 language tag (e.g., `"en"`, `"fr"`, `"es"`, `"de"`, `"ja"`) + - `notifications_enabled`: `true` or `false` +- [ ] `PUT /api/preferences-api/preferences/{user_id}` returns `400` with details when validation fails +- [ ] Unknown preference keys are accepted and stored (extensibility for future preference types) +- [ ] Preferences are persisted in PostgreSQL and survive service restarts +- [ ] All responses follow the `{data, meta}` envelope pattern +- [ ] OpenAPI spec documents both endpoints with request/response schemas +- [ ] Handler tests cover success paths and error cases +- [ ] Service-layer tests cover business logic (validation, upsert behavior) +- [ ] Database migration creates the preferences table + +## 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_enabled": true + }, + "updated_at": "2026-02-09T12:00:00Z" + }, + "meta": { + "request_id": "...", + "timestamp": "..." + } +} +``` + +### PUT /api/preferences-api/preferences/{user_id} + +**Request Body:** +```json +{ + "preferences": { + "theme": "dark", + "language": "fr", + "notifications_enabled": false + } +} +``` + +**Response 200:** +```json +{ + "data": { + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "preferences": { + "theme": "dark", + "language": "fr", + "notifications_enabled": false + }, + "updated_at": "2026-02-09T12:01:00Z" + }, + "meta": { + "request_id": "...", + "timestamp": "..." + } +} +``` + +**Response 400 (validation error):** +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid preference values", + "details": { + "theme": "must be one of: light, dark, system" + } + }, + "meta": { + "request_id": "...", + "timestamp": "..." + } +} +``` + +## Technical Constraints + +- Must follow existing hexagonal architecture: domain -> service -> port -> adapter +- Must use `app.Wrap()` handler pattern, `httpresponse.*` envelope, `httperror.*` error types +- Must use `{param}` brace syntax for chi URL parameters (never colon syntax) +- Database adapter uses `pkg/database` (sqlx) with embedded SQL migrations +- Preferences stored as JSONB column in PostgreSQL for flexible key-value storage +- `user_id` is a UUID path parameter (validated at handler level) +- PUT is idempotent: creates preferences row if none exists, updates if it does (upsert via `ON CONFLICT`) +- Service runs on port 8001 under route prefix `/api/preferences-api` + +## Database Schema + +```sql +-- migrations/001_create_preferences.sql +CREATE TABLE IF NOT EXISTS preferences ( + user_id UUID PRIMARY KEY, + preferences JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_preferences_updated_at ON preferences (updated_at); +``` + +## Hexagonal Architecture Mapping + +| Layer | File | Responsibility | +|-------|------|----------------| +| Domain | `internal/domain/preference.go` | `UserPreferences` model, validation rules for known keys | +| Port | `internal/port/preference.go` | `PreferenceRepository` interface (`Get`, `Upsert`) | +| Service | `internal/service/preference.go` | `PreferenceService` with validation + delegation to port | +| Adapter | `internal/adapter/postgres/preference.go` | PostgreSQL implementation of `PreferenceRepository` | +| Handler | `internal/api/handlers/preference.go` | HTTP handlers for GET/PUT endpoints | +| Routes | `internal/api/routes.go` | Register preference routes | +| Spec | `internal/api/spec.go` | OpenAPI documentation for preference endpoints | + +## Dependencies + +- PostgreSQL database (already configured via `DATABASE_URL` in `.env.example`) +- `pkg/database` package for connection pooling and migrations +- `pkg/app`, `pkg/httperror`, `pkg/httpresponse` for handler patterns +- `pkg/openapi` for API documentation +- Existing preferences-api service scaffolding + +## Out of Scope + +- Authentication/authorization enforcement (auth is opt-in per CLAUDE.md; can be layered on later) +- Per-preference-key endpoints (e.g., `GET /preferences/{user_id}/theme`) - full object only +- Preference defaults management (hardcoded defaults in frontend, not API) +- Preference change history/audit log +- Bulk preference operations across multiple users +- Real-time preference change notifications (WebSocket/SSE) +- DELETE endpoint (preferences are upserted, not deleted) + +## Open Questions + +1. **Should unknown preference keys have value-type validation?** Currently spec allows any JSON value for unknown keys. Should we restrict to strings/booleans/numbers only? +2. **Should there be a maximum number of preference keys per user?** A limit would prevent abuse but adds complexity. +3. **Should the API enforce that `user_id` corresponds to a real user?** This requires a call to an auth/user service. Current spec treats `user_id` as an opaque UUID with no cross-service validation. +4. **Should preference values have a max size limit?** JSONB columns can hold large values; a per-value or total-size limit may be prudent.