# Tasks: User Preferences API

## Task Order (dependency sequence)

### T1: Domain layer - preference types, validation, defaults, and domain errors
- **Scope:** Create the core domain model: `PreferenceKey` constants, `PreferenceDefinition` registry with default values and per-key validators, `UserPreferences` aggregate, helper functions (`DefaultPreferences`, `ValidateKey`, `ValidateValue`, `MergeWithDefaults`, `SerializeForResponse`), and domain errors (`ErrUnknownPreferenceKey`, `ErrInvalidPreferenceValue`, `ErrInvalidUserID`). Also implement `ValidateUserID` (UUID format check).
- **Files:**
  - Create `services/preferences-api/internal/domain/preference.go`
  - Modify `services/preferences-api/internal/domain/errors.go` (replace example errors)
  - Delete `services/preferences-api/internal/domain/example.go`
- **Depends on:** None
- **Acceptance criteria:**
  - [ ] `PreferenceKey` constants defined for `theme`, `language`, `notifications_enabled`
  - [ ] `DefaultPreferences()` returns all 3 keys with correct defaults (`system`, `en`, `true`)
  - [ ] `ValidateKey()` rejects unknown keys, accepts known keys
  - [ ] `ValidateValue()` enforces: theme in {light, dark, system}, language matches BCP 47 regex, notifications_enabled in {true, false}
  - [ ] `MergeWithDefaults()` fills missing keys with defaults, preserves stored values
  - [ ] `SerializeForResponse()` converts `"true"`/`"false"` to boolean for `notifications_enabled`
  - [ ] Domain errors are sentinel errors with descriptive messages
  - [ ] `ValidateUserID()` accepts valid UUIDs, rejects non-UUID strings

### T2: Port layer - PreferenceRepository interface and row type
- **Scope:** Define the `PreferenceRepository` port interface with `GetByUserID` and `Upsert` methods, plus the `PreferenceRow` data transfer type.
- **Files:**
  - Create `services/preferences-api/internal/port/preference.go`
  - Delete `services/preferences-api/internal/port/example.go`
- **Depends on:** T1
- **Acceptance criteria:**
  - [ ] `PreferenceRepository` interface defined with `GetByUserID(ctx, userID) ([]PreferenceRow, error)` and `Upsert(ctx, userID, key, value) error`
  - [ ] `PreferenceRow` struct has fields: `UserID`, `Key`, `Value`, `CreatedAt`, `UpdatedAt`
  - [ ] Interface is minimal (no delete, no list-all-users)

### T3: Service layer - PreferenceService with get, update, validation logic and unit tests
- **Scope:** Implement `PreferenceService` with `GetPreferences` and `UpdatePreferences` methods. `GetPreferences` fetches stored rows, merges with defaults, serializes for response. `UpdatePreferences` validates keys/values, upserts each, then returns full merged result. Write comprehensive unit tests with a mock repository.
- **Files:**
  - Create `services/preferences-api/internal/service/preference.go`
  - Create `services/preferences-api/internal/service/preference_test.go`
  - Delete `services/preferences-api/internal/service/example.go`
  - Delete `services/preferences-api/internal/service/example_test.go`
- **Depends on:** T1, T2
- **Acceptance criteria:**
  - [ ] `GetPreferences` returns all defaults when no rows stored
  - [ ] `GetPreferences` merges stored values with defaults for missing keys
  - [ ] `GetPreferences` returns `ErrInvalidUserID` for non-UUID user_id
  - [ ] `UpdatePreferences` rejects unknown keys with `ErrUnknownPreferenceKey`
  - [ ] `UpdatePreferences` rejects invalid values with `ErrInvalidPreferenceValue`
  - [ ] `UpdatePreferences` calls `Upsert` for each provided key
  - [ ] `UpdatePreferences` returns full merged preferences after upsert
  - [ ] `UpdatePreferences` handles boolean input (converts `true`/`false` to string)
  - [ ] Unit tests cover: defaults-only, partial stored, full stored, unknown key, invalid value, invalid user_id
  - [ ] Tests use mock repository (no database dependency)

### T4: Database migration - create user_preferences table
- **Scope:** Write the SQL migration to create the `user_preferences` table with composite primary key `(user_id, key)` and an index on `user_id`.
- **Files:**
  - Create `services/preferences-api/migrations/001_create_user_preferences.sql`
- **Depends on:** None
- **Acceptance criteria:**
  - [ ] Table `user_preferences` created with columns: `user_id` (UUID NOT NULL), `key` (VARCHAR(64) NOT NULL), `value` (TEXT NOT NULL), `created_at` (TIMESTAMPTZ DEFAULT NOW()), `updated_at` (TIMESTAMPTZ DEFAULT NOW())
  - [ ] Primary key is `(user_id, key)`
  - [ ] Index `idx_user_preferences_user_id` created on `user_id`
  - [ ] Migration is idempotent-safe (CREATE TABLE, not CREATE TABLE IF NOT EXISTS — rely on migration runner)

