120 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|