build: /breakdown-feature user-preferences
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
100b3c4035
commit
9db9b8bbb6
@ -25,5 +25,31 @@ artifacts:
|
||||
status: draft
|
||||
path: spec.md
|
||||
tasks:
|
||||
status: pending
|
||||
status: draft
|
||||
path: tasks.md
|
||||
total: 8
|
||||
tasks:
|
||||
- id: task-001
|
||||
title: Domain layer - preferences types, validation, defaults, and errors
|
||||
status: pending
|
||||
- id: task-002
|
||||
title: Port layer - PreferencesRepository interface
|
||||
status: pending
|
||||
- id: task-003
|
||||
title: Database migration - user_preferences table
|
||||
status: pending
|
||||
- id: task-004
|
||||
title: PostgreSQL adapter - PreferencesRepository implementation
|
||||
status: pending
|
||||
- id: task-005
|
||||
title: Service layer - PreferencesService with get/set logic and tests
|
||||
status: pending
|
||||
- id: task-006
|
||||
title: HTTP handlers - GET and PUT with auth, mapping, and tests
|
||||
status: pending
|
||||
- id: task-007
|
||||
title: Routes, OpenAPI spec, and main.go wiring
|
||||
status: pending
|
||||
- id: task-008
|
||||
title: Remove example scaffold code
|
||||
status: pending
|
||||
|
||||
150
.sdlc/features/user-preferences/tasks.md
Normal file
150
.sdlc/features/user-preferences/tasks.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Tasks: User Preferences API
|
||||
|
||||
## Task Order (dependency sequence)
|
||||
|
||||
### T1: Domain layer - preferences types, validation, defaults, and errors
|
||||
- **Scope:** Create the pure domain model for user preferences. Define `UserID`, `NotificationPreferences`, `Preferences` (with custom JSON marshal/unmarshal for unknown key preservation via `Extra` map), and `UserPreferences` types. Implement `DefaultPreferences()` as the single source of truth for defaults. Implement `Preferences.Validate()` with theme enum and language length checks. Add domain errors: `ErrInvalidTheme`, `ErrInvalidLanguage`, `ErrForbidden`, `ErrPreferencesNotFound`.
|
||||
- **Files:**
|
||||
- Create: `services/preferences-api/internal/domain/preferences.go`
|
||||
- Modify: `services/preferences-api/internal/domain/errors.go`
|
||||
- **Depends on:** None
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `UserPreferences`, `Preferences`, `NotificationPreferences`, `UserID` types defined
|
||||
- [ ] `DefaultPreferences()` returns theme=`"system"`, language=`"en"`, notifications email=true, push=true, sms=false
|
||||
- [ ] `Validate()` rejects theme values not in `["light", "dark", "system"]` with `ErrInvalidTheme`
|
||||
- [ ] `Validate()` allows empty theme (treated as valid, will use default)
|
||||
- [ ] `Validate()` rejects language longer than 10 runes with `ErrInvalidLanguage`
|
||||
- [ ] Custom `MarshalJSON`/`UnmarshalJSON` on `Preferences` preserves unknown keys via `Extra` map
|
||||
- [ ] `ErrInvalidTheme`, `ErrInvalidLanguage`, `ErrForbidden`, `ErrPreferencesNotFound` defined in errors.go
|
||||
- [ ] No external dependencies (pure domain)
|
||||
|
||||
### T2: Port layer - PreferencesRepository interface
|
||||
- **Scope:** Define the `PreferencesRepository` interface with `Get` and `Upsert` methods. This is the contract between the service layer and the persistence adapter.
|
||||
- **Files:**
|
||||
- Create: `services/preferences-api/internal/port/preferences.go`
|
||||
- **Depends on:** T1
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `PreferencesRepository` interface defined with `Get(ctx, userID) (*domain.UserPreferences, error)` and `Upsert(ctx, prefs *domain.UserPreferences) error`
|
||||
- [ ] Uses `domain.UserID` and `domain.UserPreferences` types from T1
|
||||
- [ ] No implementation, interface only
|
||||
|
||||
### T3: Database migration - user_preferences table
|
||||
- **Scope:** Create the SQL migration file for the `user_preferences` table with `user_id TEXT PRIMARY KEY`, `preferences JSONB NOT NULL`, `created_at TIMESTAMPTZ`, and `updated_at TIMESTAMPTZ`. Set up the `//go:embed` migration embedding in a migrations package.
|
||||
- **Files:**
|
||||
- Create: `services/preferences-api/migrations/001_create_user_preferences.sql`
|
||||
- Create or modify: `services/preferences-api/migrations/migrations.go` (embed directive)
|
||||
- **Depends on:** None
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Migration creates `user_preferences` table with `IF NOT EXISTS`
|
||||
- [ ] `user_id TEXT PRIMARY KEY` column
|
||||
- [ ] `preferences JSONB NOT NULL` column
|
||||
- [ ] `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` column
|
||||
- [ ] `updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` column
|
||||
- [ ] Migration file embedded via `//go:embed` per project convention
|
||||
|
||||
### T4: PostgreSQL adapter - PreferencesRepository implementation
|
||||
- **Scope:** Implement the `postgres.PreferencesRepository` struct that satisfies the `port.PreferencesRepository` interface. `Get` queries by `user_id` and returns `domain.ErrPreferencesNotFound` when no row exists. `Upsert` uses `INSERT ... ON CONFLICT (user_id) DO UPDATE SET` for idempotent writes. JSON marshaling between `domain.Preferences` and the JSONB column.
|
||||
- **Files:**
|
||||
- Create: `services/preferences-api/internal/adapter/postgres/preferences.go`
|
||||
- **Depends on:** T1, T2, T3
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `postgres.PreferencesRepository` struct with `*sqlx.DB` and logger
|
||||
- [ ] `NewPreferencesRepository(db, logger)` constructor
|
||||
- [ ] `Get` returns `domain.ErrPreferencesNotFound` when no row found (`sql.ErrNoRows`)
|
||||
- [ ] `Get` unmarshals JSONB into `domain.Preferences` correctly
|
||||
- [ ] `Upsert` uses parameterized `INSERT ... ON CONFLICT DO UPDATE` query
|
||||
- [ ] `Upsert` marshals `domain.Preferences` to JSON for JSONB storage
|
||||
- [ ] All queries use parameterized statements (no SQL injection)
|
||||
|
||||
### T5: Service layer - PreferencesService with get/set logic and tests
|
||||
- **Scope:** Implement `PreferencesService` with `GetPreferences` and `SetPreferences` methods. `GetPreferences` returns defaults when the repo returns `ErrPreferencesNotFound`. `SetPreferences` validates via domain, builds the entity, and upserts. Write 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`
|
||||
- **Depends on:** T1, T2
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `PreferencesService` struct with `port.PreferencesRepository` and logger
|
||||
- [ ] `NewPreferencesService(repo, logger)` constructor
|
||||
- [ ] `GetPreferences` returns stored preferences when they exist
|
||||
- [ ] `GetPreferences` returns `DefaultPreferences()` with given `userID` when repo returns `ErrPreferencesNotFound`
|
||||
- [ ] `SetPreferences` calls `Validate()` on input and returns domain errors for invalid data
|
||||
- [ ] `SetPreferences` calls `repo.Upsert()` and returns the saved entity
|
||||
- [ ] Unit tests use a mock `PreferencesRepository`
|
||||
- [ ] Tests cover: get with existing prefs, get with defaults, set valid prefs, set with invalid theme, set with invalid language
|
||||
- [ ] Tests use `logging.Nop()` for logger
|
||||
|
||||
### T6: HTTP handlers - GET and PUT with auth, mapping, and tests
|
||||
- **Scope:** Implement the `Preferences` handler struct with `Get` and `Put` methods. Both extract `user_id` from URL params, perform authorization check (authenticated user matches path `user_id`), and delegate to the service. Request/response DTO types for JSON serialization. `mapDomainError()` function to convert domain errors to `httperror.*`. Handler unit tests with mocked service.
|
||||
- **Files:**
|
||||
- Create: `services/preferences-api/internal/api/handlers/preferences.go`
|
||||
- Create: `services/preferences-api/internal/api/handlers/preferences_test.go`
|
||||
- **Depends on:** T1, T5
|
||||
- **Acceptance criteria:**
|
||||
- [ ] `Preferences` handler struct with service and logger
|
||||
- [ ] `NewPreferences(svc, logger)` constructor
|
||||
- [ ] `Get` handler: extracts `user_id` via `chi.URLParam`, checks auth via `auth.GetUser`, returns 403 if user mismatch, returns preferences via `httpresponse.OK`
|
||||
- [ ] `Put` handler: extracts `user_id`, checks auth, binds request via `app.BindAndValidate`, delegates to service, returns saved preferences via `httpresponse.OK`
|
||||
- [ ] Request DTOs: `PutPreferencesRequest`, `PreferencesPayload`, `NotificationPreferencesPayload`
|
||||
- [ ] Response DTO: `PreferencesResponse` with `user_id`, `preferences`, `updated_at`
|
||||
- [ ] `mapDomainError()` maps `ErrInvalidTheme` and `ErrInvalidLanguage` to `httperror.BadRequest`
|
||||
- [ ] `toResponse()` and `toDomain()` conversion functions
|
||||
- [ ] Tests cover: GET success, GET defaults, GET forbidden, PUT success, PUT validation error, PUT forbidden
|
||||
|
||||
### T7: Routes, OpenAPI spec, and main.go wiring
|
||||
- **Scope:** Update `routes.go` to register authenticated preference routes (`GET /{user_id}`, `PUT /{user_id}`) under `/api/preferences-api/preferences` with `auth.Middleware()`. Update `spec.go` with preference schemas and endpoint documentation. Update `main.go` to connect to PostgreSQL via `database.Connect()`, run migrations, create the Postgres adapter, wire the service, and register cleanup on shutdown.
|
||||
- **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:** T3, T4, T5, T6
|
||||
- **Acceptance criteria:**
|
||||
- [ ] Routes registered: `GET /api/preferences-api/preferences/{user_id}` and `PUT /api/preferences-api/preferences/{user_id}`
|
||||
- [ ] Auth middleware applied to preference route group (conditional on `AuthEnabled`)
|
||||
- [ ] URL parameters use brace syntax `{user_id}`
|
||||
- [ ] OpenAPI spec defines `Preferences`, `NotificationPreferences`, `UserPreferencesResponse`, `PutPreferencesRequest` schemas
|
||||
- [ ] OpenAPI spec documents both endpoints with auth requirements and error responses
|
||||
- [ ] `main.go` connects to PostgreSQL with `database.Connect()`
|
||||
- [ ] `main.go` runs migrations via `database.MustRunMigrations()`
|
||||
- [ ] `main.go` creates postgres repo, preferences service, and passes to route registration
|
||||
- [ ] `main.go` registers `pool.Close()` for graceful shutdown
|
||||
- [ ] Health check route preserved at `/api/preferences-api/health`
|
||||
|
||||
### T8: Remove example scaffold code
|
||||
- **Scope:** Delete all example/scaffold files that are replaced by the preferences implementation. This is the final cleanup task.
|
||||
- **Files:**
|
||||
- Delete: `services/preferences-api/internal/domain/example.go`
|
||||
- Delete: `services/preferences-api/internal/port/example.go`
|
||||
- Delete: `services/preferences-api/internal/service/example.go`
|
||||
- Delete: `services/preferences-api/internal/service/example_test.go`
|
||||
- Delete: `services/preferences-api/internal/adapter/memory/example.go`
|
||||
- Delete: `services/preferences-api/internal/api/handlers/example.go`
|
||||
- Delete: `services/preferences-api/internal/api/handlers/example_test.go`
|
||||
- **Depends on:** T7 (all new code is in place before deleting old code)
|
||||
- **Acceptance criteria:**
|
||||
- [ ] All 7 example files deleted
|
||||
- [ ] `internal/adapter/memory/` directory removed (no longer needed)
|
||||
- [ ] No remaining references to `Example`, `ExampleService`, `ExampleRepository` in the codebase
|
||||
- [ ] Service compiles cleanly (`go build ./...`)
|
||||
- [ ] All tests pass (`go test ./...`)
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
T1 (Domain) ──┬──▶ T2 (Port) ──┬──▶ T5 (Service) ──┬──▶ T6 (Handlers) ──┐
|
||||
│ │ │ │
|
||||
│ └──▶ T4 (Adapter) ──┐│ │
|
||||
│ ││ │
|
||||
T3 (Migration)─────────────────────▶ T4 ───────────┘│ │
|
||||
│ │
|
||||
└──▶ T7 (Wiring) ───┤
|
||||
│
|
||||
T8 (Cleanup) ◀──┘
|
||||
```
|
||||
|
||||
**Longest dependency chain:** T1 → T2 → T5 → T6 → T7 → T8 (depth: 6)
|
||||
|
||||
**Parallelization opportunities:**
|
||||
- T1 and T3 can be done in parallel (no dependencies)
|
||||
- T2 can start immediately after T1
|
||||
- T4 can start after T1 + T2 + T3
|
||||
- T5 can start after T1 + T2 (doesn't need T3 or T4)
|
||||
Loading…
Reference in New Issue
Block a user