From 9a069d3f71339d44ba8eec55a236c723cab44caf Mon Sep 17 00:00:00 2001 From: rdev-worker Date: Mon, 9 Feb 2026 02:26:07 +0000 Subject: [PATCH] build: /breakdown-feature user-preferences --- .sdlc/features/user-preferences/manifest.yaml | 22 +++- .sdlc/features/user-preferences/tasks.md | 103 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 .sdlc/features/user-preferences/tasks.md diff --git a/.sdlc/features/user-preferences/manifest.yaml b/.sdlc/features/user-preferences/manifest.yaml index 4fd8deb..3043b76 100644 --- a/.sdlc/features/user-preferences/manifest.yaml +++ b/.sdlc/features/user-preferences/manifest.yaml @@ -32,5 +32,25 @@ artifacts: approved_by: user approved_at: 2026-02-09T02:18:15.533898188Z tasks: - status: pending + status: draft path: tasks.md + total: 6 +tasks: + - id: task-001 + title: Domain layer - preferences entity, validation, defaults, and errors + status: pending + - id: task-002 + title: Port layer - PreferencesRepository interface + status: pending + - id: task-003 + title: Service layer - PreferencesService with deep merge, get/update logic, and unit tests + status: pending + - id: task-004 + title: Database migration and PostgreSQL adapter + status: pending + - id: task-005 + title: HTTP handlers - GET and PUT preferences with request/response types and unit tests + status: pending + - id: task-006 + title: Wiring, routes, OpenAPI spec, and example scaffolding removal + status: pending diff --git a/.sdlc/features/user-preferences/tasks.md b/.sdlc/features/user-preferences/tasks.md new file mode 100644 index 0000000..95f469d --- /dev/null +++ b/.sdlc/features/user-preferences/tasks.md @@ -0,0 +1,103 @@ +# 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/`