### T5: PostgreSQL adapter - implement PreferenceRepository with sqlx
- **Scope:** Implement `PostgresPreferenceRepository` that satisfies the `PreferenceRepository` interface using sqlx queries. `GetByUserID` selects all rows for a user. `Upsert` uses `INSERT ... ON CONFLICT DO UPDATE`.
- **Files:**
  - Create `services/preferences-api/internal/adapter/postgres/preference.go`
  - Delete `services/preferences-api/internal/adapter/memory/example.go`
- **Depends on:** T2, T4
- **Acceptance criteria:**
  - [ ] `PostgresPreferenceRepository` struct holds a `*sqlx.DB` (or pool from `pkg/database`)
  - [ ] `GetByUserID` executes `SELECT user_id, key, value, created_at, updated_at FROM user_preferences WHERE user_id = $1`
  - [ ] `Upsert` executes `INSERT INTO user_preferences ... ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`
  - [ ] Constructor `NewPreferenceRepository(db)` returns a new instance
  - [ ] Implements `port.PreferenceRepository` interface (compile-time check)

### T6: Handler layer - GET and PUT preference handlers with error mapping and handler tests
- **Scope:** Implement `PreferenceHandler` with `GetPreferences` and `UpdatePreferences` HTTP handlers. GET extracts `user_id` from URL, calls service, maps errors. PUT also binds request body via `app.Bind`. Write handler tests using chi test router with a mock service or mock repository.
- **Files:**
  - Create `services/preferences-api/internal/api/handlers/preference.go`
  - Create `services/preferences-api/internal/api/handlers/preference_test.go`
  - Delete `services/preferences-api/internal/api/handlers/example.go`
  - Delete `services/preferences-api/internal/api/handlers/example_test.go`
- **Depends on:** T3
- **Acceptance criteria:**
  - [ ] GET handler extracts `user_id` via `chi.URLParam(r, "user_id")`
  - [ ] GET handler returns 200 with `{data, meta}` envelope containing `user_id` and `preferences`
  - [ ] GET handler returns 400 for invalid `user_id`
  - [ ] PUT handler binds request body via `app.Bind()`
  - [ ] PUT handler returns 200 with full merged preferences after update
  - [ ] PUT handler returns 400 for unknown key, invalid value, or invalid user_id
  - [ ] Both handlers return `error` wrapped with `app.Wrap()`
  - [ ] Handler tests cover: successful GET, GET with defaults, successful PUT, PUT unknown key, PUT invalid value, invalid user_id for both endpoints
  - [ ] Response shape matches spec: `{"data": {"user_id": "...", "preferences": {...}}, "meta": {...}}`

### T7: Routes, OpenAPI spec, and main.go wiring
- **Scope:** Update `routes.go` to register new preference routes (replacing example routes). Update `spec.go` to document the new endpoints with schemas. Update `main.go` to wire database connection, run migrations, create PostgreSQL adapter, and inject into the service.
- **Files:**
  - Modify `services/preferences-api/internal/api/routes.go`
  - Modify `services/preferences-api/internal/api/spec.go`
  - Modify `services/preferences-api/cmd/server/main.go`
- **Depends on:** T5, T6
- **Acceptance criteria:**
  - [ ] Routes: `GET /api/preferences-api/preferences/{user_id}` registered
  - [ ] Routes: `PUT /api/preferences-api/preferences/{user_id}` registered
  - [ ] Routes: Old `/examples` routes removed
  - [ ] Routes: Preference routes in auth-protectable group (conditional on `AUTH_ENABLED`)
  - [ ] Routes: Health endpoint unchanged
  - [ ] OpenAPI spec documents both endpoints with request/response schemas
  - [ ] OpenAPI spec includes `UserPreferences` and `UpdatePreferencesRequest` schemas
  - [ ] `main.go` connects to database via `pkg/database`
  - [ ] `main.go` runs migrations on startup
  - [ ] `main.go` creates `PostgresPreferenceRepository` and injects into service
  - [ ] `main.go` adds DB pool shutdown hook

### T8: Cleanup - remove example scaffold files
- **Scope:** Delete all remaining example scaffold files that were not already replaced in previous tasks. Verify no references to `example` or `Example` remain in the codebase under `services/preferences-api/`. Ensure the service compiles and all tests pass.
- **Files:**
  - Delete any remaining `example*.go` files
  - Delete `services/preferences-api/internal/adapter/memory/` directory (if not already removed)
- **Depends on:** T7
- **Acceptance criteria:**
  - [ ] No files named `example*.go` exist under `services/preferences-api/`
  - [ ] No references to `Example`, `example`, or `memory.New` in `services/preferences-api/`
  - [ ] `go build ./...` succeeds from `services/preferences-api/`
  - [ ] `go test ./...` passes from `services/preferences-api/`
  - [ ] `go vet ./...` passes from `services/preferences-api/`
