# Tasks: User Preferences API

## Task Order (dependency sequence)

### T1: Domain layer - preferences entity, validation, defaults, and errors (task-001)
- **Scope:** Create the `UserPreferences` domain entity with `Preferences` and `NotificationSettings` structs, `UserID` type, `DefaultPreferences()` factory, `Validate()` method, and domain errors (`ErrInvalidTheme`, `ErrInvalidLanguage`, `ErrInvalidDigest`). Update `errors.go` to remove example-specific errors.
- **Files:**
  - Create `services/preferences-api/internal/domain/preferences.go`
  - Modify `services/preferences-api/internal/domain/errors.go`
  - Delete `services/preferences-api/internal/domain/example.go`
- **Depends on:** None
- **Acceptance criteria:**
  - [ ] `UserID`, `Preferences`, `NotificationSettings`, `UserPreferences` types defined
  - [ ] `DefaultPreferences()` returns correct defaults (theme=system, language=en, notifications: email=true, push=true, digest=weekly)
  - [ ] `Validate()` rejects invalid theme values (not light/dark/system)
  - [ ] `Validate()` rejects empty language string
  - [ ] `Validate()` rejects invalid digest values (not daily/weekly/never)
  - [ ] `Validate()` accepts valid combinations
  - [ ] Domain errors `ErrInvalidTheme`, `ErrInvalidLanguage`, `ErrInvalidDigest` defined
  - [ ] Example-specific errors (`ErrExampleNotFound`, `ErrDuplicateExample`, `ErrInvalidExampleName`) removed from `errors.go`
  - [ ] `example.go` deleted

### T2: Port layer - PreferencesRepository interface (task-002)
- **Scope:** Define the `PreferencesRepository` interface with `Get` and `Upsert` methods. Remove the `ExampleRepository` interface.
- **Files:**
  - Create `services/preferences-api/internal/port/preferences.go`
  - Delete `services/preferences-api/internal/port/example.go`
- **Depends on:** T1 (uses domain types)
- **Acceptance criteria:**
  - [ ] `PreferencesRepository` interface defined with `Get(ctx, UserID) (*UserPreferences, error)` and `Upsert(ctx, *UserPreferences) error`
  - [ ] Interface uses domain types (`domain.UserID`, `domain.UserPreferences`)
  - [ ] `example.go` deleted from port package

### T3: Service layer - PreferencesService with deep merge, get/update logic, and unit tests (task-003)
- **Scope:** Implement `PreferencesService` with `GetPreferences` (returns defaults for unknown users) and `UpdatePreferences` (fetch existing → deep merge incoming partial data → validate merged result → upsert). Include comprehensive unit tests with a mock repository.
- **Files:**
  - Create `services/preferences-api/internal/service/preferences.go`
  - Create `services/preferences-api/internal/service/preferences_test.go`
  - Delete `services/preferences-api/internal/service/example.go`
  - Delete `services/preferences-api/internal/service/example_test.go`
- **Depends on:** T1 (domain types, validation), T2 (port interface)
- **Acceptance criteria:**
  - [ ] `NewPreferencesService(repo, logger)` constructor
  - [ ] `GetPreferences(ctx, userID)` returns stored preferences or defaults for unknown users
  - [ ] `UpdatePreferences(ctx, userID, partialInput)` performs deep merge with existing preferences
  - [ ] Deep merge handles: theme only, language only, notifications only, nested notification fields individually, all fields together
  - [ ] Validation runs on the merged result before persisting
  - [ ] Invalid values after merge return domain validation errors
  - [ ] Unit tests cover: get existing user, get unknown user returns defaults, update creates new preferences, update merges partial data, update with invalid theme rejected, update with invalid digest rejected, update with empty language rejected
  - [ ] Example service files deleted

### T4: Database migration and PostgreSQL adapter (task-004)
- **Scope:** Create the `user_preferences` table migration and implement the PostgreSQL adapter for `PreferencesRepository`. Remove the in-memory example adapter.
- **Files:**
  - Create `services/preferences-api/migrations/001_create_user_preferences.sql`
  - Create `services/preferences-api/internal/adapter/postgres/preferences.go`
  - Delete `services/preferences-api/internal/adapter/memory/example.go`
