9.7 KiB
9.7 KiB
Tasks: User Preferences API
Task Order (dependency sequence)
T1: Domain layer - preference types, validation, defaults, and errors
- ID: task-001
- Scope: Create the pure domain model for user preferences. Define
UserPreferencesstruct with typed fields (Theme,Language,NotificationPreferences), enum types (Theme,DigestFrequency) with constants, aDefaultPreferences()factory, aValidate()method with domain-level validation, and domain error sentinels (ErrInvalidTheme,ErrInvalidLanguage,ErrInvalidDigest). - Files:
services/preferences-api/internal/domain/preferences.go(create)services/preferences-api/internal/domain/errors.go(replace contents)
- Depends on: None
- Acceptance criteria:
UserPreferencesstruct has fields: UserID, Theme, Language, Notifications, UpdatedAtThemetype with constants:ThemeLight,ThemeDark,ThemeSystemDigestFrequencytype with constants:DigestNone,DigestDaily,DigestWeeklyNotificationPreferencesstruct with Email (bool), Push (bool), Digest (DigestFrequency)DefaultPreferences(userID)returns correct defaults (theme=system, language=en, email=true, push=true, digest=weekly)Validate()returnsErrInvalidThemefor invalid theme valuesValidate()returnsErrInvalidLanguagefor invalid language values (only en, fr, es, de, ja)Validate()returnsErrInvalidDigestfor invalid digest values- Domain errors defined as package-level sentinel errors
- No external dependencies (pure Go, no framework imports)
T2: Port layer - PreferenceRepository interface
- ID: task-002
- Scope: Define the
PreferenceRepositoryport interface withGet(ctx, userID)andUpsert(ctx, prefs)methods. This is the hexagonal architecture boundary between the service layer and storage adapters. - Files:
services/preferences-api/internal/port/preferences.go(create)
- Depends on: T1
- Acceptance criteria:
PreferenceRepositoryinterface defined withGet(ctx context.Context, userID string) (*domain.UserPreferences, error)methodPreferenceRepositoryinterface includesUpsert(ctx context.Context, prefs *domain.UserPreferences) errormethodGetdocuments that it returnsnil, nilwhen no preferences exist (service applies defaults)- Interface uses domain types only (no adapter-specific types)
T3: Adapter layer - in-memory PreferenceRepository for tests
- ID: task-003
- Scope: Implement a thread-safe in-memory
PreferenceRepositoryadapter for use in unit and handler tests. Uses amap[string]*domain.UserPreferenceswithsync.RWMutex. - Files:
services/preferences-api/internal/adapter/memory/preferences.go(create)
- Depends on: T1, T2
- Acceptance criteria:
- Implements
port.PreferenceRepositoryinterface (compile-time verification) Get()returnsnil, nilfor non-existent user (not an error)Get()returns a defensive copy (mutation-safe)Upsert()stores preferences, overwriting any existing entry- Thread-safe via
sync.RWMutex - Constructor
NewPreferenceRepository()initializes empty map
- Implements
T4: Adapter layer - PostgreSQL PreferenceRepository with schema creation
- ID: task-004
- Scope: Implement the PostgreSQL adapter for
PreferenceRepository. IncludesEnsureSchema()for idempotent table creation viaCREATE TABLE IF NOT EXISTS,Get()with single-row PK lookup, andUpsert()withINSERT ... ON CONFLICT UPDATE. Usesdatabase/sqlwith parameterized queries. - Files:
services/preferences-api/internal/adapter/postgres/preferences.go(create)
- Depends on: T1, T2
- Acceptance criteria:
- Implements
port.PreferenceRepositoryinterface (compile-time verification) - Constructor accepts
*sql.DBand callsEnsureSchema()to create theuser_preferencestable EnsureSchema()usesCREATE TABLE IF NOT EXISTSwith correct column types (TEXT PK, TEXT, BOOLEAN, TIMESTAMPTZ)Get()returnsnil, nilwhensql.ErrNoRows(not an error, service applies defaults)Get()maps flat columns back todomain.UserPreferencesstructUpsert()usesINSERT ... ON CONFLICT (user_id) DO UPDATE SET ...for atomic upsert- All queries use parameterized placeholders (
$1,$2, ...) - no SQL interpolation updated_atset toNOW()on upsert
- Implements
T5: Service layer - PreferenceService with business logic and tests
- ID: task-005
- Scope: Implement
PreferenceServicewithGetPreferences(ctx, userID)andUpdatePreferences(ctx, userID, prefs). GET applies defaults when repository returns nil. UPDATE validates via domainValidate()before persisting. Write comprehensive unit tests using the in-memory adapter. - Files:
services/preferences-api/internal/service/preferences.go(create)services/preferences-api/internal/service/preferences_test.go(create)
- Depends on: T1, T2, T3
- Acceptance criteria:
GetPreferences()returns default preferences when repository returns nilGetPreferences()returns stored preferences when they existUpdatePreferences()callsValidate()and returns domain errors on invalid inputUpdatePreferences()sets UserID and UpdatedAt before upsertingUpdatePreferences()delegates to repositoryUpsert()after validation- Constructor accepts
port.PreferenceRepositoryand*logging.Logger - Unit tests cover: get defaults, get existing, update valid, update invalid theme/language/digest
- Tests use
logging.Nop()for no-op logger
T6: Handler layer - GET and PUT preference handlers with tests
- ID: task-006
- Scope: Implement
PreferenceHandlerwithGet(w, r) errorandUpdate(w, r) error. GET extracts{user_id}from URL, checks authorization (self-access or admin read), delegates to service, returns response envelope. PUT extracts user_id, checks self-only auth, binds/validates request body strictly, delegates to service. Write handler tests using httptest with the in-memory adapter. - Files:
services/preferences-api/internal/api/handlers/preferences.go(create)services/preferences-api/internal/api/handlers/preferences_test.go(create)
- Depends on: T1, T2, T3, T5
- Acceptance criteria:
Get()extractsuser_idfrom chi URL param using brace syntaxGet()returns 403 Forbidden when JWT user ID != URL user_id (and not admin)Get()allows admin role to read any user's preferencesGet()returns 200 with{data, meta}envelope viahttpresponse.OK()Update()returns 403 for any non-self access (even admin)Update()uses strict binding to reject unknown JSON fields with 400Update()returns 400 with validation errors for invalid valuesUpdate()returns 200 with updated preferences on success- Request DTOs use struct validation tags (
oneof=light dark system, etc.) - Response DTO maps domain types to JSON representation
- Domain errors mapped to appropriate HTTP errors (400, 403)
- Tests cover: self-access GET, admin GET, forbidden GET, self PUT, forbidden PUT, invalid body, unknown fields, valid update
T7: Routes, OpenAPI spec, and main.go wiring
- ID: task-007
- Scope: Update
routes.goto register preference endpoints (GET and PUT under auth middleware). Updatespec.goto document preference endpoints with schemas, examples, and error responses. Updatemain.goto wire PostgreSQL adapter (viaDatabaseConfig), createPreferenceService, and pass to route registration. - Files:
services/preferences-api/internal/api/routes.go(modify)services/preferences-api/internal/api/spec.go(modify)services/preferences-api/cmd/server/main.go(modify)services/preferences-api/internal/config/config.go(modify if needed)
- Depends on: T4, T5, T6
- Acceptance criteria:
GET /api/preferences-api/preferences/{user_id}registered with auth middlewarePUT /api/preferences-api/preferences/{user_id}registered with auth middleware- Health endpoint preserved at
/api/preferences-api/health - OpenAPI spec documents both preference endpoints with request/response schemas
- OpenAPI spec includes
UserPreferencesschema,UpdatePreferencesRequestschema - OpenAPI spec includes error responses (400, 401, 403)
main.goopens PostgreSQL connection viasql.OpenwithDatabaseConfig.DSN()main.gocreates PostgreSQL adapter, PreferenceService, passes to RegisterRoutes- Docs mounted via
application.EnableDocs(spec)
T8: Remove example scaffold and verify clean build
- ID: task-008
- Scope: Delete all example scaffold files that have been replaced by preference equivalents. Verify the service compiles, all tests pass, and no dead code remains.
- Files:
services/preferences-api/internal/domain/example.go(delete)services/preferences-api/internal/port/example.go(delete)services/preferences-api/internal/adapter/memory/example.go(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: T7
- Acceptance criteria:
- All example files deleted (7 files listed above)
- No remaining imports of example types in any file
go build ./...succeeds with zero errorsgo test ./...passes all testsgo vet ./...reports no issues- No dead code or unused imports remain