slack5-1770529463/services/preferences-api/internal/service/preference.go
rdev-worker 73532902e7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature user-preferences
2026-02-08 06:13:10 +00:00

120 lines
3.1 KiB
Go

package service
import (
"context"
"fmt"
"git.threesix.ai/jordan/slack5-1770529463/pkg/logging"
"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/domain"
"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/port"
)
// PreferencesResult is the result of a preference operation.
type PreferencesResult struct {
UserID string
Preferences map[string]any
}
// PreferenceService handles preference 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"),
}
}
// GetPreferences returns all preferences for a user, merged with defaults.
func (s *PreferenceService) GetPreferences(ctx context.Context, userID string) (*PreferencesResult, error) {
if err := domain.ValidateUserID(userID); err != nil {
return nil, err
}
rows, err := s.repo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
stored := make(map[domain.PreferenceKey]string, len(rows))
for _, row := range rows {
stored[domain.PreferenceKey(row.Key)] = row.Value
}
merged := domain.MergeWithDefaults(stored)
serialized := domain.SerializeForResponse(merged)
return &PreferencesResult{
UserID: userID,
Preferences: serialized,
}, nil
}
// UpdatePreferences validates and upserts the provided preferences, then returns the full merged result.
func (s *PreferenceService) UpdatePreferences(ctx context.Context, userID string, input map[string]any) (*PreferencesResult, error) {
if err := domain.ValidateUserID(userID); err != nil {
return nil, err
}
// Validate and serialize each input key-value pair
toUpsert := make(map[domain.PreferenceKey]string, len(input))
for key, val := range input {
if err := domain.ValidateKey(key); err != nil {
return nil, err
}
// Serialize value to string
strVal, err := serializeValue(val)
if err != nil {
return nil, fmt.Errorf("%w: %s must have a valid value", domain.ErrInvalidPreferenceValue, key)
}
pk := domain.PreferenceKey(key)
if err := domain.ValidateValue(pk, strVal); err != nil {
return nil, err
}
toUpsert[pk] = strVal
}
// Upsert each preference
for key, value := range toUpsert {
if err := s.repo.Upsert(ctx, userID, string(key), value); err != nil {
return nil, err
}
}
s.logger.Info("preferences updated", "user_id", userID, "keys_updated", len(toUpsert))
// Fetch and return full merged preferences
return s.GetPreferences(ctx, userID)
}
// serializeValue converts an any value to a string representation.
func serializeValue(val any) (string, error) {
switch v := val.(type) {
case string:
return v, nil
case bool:
if v {
return "true", nil
}
return "false", nil
case float64:
// JSON numbers are decoded as float64
if v == 1 {
return "true", nil
}
if v == 0 {
return "false", nil
}
return "", fmt.Errorf("unsupported numeric value")
default:
return "", fmt.Errorf("unsupported value type %T", val)
}
}