You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

679 lines
19 KiB
Go

package valueobjects
import (
"fmt"
"knowfoolery/backend/shared/types"
"knowfoolery/backend/shared/errors"
)
// UserPreferences represents a user's preferences and settings
type UserPreferences struct {
// Game preferences
preferredDifficulty types.DifficultyLevel
preferredThemes []types.ThemeID
enableHints bool
enableTimer bool
enableSound bool
enableNotifications bool
// Display preferences
theme string // "light", "dark", "auto"
language string // ISO 639-1 language code
timezone string // IANA timezone
// Privacy preferences
showProfile bool
showStatistics bool
showLeaderboard bool
allowFriendRequests bool
// Notification preferences
emailNotifications *EmailNotificationSettings
pushNotifications *PushNotificationSettings
// Accessibility preferences
accessibility *AccessibilitySettings
// Update tracking
updatedAt types.Timestamp
}
// EmailNotificationSettings represents email notification preferences
type EmailNotificationSettings struct {
Enabled bool `json:"enabled"`
GameInvitations bool `json:"game_invitations"`
LeaderboardUpdates bool `json:"leaderboard_updates"`
WeeklyDigest bool `json:"weekly_digest"`
SecurityAlerts bool `json:"security_alerts"`
ProductUpdates bool `json:"product_updates"`
}
// PushNotificationSettings represents push notification preferences
type PushNotificationSettings struct {
Enabled bool `json:"enabled"`
GameInvitations bool `json:"game_invitations"`
TurnReminders bool `json:"turn_reminders"`
LeaderboardUpdates bool `json:"leaderboard_updates"`
DailyReminders bool `json:"daily_reminders"`
}
// AccessibilitySettings represents accessibility preferences
type AccessibilitySettings struct {
HighContrast bool `json:"high_contrast"`
LargeText bool `json:"large_text"`
ReducedMotion bool `json:"reduced_motion"`
ScreenReaderSupport bool `json:"screen_reader_support"`
KeyboardNavigation bool `json:"keyboard_navigation"`
FontSize float64 `json:"font_size"` // 0.8 - 2.0 multiplier
}
// NewDefaultUserPreferences creates default user preferences
func NewDefaultUserPreferences() *UserPreferences {
return &UserPreferences{
preferredDifficulty: types.DifficultyMedium,
preferredThemes: []types.ThemeID{},
enableHints: true,
enableTimer: true,
enableSound: true,
enableNotifications: true,
theme: "auto",
language: "en",
timezone: "UTC",
showProfile: true,
showStatistics: true,
showLeaderboard: true,
allowFriendRequests: true,
emailNotifications: &EmailNotificationSettings{
Enabled: true,
GameInvitations: true,
LeaderboardUpdates: false,
WeeklyDigest: true,
SecurityAlerts: true,
ProductUpdates: false,
},
pushNotifications: &PushNotificationSettings{
Enabled: true,
GameInvitations: true,
TurnReminders: true,
LeaderboardUpdates: false,
DailyReminders: false,
},
accessibility: &AccessibilitySettings{
HighContrast: false,
LargeText: false,
ReducedMotion: false,
ScreenReaderSupport: false,
KeyboardNavigation: false,
FontSize: 1.0,
},
updatedAt: types.NewTimestamp(),
}
}
// NewUserPreferences creates user preferences with specified values
func NewUserPreferences(
preferredDifficulty types.DifficultyLevel,
preferredThemes []types.ThemeID,
enableHints bool,
enableTimer bool,
) (*UserPreferences, error) {
preferences := NewDefaultUserPreferences()
// Validate and set preferred difficulty
if err := preferences.SetPreferredDifficulty(preferredDifficulty); err != nil {
return nil, err
}
// Set preferred themes
if err := preferences.SetPreferredThemes(preferredThemes); err != nil {
return nil, err
}
preferences.enableHints = enableHints
preferences.enableTimer = enableTimer
return preferences, nil
}
// Getters
// PreferredDifficulty returns the preferred difficulty level
func (p *UserPreferences) PreferredDifficulty() types.DifficultyLevel {
return p.preferredDifficulty
}
// PreferredThemes returns the preferred themes
func (p *UserPreferences) PreferredThemes() []types.ThemeID {
return append([]types.ThemeID(nil), p.preferredThemes...) // Return copy
}
// EnableHints returns true if hints are enabled
func (p *UserPreferences) EnableHints() bool {
return p.enableHints
}
// EnableTimer returns true if timer is enabled
func (p *UserPreferences) EnableTimer() bool {
return p.enableTimer
}
// EnableSound returns true if sound is enabled
func (p *UserPreferences) EnableSound() bool {
return p.enableSound
}
// EnableNotifications returns true if notifications are enabled
func (p *UserPreferences) EnableNotifications() bool {
return p.enableNotifications
}
// Theme returns the display theme
func (p *UserPreferences) Theme() string {
return p.theme
}
// Language returns the language code
func (p *UserPreferences) Language() string {
return p.language
}
// Timezone returns the timezone
func (p *UserPreferences) Timezone() string {
return p.timezone
}
// ShowProfile returns true if profile should be shown
func (p *UserPreferences) ShowProfile() bool {
return p.showProfile
}
// ShowStatistics returns true if statistics should be shown
func (p *UserPreferences) ShowStatistics() bool {
return p.showStatistics
}
// ShowLeaderboard returns true if leaderboard should be shown
func (p *UserPreferences) ShowLeaderboard() bool {
return p.showLeaderboard
}
// AllowFriendRequests returns true if friend requests are allowed
func (p *UserPreferences) AllowFriendRequests() bool {
return p.allowFriendRequests
}
// EmailNotifications returns email notification settings
func (p *UserPreferences) EmailNotifications() *EmailNotificationSettings {
if p.emailNotifications == nil {
return nil
}
// Return copy
return &EmailNotificationSettings{
Enabled: p.emailNotifications.Enabled,
GameInvitations: p.emailNotifications.GameInvitations,
LeaderboardUpdates: p.emailNotifications.LeaderboardUpdates,
WeeklyDigest: p.emailNotifications.WeeklyDigest,
SecurityAlerts: p.emailNotifications.SecurityAlerts,
ProductUpdates: p.emailNotifications.ProductUpdates,
}
}
// PushNotifications returns push notification settings
func (p *UserPreferences) PushNotifications() *PushNotificationSettings {
if p.pushNotifications == nil {
return nil
}
// Return copy
return &PushNotificationSettings{
Enabled: p.pushNotifications.Enabled,
GameInvitations: p.pushNotifications.GameInvitations,
TurnReminders: p.pushNotifications.TurnReminders,
LeaderboardUpdates: p.pushNotifications.LeaderboardUpdates,
DailyReminders: p.pushNotifications.DailyReminders,
}
}
// Accessibility returns accessibility settings
func (p *UserPreferences) Accessibility() *AccessibilitySettings {
if p.accessibility == nil {
return nil
}
// Return copy
return &AccessibilitySettings{
HighContrast: p.accessibility.HighContrast,
LargeText: p.accessibility.LargeText,
ReducedMotion: p.accessibility.ReducedMotion,
ScreenReaderSupport: p.accessibility.ScreenReaderSupport,
KeyboardNavigation: p.accessibility.KeyboardNavigation,
FontSize: p.accessibility.FontSize,
}
}
// UpdatedAt returns the last update timestamp
func (p *UserPreferences) UpdatedAt() types.Timestamp {
return p.updatedAt
}
// Business methods
// SetPreferredDifficulty sets the preferred difficulty level
func (p *UserPreferences) SetPreferredDifficulty(difficulty types.DifficultyLevel) error {
// Validate difficulty level
validDifficulties := []types.DifficultyLevel{
types.DifficultyEasy,
types.DifficultyMedium,
types.DifficultyHard,
types.DifficultyExpert,
types.DifficultyImpossible,
}
isValid := false
for _, valid := range validDifficulties {
if difficulty == valid {
isValid = true
break
}
}
if !isValid {
return errors.ErrValidationFailed("preferred_difficulty", "invalid difficulty level")
}
p.preferredDifficulty = difficulty
p.markUpdated()
return nil
}
// SetPreferredThemes sets the preferred themes
func (p *UserPreferences) SetPreferredThemes(themes []types.ThemeID) error {
if len(themes) > types.MaxPreferredThemes {
return errors.ErrValidationFailed("preferred_themes",
fmt.Sprintf("too many preferred themes (max %d)", types.MaxPreferredThemes))
}
// Validate theme IDs
for i, themeID := range themes {
if themeID.IsEmpty() {
return errors.ErrValidationFailed("preferred_themes",
fmt.Sprintf("theme ID at index %d is empty", i))
}
}
// Remove duplicates
uniqueThemes := make([]types.ThemeID, 0, len(themes))
themeSet := make(map[types.ThemeID]bool)
for _, themeID := range themes {
if !themeSet[themeID] {
uniqueThemes = append(uniqueThemes, themeID)
themeSet[themeID] = true
}
}
p.preferredThemes = uniqueThemes
p.markUpdated()
return nil
}
// AddPreferredTheme adds a theme to preferred themes
func (p *UserPreferences) AddPreferredTheme(themeID types.ThemeID) error {
if themeID.IsEmpty() {
return errors.ErrValidationFailed("theme_id", "theme ID cannot be empty")
}
// Check if already in preferred themes
for _, existing := range p.preferredThemes {
if existing == themeID {
return nil // Already exists
}
}
if len(p.preferredThemes) >= types.MaxPreferredThemes {
return errors.ErrValidationFailed("preferred_themes",
fmt.Sprintf("too many preferred themes (max %d)", types.MaxPreferredThemes))
}
p.preferredThemes = append(p.preferredThemes, themeID)
p.markUpdated()
return nil
}
// RemovePreferredTheme removes a theme from preferred themes
func (p *UserPreferences) RemovePreferredTheme(themeID types.ThemeID) error {
for i, existing := range p.preferredThemes {
if existing == themeID {
p.preferredThemes = append(p.preferredThemes[:i], p.preferredThemes[i+1:]...)
p.markUpdated()
return nil
}
}
return errors.ErrNotFound("preferred_theme", string(themeID))
}
// SetGamePreferences sets game-related preferences
func (p *UserPreferences) SetGamePreferences(
enableHints bool,
enableTimer bool,
enableSound bool,
enableNotifications bool,
) {
p.enableHints = enableHints
p.enableTimer = enableTimer
p.enableSound = enableSound
p.enableNotifications = enableNotifications
p.markUpdated()
}
// SetDisplayPreferences sets display-related preferences
func (p *UserPreferences) SetDisplayPreferences(
theme string,
language string,
timezone string,
) error {
if err := p.SetTheme(theme); err != nil {
return err
}
if err := p.SetLanguage(language); err != nil {
return err
}
if err := p.SetTimezone(timezone); err != nil {
return err
}
return nil
}
// SetTheme sets the display theme
func (p *UserPreferences) SetTheme(theme string) error {
validThemes := []string{"light", "dark", "auto"}
isValid := false
for _, valid := range validThemes {
if theme == valid {
isValid = true
break
}
}
if !isValid {
return errors.ErrValidationFailed("theme", "theme must be 'light', 'dark', or 'auto'")
}
p.theme = theme
p.markUpdated()
return nil
}
// SetLanguage sets the language
func (p *UserPreferences) SetLanguage(language string) error {
if len(language) != 2 {
return errors.ErrValidationFailed("language", "language must be a 2-character ISO 639-1 code")
}
// Could validate against known language codes
p.language = language
p.markUpdated()
return nil
}
// SetTimezone sets the timezone
func (p *UserPreferences) SetTimezone(timezone string) error {
// Basic validation - could be more comprehensive
if timezone == "" {
timezone = "UTC"
}
p.timezone = timezone
p.markUpdated()
return nil
}
// SetPrivacyPreferences sets privacy-related preferences
func (p *UserPreferences) SetPrivacyPreferences(
showProfile bool,
showStatistics bool,
showLeaderboard bool,
allowFriendRequests bool,
) {
p.showProfile = showProfile
p.showStatistics = showStatistics
p.showLeaderboard = showLeaderboard
p.allowFriendRequests = allowFriendRequests
p.markUpdated()
}
// UpdateEmailNotifications updates email notification settings
func (p *UserPreferences) UpdateEmailNotifications(settings *EmailNotificationSettings) error {
if settings == nil {
return errors.ErrValidationFailed("email_notifications", "email notification settings cannot be nil")
}
p.emailNotifications = &EmailNotificationSettings{
Enabled: settings.Enabled,
GameInvitations: settings.GameInvitations,
LeaderboardUpdates: settings.LeaderboardUpdates,
WeeklyDigest: settings.WeeklyDigest,
SecurityAlerts: settings.SecurityAlerts,
ProductUpdates: settings.ProductUpdates,
}
p.markUpdated()
return nil
}
// UpdatePushNotifications updates push notification settings
func (p *UserPreferences) UpdatePushNotifications(settings *PushNotificationSettings) error {
if settings == nil {
return errors.ErrValidationFailed("push_notifications", "push notification settings cannot be nil")
}
p.pushNotifications = &PushNotificationSettings{
Enabled: settings.Enabled,
GameInvitations: settings.GameInvitations,
TurnReminders: settings.TurnReminders,
LeaderboardUpdates: settings.LeaderboardUpdates,
DailyReminders: settings.DailyReminders,
}
p.markUpdated()
return nil
}
// UpdateAccessibility updates accessibility settings
func (p *UserPreferences) UpdateAccessibility(settings *AccessibilitySettings) error {
if settings == nil {
return errors.ErrValidationFailed("accessibility", "accessibility settings cannot be nil")
}
// Validate font size
if settings.FontSize < 0.5 || settings.FontSize > 3.0 {
return errors.ErrValidationFailed("font_size", "font size must be between 0.5 and 3.0")
}
p.accessibility = &AccessibilitySettings{
HighContrast: settings.HighContrast,
LargeText: settings.LargeText,
ReducedMotion: settings.ReducedMotion,
ScreenReaderSupport: settings.ScreenReaderSupport,
KeyboardNavigation: settings.KeyboardNavigation,
FontSize: settings.FontSize,
}
p.markUpdated()
return nil
}
// Query methods
// HasPreferredTheme checks if a theme is in preferred themes
func (p *UserPreferences) HasPreferredTheme(themeID types.ThemeID) bool {
for _, preferred := range p.preferredThemes {
if preferred == themeID {
return true
}
}
return false
}
// IsAccessibilityEnabled returns true if any accessibility features are enabled
func (p *UserPreferences) IsAccessibilityEnabled() bool {
if p.accessibility == nil {
return false
}
return p.accessibility.HighContrast ||
p.accessibility.LargeText ||
p.accessibility.ReducedMotion ||
p.accessibility.ScreenReaderSupport ||
p.accessibility.KeyboardNavigation ||
p.accessibility.FontSize != 1.0
}
// AreEmailNotificationsEnabled returns true if email notifications are globally enabled
func (p *UserPreferences) AreEmailNotificationsEnabled() bool {
return p.emailNotifications != nil && p.emailNotifications.Enabled
}
// ArePushNotificationsEnabled returns true if push notifications are globally enabled
func (p *UserPreferences) ArePushNotificationsEnabled() bool {
return p.pushNotifications != nil && p.pushNotifications.Enabled
}
// Validation
// Validate performs comprehensive validation
func (p *UserPreferences) Validate() *types.ValidationResult {
result := types.NewValidationResult()
// Validate preferred difficulty
validDifficulties := []types.DifficultyLevel{
types.DifficultyEasy,
types.DifficultyMedium,
types.DifficultyHard,
types.DifficultyExpert,
types.DifficultyImpossible,
}
isValidDifficulty := false
for _, valid := range validDifficulties {
if p.preferredDifficulty == valid {
isValidDifficulty = true
break
}
}
if !isValidDifficulty {
result.AddError("invalid preferred difficulty")
}
// Validate preferred themes count
if len(p.preferredThemes) > types.MaxPreferredThemes {
result.AddErrorf("too many preferred themes (max %d)", types.MaxPreferredThemes)
}
// Validate theme IDs
for i, themeID := range p.preferredThemes {
if themeID.IsEmpty() {
result.AddErrorf("preferred theme at index %d is empty", i)
}
}
// Validate theme
validThemes := []string{"light", "dark", "auto"}
isValidTheme := false
for _, valid := range validThemes {
if p.theme == valid {
isValidTheme = true
break
}
}
if !isValidTheme {
result.AddError("invalid theme")
}
// Validate language
if len(p.language) != 2 {
result.AddError("invalid language code")
}
// Validate accessibility settings
if p.accessibility != nil {
if p.accessibility.FontSize < 0.5 || p.accessibility.FontSize > 3.0 {
result.AddError("font size must be between 0.5 and 3.0")
}
}
return result
}
// Helper methods
// markUpdated updates the timestamp
func (p *UserPreferences) markUpdated() {
p.updatedAt = types.NewTimestamp()
}
// ToSnapshot creates a read-only snapshot
func (p *UserPreferences) ToSnapshot() *UserPreferencesSnapshot {
return &UserPreferencesSnapshot{
PreferredDifficulty: p.preferredDifficulty,
PreferredThemes: append([]types.ThemeID(nil), p.preferredThemes...),
EnableHints: p.enableHints,
EnableTimer: p.enableTimer,
EnableSound: p.enableSound,
EnableNotifications: p.enableNotifications,
Theme: p.theme,
Language: p.language,
Timezone: p.timezone,
ShowProfile: p.showProfile,
ShowStatistics: p.showStatistics,
ShowLeaderboard: p.showLeaderboard,
AllowFriendRequests: p.allowFriendRequests,
EmailNotifications: p.EmailNotifications(),
PushNotifications: p.PushNotifications(),
Accessibility: p.Accessibility(),
UpdatedAt: p.updatedAt,
}
}
// UserPreferencesSnapshot represents a read-only view of user preferences
type UserPreferencesSnapshot struct {
PreferredDifficulty types.DifficultyLevel `json:"preferred_difficulty"`
PreferredThemes []types.ThemeID `json:"preferred_themes"`
EnableHints bool `json:"enable_hints"`
EnableTimer bool `json:"enable_timer"`
EnableSound bool `json:"enable_sound"`
EnableNotifications bool `json:"enable_notifications"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
ShowProfile bool `json:"show_profile"`
ShowStatistics bool `json:"show_statistics"`
ShowLeaderboard bool `json:"show_leaderboard"`
AllowFriendRequests bool `json:"allow_friend_requests"`
EmailNotifications *EmailNotificationSettings `json:"email_notifications"`
PushNotifications *PushNotificationSettings `json:"push_notifications"`
Accessibility *AccessibilitySettings `json:"accessibility"`
UpdatedAt types.Timestamp `json:"updated_at"`
}
// HasPreferredTheme checks if a theme is preferred in the snapshot
func (ps *UserPreferencesSnapshot) HasPreferredTheme(themeID types.ThemeID) bool {
for _, preferred := range ps.PreferredThemes {
if preferred == themeID {
return true
}
}
return false
}