9.1 KiB
9.1 KiB
Tasks: User Preferences API
Task Order (dependency sequence)
T1: Domain layer - preferences entity, validation, and errors
- Scope: Create
UserPreferencesstruct, preference key/value validation functions, and domain error definitions. ImplementValidatePreferences,ValidatePreferenceKey, andValidatePreferenceValuewith the closed key set (theme,language,notifications_enabled) and per-key value rules. - Files:
- Create
services/preferences-api/internal/domain/preferences.go - Replace
services/preferences-api/internal/domain/errors.go
- Create
- Depends on: None
- Acceptance criteria:
UserPreferencesstruct hasUserID,Preferences,CreatedAt,UpdatedAtfieldsValidatePreferencesrejects unknown keys withErrInvalidPreferenceKeyValidatePreferenceValuevalidatesthemeaccepts only"light"and"dark"ValidatePreferenceValuevalidateslanguagematches^[a-z]{2}$ValidatePreferenceValuevalidatesnotifications_enabledis a boolean- Error messages include the offending key/value for debuggability
ErrInvalidPreferenceKeyandErrInvalidPreferenceValuesentinel errors defined
T2: Port layer - PreferencesRepository interface
- Scope: Define the
PreferencesRepositoryinterface withGetandUpsertmethods. This is a thin interface file replacing the Example port. - Files:
- Create
services/preferences-api/internal/port/preferences.go
- Create
- Depends on: T1
- Acceptance criteria:
PreferencesRepositoryinterface defined withGet(ctx, userID) (*UserPreferences, error)andUpsert(ctx, userID, prefs) (*UserPreferences, error)- Uses domain types from
internal/domain - Context parameter on all methods for cancellation/timeout support
T3: Database migration and PostgreSQL adapter
- Scope: Create the SQL migration for the
user_preferencestable and implement the PostgreSQL repository adapter that satisfies thePreferencesRepositoryport. - Files:
- Create
services/preferences-api/migrations/001_create_user_preferences.sql - Create
services/preferences-api/internal/adapter/postgres/preferences.go
- Create
- Depends on: T2
- Acceptance criteria:
- Migration creates
user_preferencestable withuser_id UUID PRIMARY KEY,preferences JSONB NOT NULL DEFAULT '{}',created_at TIMESTAMPTZ,updated_at TIMESTAMPTZ - Migration uses
IF NOT EXISTSfor idempotency Getreturnsnil(not error) when no row found, so handler returns empty preferencesUpsertusesINSERT ... ON CONFLICT (user_id) DO UPDATEwith JSONB merge (preferences || $2)Upsertreturns the full merged row after upsert- All queries use parameterized statements (no SQL injection)
- Repository struct accepts
*sql.DBor pool via constructor
- Migration creates
T4: Service layer - PreferencesService with Get and Update
- Scope: Implement
PreferencesServicewithGetandUpdatemethods. Get retrieves preferences (returning empty object for new users). Update validates input via domain layer, then delegates to repository upsert. - Files:
- Create
services/preferences-api/internal/service/preferences.go
- Create
- Depends on: T1, T2
- Acceptance criteria:
Get(ctx, userID)returns*UserPreferences— empty preferences struct for new users (not nil, not error)Update(ctx, userID, prefs)callsdomain.ValidatePreferencesbefore persistingUpdatereturns the full merged preferences after upsert- Validation errors from domain layer propagate to caller unchanged
- Repository errors propagate to caller unchanged (will become 500s)
- Structured logging with user_id context on operations
T5: Service layer unit tests
- Scope: Write table-driven unit tests for
PreferencesServicewith a mock repository. Cover valid operations, validation failures, and repository errors. - Files:
- Create
services/preferences-api/internal/service/preferences_test.go
- Create
- Depends on: T4
- Acceptance criteria:
- Tests use mock repository implementing
port.PreferencesRepository - Test
Getfor existing user (returns preferences) and new user (returns empty) - Test
Updatewith valid preferences succeeds - Test
Updatewith unknown key returnsErrInvalidPreferenceKey - Test
Updatewith invalid theme value returnsErrInvalidPreferenceValue - Test
Updatewith invalid language format returnsErrInvalidPreferenceValue - Test
Updatewith non-booleannotifications_enabledreturnsErrInvalidPreferenceValue - Tests use
logging.Nop()for no-op logger - All tests pass with
go test -v ./internal/service/...
- Tests use mock repository implementing
T6: HTTP handlers - Get and Update preferences
- Scope: Implement
Preferenceshandler struct withGetandUpdatemethods. Include UUID validation on path params, ownership check against JWT user, request binding, domain error mapping, and response envelope formatting. - Files:
- Create
services/preferences-api/internal/api/handlers/preferences.go
- Create
- Depends on: T4
- Acceptance criteria:
Getextractsuser_idfromchi.URLParam, validates UUID formatGetchecks ownership viaauth.MustGetUser(ctx)comparisonGetreturns 200 with{data, meta}envelope viahttpresponse.OKUpdatebinds request withapp.BindAndValidateUpdatechecks ownership before calling serviceUpdatereturns 200 with full merged preferences- Invalid UUID returns 400 via
httperror.BadRequest - Ownership mismatch returns 403 via
httperror.Forbidden - Domain errors mapped:
ErrInvalidPreferenceKey→ 400,ErrInvalidPreferenceValue→ 400 - Unhandled errors bubble up as 500 via
app.Wrap PreferencesResponseDTO withuser_id,preferences,updated_at(nullable)
T7: Handler integration tests
- Scope: Write HTTP-level integration tests for the preferences handlers using
httptestand chi router. Mock the repository at the port layer. Test all status codes, response shapes, and error cases. - Files:
- Create
services/preferences-api/internal/api/handlers/preferences_test.go
- Create
- Depends on: T6
- Acceptance criteria:
- Test
GETreturns 200 with preferences for existing user - Test
GETreturns 200 with empty preferences for new user - Test
GETreturns 400 for invalid UUID - Test
PUTreturns 200 with merged preferences on success - Test
PUTreturns 400 for unknown preference keys - Test
PUTreturns 400 for invalid preference values - Test
PUTreturns 400 for missingpreferencesfield - All responses use
{data, meta}envelope structure - Tests use table-driven pattern with subtests
- All tests pass with
go test -v ./internal/api/handlers/...
- Test
T8: Routes, OpenAPI spec, and main.go wiring
- Scope: Update route registration to mount preferences endpoints under auth middleware, update OpenAPI spec with the two new endpoints, and wire the PostgreSQL adapter + service in
main.go(DB connect, migrations, shutdown hook). - 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
- Modify
- Depends on: T3, T6
- Acceptance criteria:
- Routes:
GET /api/preferences-api/preferences/{user_id}andPUT /api/preferences-api/preferences/{user_id}registered - Both preference routes wrapped in
auth.Middleware()group - URL parameters use
{user_id}brace syntax (not colon) - Handlers wrapped with
app.Wrap() - OpenAPI spec defines both endpoints with request/response schemas, security requirements, and error codes
main.goconnects to PostgreSQL viadatabase.MustConnect()withDatabaseConfigmain.goruns migrations viadatabase.MustRunMigrations()main.gocreatespostgres.PreferencesRepositoryandPreferencesServicemain.goregisters DB pool shutdown hook- Health endpoint remains functional
- Routes:
T9: Remove Example scaffold code
- Scope: Delete all Example scaffold files that have been replaced by preferences code. Ensure no references to Example types remain in routes, spec, main, or tests.
- Files:
- Delete
services/preferences-api/internal/domain/example.go - Delete
services/preferences-api/internal/port/example.go - Delete
services/preferences-api/internal/adapter/memory/example.go(andmemory/directory) - Delete
services/preferences-api/internal/service/example.go - Delete
services/preferences-api/internal/service/example_test.go - Delete
services/preferences-api/internal/api/handlers/example.go - Delete
services/preferences-api/internal/api/handlers/example_test.go
- Delete
- Depends on: T8
- Acceptance criteria:
- All Example files deleted
- No remaining imports of Example types in any file
- No remaining references to
/examplesroutes go build ./...succeeds with no compilation errorsgo test ./...passes with no failures- Health endpoint still works