134 lines
3.4 KiB
Go
134 lines
3.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"git.threesix.ai/jordan/slack5-1770606136/pkg/logging"
|
|
"git.threesix.ai/jordan/slack5-1770606136/services/preferences-api/internal/domain"
|
|
"git.threesix.ai/jordan/slack5-1770606136/services/preferences-api/internal/port"
|
|
)
|
|
|
|
// ValidationError carries per-field validation details.
|
|
// It wraps domain.ErrInvalidPreferenceValue for errors.Is() matching.
|
|
type ValidationError struct {
|
|
Details map[string]string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string { return "invalid preference values" }
|
|
func (e *ValidationError) Unwrap() error { return domain.ErrInvalidPreferenceValue }
|
|
|
|
// validThemes is the set of allowed values for the "theme" preference.
|
|
var validThemes = map[string]bool{
|
|
"light": true,
|
|
"dark": true,
|
|
"system": true,
|
|
}
|
|
|
|
// PreferenceService handles preference-related business logic.
|
|
type PreferenceService struct {
|
|
repo port.PreferenceRepository
|
|
logger *logging.Logger
|
|
}
|
|
|
|
// NewPreferenceService creates a new preference service.
|
|
func NewPreferenceService(repo port.PreferenceRepository, logger *logging.Logger) *PreferenceService {
|
|
return &PreferenceService{
|
|
repo: repo,
|
|
logger: logger.WithService("PreferenceService"),
|
|
}
|
|
}
|
|
|
|
// Get returns preferences for a user.
|
|
// Returns nil when no preferences exist.
|
|
func (s *PreferenceService) Get(ctx context.Context, userID string) (*domain.UserPreferences, error) {
|
|
return s.repo.Get(ctx, userID)
|
|
}
|
|
|
|
// UpsertInput contains the data needed to upsert preferences.
|
|
type UpsertInput struct {
|
|
UserID string
|
|
Preferences map[string]any
|
|
}
|
|
|
|
// Upsert validates and stores preferences for a user.
|
|
// Known keys are validated; unknown keys are accepted as-is.
|
|
// Incoming keys are merged on top of existing preferences.
|
|
func (s *PreferenceService) Upsert(ctx context.Context, input UpsertInput) (*domain.UserPreferences, error) {
|
|
// Validate known preference keys
|
|
if err := validatePreferences(input.Preferences); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Fetch existing preferences for merge
|
|
existing, err := s.repo.Get(ctx, input.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
merged := make(map[string]any)
|
|
if existing != nil {
|
|
for k, v := range existing.Preferences {
|
|
merged[k] = v
|
|
}
|
|
}
|
|
for k, v := range input.Preferences {
|
|
merged[k] = v
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
prefs := &domain.UserPreferences{
|
|
UserID: input.UserID,
|
|
Preferences: merged,
|
|
UpdatedAt: now,
|
|
}
|
|
if existing != nil {
|
|
prefs.CreatedAt = existing.CreatedAt
|
|
} else {
|
|
prefs.CreatedAt = now
|
|
}
|
|
|
|
if err := s.repo.Upsert(ctx, prefs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("preferences upserted", "user_id", input.UserID)
|
|
return prefs, nil
|
|
}
|
|
|
|
// validatePreferences checks known preference keys against their allowed values.
|
|
func validatePreferences(prefs map[string]any) error {
|
|
details := make(map[string]string)
|
|
|
|
for key, value := range prefs {
|
|
switch key {
|
|
case "theme":
|
|
s, ok := value.(string)
|
|
if !ok || !validThemes[s] {
|
|
details[key] = "must be one of: light, dark, system"
|
|
}
|
|
case "language":
|
|
s, ok := value.(string)
|
|
if !ok {
|
|
details[key] = "must be a valid BCP-47 language tag"
|
|
continue
|
|
}
|
|
if _, err := language.Parse(s); err != nil {
|
|
details[key] = fmt.Sprintf("must be a valid BCP-47 language tag")
|
|
}
|
|
case "notifications_enabled":
|
|
if _, ok := value.(bool); !ok {
|
|
details[key] = "must be a boolean"
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(details) > 0 {
|
|
return &ValidationError{Details: details}
|
|
}
|
|
return nil
|
|
}
|