package user import ( "fmt" "strings" "time" "knowfoolery/backend/shared/types" "knowfoolery/backend/shared/errors" "knowfoolery/backend/services/user-service/internal/domain/valueobjects" ) // User represents a user aggregate type User struct { // Identity id types.UserID email string // Profile information profile *valueobjects.UserProfile playerName *valueobjects.PlayerName preferences *valueobjects.UserPreferences // Authentication data authProvider string // "zitadel", "google", "facebook", etc. externalID string // ID from external auth provider emailVerified bool // Account status status types.UserStatus isActive bool isBanned bool banReason *string bannedUntil *types.Timestamp // Statistics statistics *valueobjects.UserStatistics // Lifecycle createdAt types.Timestamp updatedAt types.Timestamp lastLoginAt *types.Timestamp // Security roles []types.UserRole permissions []string // Privacy settings privacySettings *valueobjects.PrivacySettings // Version control version int } // NewUser creates a new user func NewUser( email string, authProvider string, externalID string, ) (*User, error) { // Validate inputs if err := validateEmail(email); err != nil { return nil, fmt.Errorf("invalid email: %w", err) } if authProvider == "" { return nil, errors.ErrValidationFailed("auth_provider", "auth provider cannot be empty") } if externalID == "" { return nil, errors.ErrValidationFailed("external_id", "external ID cannot be empty") } now := types.NewTimestamp() // Create default profile profile, err := valueobjects.NewUserProfile("", "", nil, nil) if err != nil { return nil, fmt.Errorf("failed to create default profile: %w", err) } // Create default preferences preferences := valueobjects.NewDefaultUserPreferences() // Create default privacy settings privacySettings := valueobjects.NewDefaultPrivacySettings() // Create default statistics statistics := valueobjects.NewUserStatistics() user := &User{ id: types.NewUserID(), email: strings.ToLower(strings.TrimSpace(email)), profile: profile, playerName: nil, // Will be set later preferences: preferences, authProvider: authProvider, externalID: externalID, emailVerified: false, status: types.UserStatusActive, isActive: true, isBanned: false, banReason: nil, bannedUntil: nil, statistics: statistics, createdAt: now, updatedAt: now, lastLoginAt: nil, roles: []types.UserRole{types.UserRolePlayer}, // Default role permissions: []string{}, privacySettings: privacySettings, version: 1, } return user, nil } // NewUserWithID creates a user with a specific ID (for loading from persistence) func NewUserWithID( id types.UserID, email string, authProvider string, externalID string, createdAt types.Timestamp, ) (*User, error) { if id.IsEmpty() { return nil, errors.ErrValidationFailed("id", "user ID cannot be empty") } user, err := NewUser(email, authProvider, externalID) if err != nil { return nil, err } user.id = id user.createdAt = createdAt user.updatedAt = createdAt return user, nil } // Getters // ID returns the user's unique identifier func (u *User) ID() types.UserID { return u.id } // Email returns the user's email address func (u *User) Email() string { return u.email } // Profile returns the user's profile func (u *User) Profile() *valueobjects.UserProfile { return u.profile } // PlayerName returns the user's player name func (u *User) PlayerName() *valueobjects.PlayerName { return u.playerName } // Preferences returns the user's preferences func (u *User) Preferences() *valueobjects.UserPreferences { return u.preferences } // AuthProvider returns the authentication provider func (u *User) AuthProvider() string { return u.authProvider } // ExternalID returns the external ID from auth provider func (u *User) ExternalID() string { return u.externalID } // IsEmailVerified returns true if email is verified func (u *User) IsEmailVerified() bool { return u.emailVerified } // Status returns the user status func (u *User) Status() types.UserStatus { return u.status } // IsActive returns true if the user is active func (u *User) IsActive() bool { return u.isActive && u.status == types.UserStatusActive && !u.isBanned } // IsBanned returns true if the user is banned func (u *User) IsBanned() bool { return u.isBanned } // BanReason returns the ban reason if banned func (u *User) BanReason() *string { return u.banReason } // BannedUntil returns when the ban expires func (u *User) BannedUntil() *types.Timestamp { return u.bannedUntil } // Statistics returns the user's statistics func (u *User) Statistics() *valueobjects.UserStatistics { return u.statistics } // CreatedAt returns the creation timestamp func (u *User) CreatedAt() types.Timestamp { return u.createdAt } // UpdatedAt returns the last update timestamp func (u *User) UpdatedAt() types.Timestamp { return u.updatedAt } // LastLoginAt returns the last login timestamp func (u *User) LastLoginAt() *types.Timestamp { return u.lastLoginAt } // Roles returns the user's roles func (u *User) Roles() []types.UserRole { return append([]types.UserRole(nil), u.roles...) // Return copy } // Permissions returns the user's permissions func (u *User) Permissions() []string { return append([]string(nil), u.permissions...) // Return copy } // PrivacySettings returns the user's privacy settings func (u *User) PrivacySettings() *valueobjects.PrivacySettings { return u.privacySettings } // Version returns the current version func (u *User) Version() int { return u.version } // Business methods // UpdateProfile updates the user's profile func (u *User) UpdateProfile( firstName string, lastName string, dateOfBirth *types.Date, avatar *string, ) error { newProfile, err := valueobjects.NewUserProfile(firstName, lastName, dateOfBirth, avatar) if err != nil { return fmt.Errorf("failed to update profile: %w", err) } u.profile = newProfile u.markUpdated() return nil } // SetPlayerName sets the user's player name func (u *User) SetPlayerName(playerName string) error { if playerName == "" { u.playerName = nil u.markUpdated() return nil } newPlayerName, err := valueobjects.NewPlayerName(playerName) if err != nil { return fmt.Errorf("failed to set player name: %w", err) } u.playerName = newPlayerName u.markUpdated() return nil } // UpdateEmail updates the user's email address func (u *User) UpdateEmail(newEmail string) error { if err := validateEmail(newEmail); err != nil { return fmt.Errorf("invalid email: %w", err) } normalizedEmail := strings.ToLower(strings.TrimSpace(newEmail)) if normalizedEmail == u.email { return nil // No change } u.email = normalizedEmail u.emailVerified = false // Reset verification when email changes u.markUpdated() return nil } // VerifyEmail marks the email as verified func (u *User) VerifyEmail() { if !u.emailVerified { u.emailVerified = true u.markUpdated() } } // UpdatePreferences updates the user's preferences func (u *User) UpdatePreferences(preferences *valueobjects.UserPreferences) error { if preferences == nil { return errors.ErrValidationFailed("preferences", "preferences cannot be nil") } if err := preferences.Validate(); err != nil { return fmt.Errorf("invalid preferences: %w", err) } u.preferences = preferences u.markUpdated() return nil } // UpdatePrivacySettings updates the user's privacy settings func (u *User) UpdatePrivacySettings(settings *valueobjects.PrivacySettings) error { if settings == nil { return errors.ErrValidationFailed("privacy_settings", "privacy settings cannot be nil") } if err := settings.Validate(); err != nil { return fmt.Errorf("invalid privacy settings: %w", err) } u.privacySettings = settings u.markUpdated() return nil } // RecordLogin records a successful login func (u *User) RecordLogin() error { if !u.IsActive() { return errors.ErrOperationNotAllowed("record login", "user is not active") } now := types.NewTimestamp() u.lastLoginAt = &now // Update statistics u.statistics.RecordLogin() u.markUpdated() return nil } // Ban bans the user func (u *User) Ban(reason string, bannedBy types.UserID, duration *types.Duration) error { if u.isBanned { return errors.ErrOperationNotAllowed("ban user", "user is already banned") } if reason == "" { return errors.ErrValidationFailed("reason", "ban reason cannot be empty") } if bannedBy.IsEmpty() { return errors.ErrValidationFailed("banned_by", "banned by user ID cannot be empty") } u.isBanned = true u.banReason = &reason // Set ban expiration if duration provided if duration != nil { bannedUntil := types.NewTimestamp().Add(*duration) u.bannedUntil = &bannedUntil } u.status = types.UserStatusBanned u.markUpdated() return nil } // Unban removes the ban from the user func (u *User) Unban() error { if !u.isBanned { return errors.ErrOperationNotAllowed("unban user", "user is not banned") } u.isBanned = false u.banReason = nil u.bannedUntil = nil u.status = types.UserStatusActive u.markUpdated() return nil } // CheckBanExpiration checks if a temporary ban has expired func (u *User) CheckBanExpiration() bool { if !u.isBanned || u.bannedUntil == nil { return false } now := types.NewTimestamp() if now.After(*u.bannedUntil) { // Ban has expired, unban the user u.Unban() return true } return false } // Deactivate deactivates the user account func (u *User) Deactivate() error { if !u.isActive { return errors.ErrOperationNotAllowed("deactivate user", "user is already inactive") } u.isActive = false u.status = types.UserStatusInactive u.markUpdated() return nil } // Reactivate reactivates the user account func (u *User) Reactivate() error { if u.isActive { return errors.ErrOperationNotAllowed("reactivate user", "user is already active") } if u.isBanned { return errors.ErrOperationNotAllowed("reactivate user", "cannot reactivate banned user") } u.isActive = true u.status = types.UserStatusActive u.markUpdated() return nil } // AddRole adds a role to the user func (u *User) AddRole(role types.UserRole) error { // Check if role already exists for _, existingRole := range u.roles { if existingRole == role { return nil // Already has role } } u.roles = append(u.roles, role) u.markUpdated() return nil } // RemoveRole removes a role from the user func (u *User) RemoveRole(role types.UserRole) error { // Don't allow removal of Player role (everyone must have it) if role == types.UserRolePlayer { return errors.ErrOperationNotAllowed("remove role", "cannot remove Player role") } for i, existingRole := range u.roles { if existingRole == role { u.roles = append(u.roles[:i], u.roles[i+1:]...) u.markUpdated() return nil } } return errors.ErrNotFound("role", string(role)) } // AddPermission adds a permission to the user func (u *User) AddPermission(permission string) error { permission = strings.TrimSpace(permission) if permission == "" { return errors.ErrValidationFailed("permission", "permission cannot be empty") } // Check if permission already exists for _, existingPermission := range u.permissions { if existingPermission == permission { return nil // Already has permission } } u.permissions = append(u.permissions, permission) u.markUpdated() return nil } // RemovePermission removes a permission from the user func (u *User) RemovePermission(permission string) error { for i, existingPermission := range u.permissions { if existingPermission == permission { u.permissions = append(u.permissions[:i], u.permissions[i+1:]...) u.markUpdated() return nil } } return errors.ErrNotFound("permission", permission) } // UpdateStatistics updates user statistics func (u *User) UpdateStatistics(stats *valueobjects.UserStatistics) error { if stats == nil { return errors.ErrValidationFailed("statistics", "statistics cannot be nil") } if err := stats.Validate(); err != nil { return fmt.Errorf("invalid statistics: %w", err) } u.statistics = stats u.markUpdated() return nil } // Query methods // HasRole checks if the user has a specific role func (u *User) HasRole(role types.UserRole) bool { for _, userRole := range u.roles { if userRole == role { return true } } return false } // HasPermission checks if the user has a specific permission func (u *User) HasPermission(permission string) bool { for _, userPermission := range u.permissions { if userPermission == permission { return true } } return false } // IsAdmin returns true if the user is an admin func (u *User) IsAdmin() bool { return u.HasRole(types.UserRoleAdmin) } // IsModerator returns true if the user is a moderator func (u *User) IsModerator() bool { return u.HasRole(types.UserRoleModerator) } // CanPlay returns true if the user can play games func (u *User) CanPlay() bool { return u.IsActive() && !u.isBanned } // GetDisplayName returns the best display name for the user func (u *User) GetDisplayName() string { // Prefer player name if u.playerName != nil && u.playerName.Value() != "" { return u.playerName.Value() } // Fall back to profile name if u.profile != nil { fullName := strings.TrimSpace(u.profile.FirstName() + " " + u.profile.LastName()) if fullName != "" { return fullName } if u.profile.FirstName() != "" { return u.profile.FirstName() } } // Fall back to email prefix parts := strings.Split(u.email, "@") if len(parts) > 0 && parts[0] != "" { return parts[0] } // Last resort return "User" } // GetAge returns the user's age if date of birth is available func (u *User) GetAge() *int { if u.profile == nil || u.profile.DateOfBirth() == nil { return nil } now := time.Now() birthDate := time.Time(*u.profile.DateOfBirth()) age := now.Year() - birthDate.Year() if now.YearDay() < birthDate.YearDay() { age-- } return &age } // GetAccountAge returns how long the user has been registered func (u *User) GetAccountAge() time.Duration { now := types.NewTimestamp() return now.Sub(u.createdAt) } // IsNewUser returns true if the user registered recently func (u *User) IsNewUser() bool { age := u.GetAccountAge() return age < 7*24*time.Hour // Less than 7 days old } // Validation // Validate performs comprehensive validation func (u *User) Validate() *types.ValidationResult { result := types.NewValidationResult() // Validate ID if u.id.IsEmpty() { result.AddError("user ID cannot be empty") } // Validate email if err := validateEmail(u.email); err != nil { result.AddErrorf("invalid email: %v", err) } // Validate auth provider if u.authProvider == "" { result.AddError("auth provider cannot be empty") } // Validate external ID if u.externalID == "" { result.AddError("external ID cannot be empty") } // Validate profile if u.profile != nil { if profileResult := u.profile.Validate(); !profileResult.IsValid() { for _, error := range profileResult.Errors() { result.AddErrorf("profile validation failed: %s", error) } } } // Validate player name if u.playerName != nil { if playerNameResult := u.playerName.Validate(); !playerNameResult.IsValid() { for _, error := range playerNameResult.Errors() { result.AddErrorf("player name validation failed: %s", error) } } } // Validate preferences if u.preferences != nil { if prefResult := u.preferences.Validate(); !prefResult.IsValid() { for _, error := range prefResult.Errors() { result.AddErrorf("preferences validation failed: %s", error) } } } // Validate privacy settings if u.privacySettings != nil { if privacyResult := u.privacySettings.Validate(); !privacyResult.IsValid() { for _, error := range privacyResult.Errors() { result.AddErrorf("privacy settings validation failed: %s", error) } } } // Validate statistics if u.statistics != nil { if statsResult := u.statistics.Validate(); !statsResult.IsValid() { for _, error := range statsResult.Errors() { result.AddErrorf("statistics validation failed: %s", error) } } } // Validate roles if len(u.roles) == 0 { result.AddError("user must have at least one role") } // Check for Player role hasPlayerRole := false for _, role := range u.roles { if role == types.UserRolePlayer { hasPlayerRole = true break } } if !hasPlayerRole { result.AddError("user must have Player role") } // Validate ban logic if u.isBanned && u.banReason == nil { result.AddError("banned user must have ban reason") } if u.banReason != nil && !u.isBanned { result.AddError("user has ban reason but is not marked as banned") } return result } // Helper methods // markUpdated updates the timestamp and version func (u *User) markUpdated() { u.updatedAt = types.NewTimestamp() u.version++ } // ToSnapshot creates a read-only snapshot of the user func (u *User) ToSnapshot() *UserSnapshot { var playerName *string if u.playerName != nil { name := u.playerName.Value() playerName = &name } return &UserSnapshot{ ID: u.id, Email: u.email, Profile: u.profile.ToSnapshot(), PlayerName: playerName, Preferences: u.preferences.ToSnapshot(), AuthProvider: u.authProvider, ExternalID: u.externalID, EmailVerified: u.emailVerified, Status: u.status, IsActive: u.isActive, IsBanned: u.isBanned, BanReason: u.banReason, BannedUntil: u.bannedUntil, Statistics: u.statistics.ToSnapshot(), CreatedAt: u.createdAt, UpdatedAt: u.updatedAt, LastLoginAt: u.lastLoginAt, Roles: append([]types.UserRole(nil), u.roles...), Permissions: append([]string(nil), u.permissions...), PrivacySettings: u.privacySettings.ToSnapshot(), Version: u.version, } } // UserSnapshot represents a read-only view of a user type UserSnapshot struct { ID types.UserID `json:"id"` Email string `json:"email"` Profile *valueobjects.UserProfileSnapshot `json:"profile"` PlayerName *string `json:"player_name,omitempty"` Preferences *valueobjects.UserPreferencesSnapshot `json:"preferences"` AuthProvider string `json:"auth_provider"` ExternalID string `json:"external_id"` EmailVerified bool `json:"email_verified"` Status types.UserStatus `json:"status"` IsActive bool `json:"is_active"` IsBanned bool `json:"is_banned"` BanReason *string `json:"ban_reason,omitempty"` BannedUntil *types.Timestamp `json:"banned_until,omitempty"` Statistics *valueobjects.UserStatisticsSnapshot `json:"statistics"` CreatedAt types.Timestamp `json:"created_at"` UpdatedAt types.Timestamp `json:"updated_at"` LastLoginAt *types.Timestamp `json:"last_login_at,omitempty"` Roles []types.UserRole `json:"roles"` Permissions []string `json:"permissions"` PrivacySettings *valueobjects.PrivacySettingsSnapshot `json:"privacy_settings"` Version int `json:"version"` } // GetDisplayName returns the display name for the snapshot func (us *UserSnapshot) GetDisplayName() string { if us.PlayerName != nil && *us.PlayerName != "" { return *us.PlayerName } if us.Profile != nil { fullName := strings.TrimSpace(us.Profile.FirstName + " " + us.Profile.LastName) if fullName != "" { return fullName } if us.Profile.FirstName != "" { return us.Profile.FirstName } } parts := strings.Split(us.Email, "@") if len(parts) > 0 && parts[0] != "" { return parts[0] } return "User" } // Validation helper functions func validateEmail(email string) error { email = strings.TrimSpace(email) if email == "" { return errors.ErrValidationFailed("email", "email cannot be empty") } if len(email) > types.MaxEmailLength { return errors.ErrValidationFailed("email", fmt.Sprintf("email too long (max %d characters)", types.MaxEmailLength)) } // Basic email format validation if !strings.Contains(email, "@") { return errors.ErrValidationFailed("email", "email must contain @ symbol") } parts := strings.Split(email, "@") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return errors.ErrValidationFailed("email", "invalid email format") } // More comprehensive email validation would go here // For production, use a proper email validation library return nil }