slack5-1770603014/services/preferences-api/internal/service/preferences.go
rdev-worker e5fc44d10e
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-09 02:37:38 +00:00

115 lines
3.0 KiB
Go

// Package service provides business logic / use cases for the application.
// Services orchestrate domain operations using port interfaces.
package service
import (
"context"
"time"
"git.threesix.ai/jordan/slack5-1770603014/pkg/logging"
"git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/domain"
"git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/port"
)
// PreferencesService handles preferences-related business logic.
type PreferencesService struct {
repo port.PreferencesRepository
logger *logging.Logger
}
// NewPreferencesService creates a new preferences service.
func NewPreferencesService(repo port.PreferencesRepository, logger *logging.Logger) *PreferencesService {
return &PreferencesService{
repo: repo,
logger: logger.WithService("PreferencesService"),
}
}
// GetPreferences returns preferences for a user. Returns defaults if none are stored.
func (s *PreferencesService) GetPreferences(ctx context.Context, userID domain.UserID) (*domain.UserPreferences, error) {
prefs, err := s.repo.Get(ctx, userID)
if err != nil {
return nil, err
}
if prefs != nil {
return prefs, nil
}
// Return defaults for unknown users
return &domain.UserPreferences{
UserID: userID,
Preferences: domain.DefaultPreferences(),
UpdatedAt: time.Now().UTC(),
}, nil
}
// UpdateInput contains the partial preference data for an update.
// Pointer fields distinguish "not provided" from zero values.
type UpdateInput struct {
Theme *string
Language *string
Notifications *NotificationsInput
}
// NotificationsInput contains partial notification settings.
type NotificationsInput struct {
Email *bool
Push *bool
Digest *string
}
// UpdatePreferences merges partial input with existing preferences and persists.
func (s *PreferencesService) UpdatePreferences(ctx context.Context, userID domain.UserID, input UpdateInput) (*domain.UserPreferences, error) {
// Fetch existing or start from defaults
existing, err := s.repo.Get(ctx, userID)
if err != nil {
return nil, err
}
var merged domain.Preferences
if existing != nil {
merged = existing.Preferences
} else {
merged = domain.DefaultPreferences()
}
// Deep merge incoming fields
if input.Theme != nil {
merged.Theme = *input.Theme
}
if input.Language != nil {
merged.Language = *input.Language
}
if input.Notifications != nil {
if input.Notifications.Email != nil {
merged.Notifications.Email = *input.Notifications.Email
}
if input.Notifications.Push != nil {
merged.Notifications.Push = *input.Notifications.Push
}
if input.Notifications.Digest != nil {
merged.Notifications.Digest = *input.Notifications.Digest
}
}
// Validate merged result
if err := merged.Validate(); err != nil {
return nil, err
}
now := time.Now().UTC()
prefs := &domain.UserPreferences{
UserID: userID,
Preferences: merged,
UpdatedAt: now,
}
if err := s.repo.Upsert(ctx, prefs); err != nil {
return nil, err
}
s.logger.Info("preferences updated", "user_id", userID)
return prefs, nil
}