- **Depends on:** T1 (domain types), T2 (port interface)
- **Acceptance criteria:**
  - [ ] Migration creates `user_preferences` table with `user_id UUID PRIMARY KEY`, `preferences JSONB NOT NULL DEFAULT '{}'`, `created_at TIMESTAMPTZ`, `updated_at TIMESTAMPTZ`
  - [ ] `Get(ctx, userID)` executes `SELECT` by user_id, returns `nil, nil` (or similar sentinel) for not-found to allow service to return defaults
  - [ ] `Upsert(ctx, prefs)` executes `INSERT ... ON CONFLICT (user_id) DO UPDATE` with JSONB preferences and updated timestamps
  - [ ] All SQL uses parameterized queries (`$1`, `$2`) — no string interpolation
  - [ ] Preferences marshaled to/from JSONB via `encoding/json`
  - [ ] In-memory example adapter deleted

### T5: HTTP handlers - GET and PUT preferences with request/response types and unit tests (task-005)
- **Scope:** Implement `PreferencesHandler` with `Get` and `Upsert` methods. Define request types (`UpdatePreferencesRequest` with pointer fields for partial updates) and response type (`PreferencesResponse`). Map domain errors to HTTP errors. Include handler unit tests with mock service.
- **Files:**
  - Create `services/preferences-api/internal/api/handlers/preferences.go`
  - Create `services/preferences-api/internal/api/handlers/preferences_test.go`
  - Delete `services/preferences-api/internal/api/handlers/example.go`
  - Delete `services/preferences-api/internal/api/handlers/example_test.go`
- **Depends on:** T1 (domain types, errors), T3 (service interface for mocking)
- **Acceptance criteria:**
  - [ ] `Get` handler: extracts `user_id` from URL path, validates UUID format, calls service, returns `httpresponse.OK` with `PreferencesResponse`
  - [ ] `Get` handler: returns 400 for invalid UUID format
  - [ ] `Upsert` handler: extracts `user_id`, validates UUID, binds request body with `app.BindAndValidate()`, calls service, returns `httpresponse.OK`
  - [ ] `Upsert` handler: returns 400 for invalid UUID, invalid body, or domain validation errors
  - [ ] `PreferencesInput` uses pointer fields (`*string`, `*bool`) for partial update semantics
  - [ ] Domain errors (`ErrInvalidTheme`, `ErrInvalidLanguage`, `ErrInvalidDigest`) mapped to `httperror.BadRequest`
  - [ ] Unit tests cover: GET success, GET unknown user returns defaults, GET invalid UUID, PUT success full update, PUT partial update, PUT invalid UUID, PUT invalid theme, PUT empty body
  - [ ] Example handler files deleted

### T6: Wiring, routes, OpenAPI spec, and example scaffolding removal (task-006)
- **Scope:** Update `main.go` to connect to PostgreSQL, run migrations, wire the PostgreSQL adapter and preferences service. Update `routes.go` to register `GET` and `PUT /preferences/{user_id}`. Update `spec.go` with OpenAPI definitions for preferences endpoints. Remove all example route and spec definitions.
- **Files:**
  - Modify `services/preferences-api/cmd/server/main.go`
  - Modify `services/preferences-api/internal/api/routes.go`
  - Modify `services/preferences-api/internal/api/spec.go`
- **Depends on:** T1-T5 (all layers must exist for wiring)
- **Acceptance criteria:**
  - [ ] `main.go` connects to PostgreSQL via `database.Connect()` using `DATABASE_URL` from config
  - [ ] `main.go` runs migrations on startup via `database.MustRunMigrations()`
  - [ ] `main.go` creates PostgreSQL adapter, service, and handler with proper dependency injection
  - [ ] `routes.go` registers `GET /api/preferences-api/preferences/{user_id}` (public)
  - [ ] `routes.go` registers `PUT /api/preferences-api/preferences/{user_id}` (public, per spec — no auth for this feature)
  - [ ] Health endpoint preserved unchanged
  - [ ] All example routes and example-related code removed from `routes.go`
  - [ ] `spec.go` defines `UserPreferences`, `UpdatePreferencesRequest`, `PreferencesResponse` schemas
  - [ ] `spec.go` documents GET and PUT endpoints with path parameters, request bodies, and response types
  - [ ] All example OpenAPI definitions removed from `spec.go`
  - [ ] `go test -v ./...` passes from `services/preferences-api/`
