package errors import ( "fmt" "knowfoolery/backend/shared/types" ) // DomainError represents a base error type for all domain errors type DomainError struct { Code string Message string Cause error } // Error implements the error interface func (e *DomainError) Error() string { if e.Cause != nil { return fmt.Sprintf("%s: %s (caused by: %v)", e.Code, e.Message, e.Cause) } return fmt.Sprintf("%s: %s", e.Code, e.Message) } // Unwrap returns the underlying cause func (e *DomainError) Unwrap() error { return e.Cause } // NewDomainError creates a new domain error func NewDomainError(code, message string, cause error) *DomainError { return &DomainError{ Code: code, Message: message, Cause: cause, } } // Error codes for different domain errors const ( // User errors ErrCodeInvalidPlayerName = "INVALID_PLAYER_NAME" ErrCodeUserNotFound = "USER_NOT_FOUND" ErrCodeUserAlreadyExists = "USER_ALREADY_EXISTS" // Game Session errors ErrCodeSessionNotFound = "SESSION_NOT_FOUND" ErrCodeSessionAlreadyExists = "SESSION_ALREADY_EXISTS" ErrCodeSessionExpired = "SESSION_EXPIRED" ErrCodeSessionNotActive = "SESSION_NOT_ACTIVE" ErrCodeInvalidSessionTransition = "INVALID_SESSION_TRANSITION" ErrCodeSessionTimedOut = "SESSION_TIMED_OUT" ErrCodeUserAlreadyHasActiveSession = "USER_ALREADY_HAS_ACTIVE_SESSION" // Attempt errors ErrCodeMaxAttemptsExceeded = "MAX_ATTEMPTS_EXCEEDED" ErrCodeInvalidAttempt = "INVALID_ATTEMPT" ErrCodeAttemptTooFast = "ATTEMPT_TOO_FAST" // Question errors ErrCodeQuestionNotFound = "QUESTION_NOT_FOUND" ErrCodeQuestionAlreadyAnswered = "QUESTION_ALREADY_ANSWERED" ErrCodeInvalidQuestion = "INVALID_QUESTION" ErrCodeNoQuestionsAvailable = "NO_QUESTIONS_AVAILABLE" // Theme errors ErrCodeThemeNotFound = "THEME_NOT_FOUND" ErrCodeInvalidTheme = "INVALID_THEME" // Answer errors ErrCodeInvalidAnswer = "INVALID_ANSWER" ErrCodeEmptyAnswer = "EMPTY_ANSWER" // Hint errors ErrCodeHintNotAvailable = "HINT_NOT_AVAILABLE" ErrCodeHintAlreadyUsed = "HINT_ALREADY_USED" // Scoring errors ErrCodeInvalidScore = "INVALID_SCORE" ErrCodeScoringFailed = "SCORING_FAILED" // Validation errors ErrCodeValidationFailed = "VALIDATION_FAILED" ErrCodeBusinessRuleViolation = "BUSINESS_RULE_VIOLATION" // General errors ErrCodeOperationNotAllowed = "OPERATION_NOT_ALLOWED" ErrCodeInternalError = "INTERNAL_ERROR" // Leaderboard errors ErrCodeInvalidLeaderboardID = "INVALID_LEADERBOARD_ID" ErrCodeLeaderboardNotFound = "LEADERBOARD_NOT_FOUND" ErrCodeLeaderboardAlreadyExists = "LEADERBOARD_ALREADY_EXISTS" ErrCodeLeaderboardFrozen = "LEADERBOARD_FROZEN" ErrCodeLeaderboardArchived = "LEADERBOARD_ARCHIVED" ErrCodeInvalidRank = "INVALID_RANK" ErrCodePlayerNotFound = "PLAYER_NOT_FOUND" ErrCodeInvalidResetFrequency = "INVALID_RESET_FREQUENCY" ErrCodeInvalidResetAction = "INVALID_RESET_ACTION" ErrCodeInvalidResetInterval = "INVALID_RESET_INTERVAL" ErrCodeInvalidTimezone = "INVALID_TIMEZONE" ErrCodeInvalidPreserveCount = "INVALID_PRESERVE_COUNT" ErrCodeInvalidResetTime = "INVALID_RESET_TIME" ErrCodeInvalidNotificationTime = "INVALID_NOTIFICATION_TIME" ErrCodeInvalidDisplayFormat = "INVALID_DISPLAY_FORMAT" ErrCodeInvalidSortOrder = "INVALID_SORT_ORDER" ErrCodeInvalidRankDisplay = "INVALID_RANK_DISPLAY" ErrCodeInvalidEntriesPerPage = "INVALID_ENTRIES_PER_PAGE" ErrCodeInvalidHighlightCount = "INVALID_HIGHLIGHT_COUNT" ErrCodeInvalidColorKey = "INVALID_COLOR_KEY" ErrCodeInvalidColorValue = "INVALID_COLOR_VALUE" ErrCodeInvalidLabelKey = "INVALID_LABEL_KEY" ErrCodeInvalidLabelValue = "INVALID_LABEL_VALUE" ErrCodeInvalidMetadata = "INVALID_METADATA" ErrCodeInvalidGameCount = "INVALID_GAME_COUNT" ErrCodeInvalidPlayerCount = "INVALID_PLAYER_COUNT" ErrCodeInvalidStreak = "INVALID_STREAK" ErrCodeInvalidPlayerID = "INVALID_PLAYER_ID" ErrCodeInvalidSessionID = "INVALID_SESSION_ID" ErrCodeInvalidRankRange = "INVALID_RANK_RANGE" ErrCodeInvalidTopCount = "INVALID_TOP_COUNT" ErrCodeInvalidContext = "INVALID_CONTEXT" ErrCodeInvalidCompetitionName = "INVALID_COMPETITION_NAME" ErrCodeInvalidCompetitionStatus = "INVALID_COMPETITION_STATUS" ErrCodeCompetitionNotYetStarted = "COMPETITION_NOT_YET_STARTED" ErrCodeCompetitionNotActive = "COMPETITION_NOT_ACTIVE" ErrCodeCompetitionAlreadyCompleted = "COMPETITION_ALREADY_COMPLETED" ErrCodePlayerNotQualified = "PLAYER_NOT_QUALIFIED" ErrCodeCompetitionFull = "COMPETITION_FULL" ErrCodeScoreBelowMinimum = "SCORE_BELOW_MINIMUM" ErrCodeTooManyEntriesPerPlayer = "TOO_MANY_ENTRIES_PER_PLAYER" ErrCodeEntryNotFound = "ENTRY_NOT_FOUND" ErrCodeInvalidStartTime = "INVALID_START_TIME" ErrCodeInvalidEndTime = "INVALID_END_TIME" ) // User-related errors // ErrInvalidPlayerName indicates the player name doesn't meet validation requirements func ErrInvalidPlayerName(name string, reason string) *DomainError { return NewDomainError( ErrCodeInvalidPlayerName, fmt.Sprintf("invalid player name '%s': %s", name, reason), nil, ) } // ErrUserNotFound indicates a user was not found func ErrUserNotFound(userID types.UserID) *DomainError { return NewDomainError( ErrCodeUserNotFound, fmt.Sprintf("user not found: %s", userID), nil, ) } // ErrUserAlreadyExists indicates a user already exists func ErrUserAlreadyExists(name string) *DomainError { return NewDomainError( ErrCodeUserAlreadyExists, fmt.Sprintf("user already exists: %s", name), nil, ) } // Game Session-related errors // ErrSessionNotFound indicates a game session was not found func ErrSessionNotFound(sessionID types.GameSessionID) *DomainError { return NewDomainError( ErrCodeSessionNotFound, fmt.Sprintf("game session not found: %s", sessionID), nil, ) } // ErrSessionExpired indicates a game session has expired func ErrSessionExpired(sessionID types.GameSessionID) *DomainError { return NewDomainError( ErrCodeSessionExpired, fmt.Sprintf("game session expired: %s", sessionID), nil, ) } // ErrSessionNotActive indicates operation requires an active session func ErrSessionNotActive(sessionID types.GameSessionID, status types.SessionStatus) *DomainError { return NewDomainError( ErrCodeSessionNotActive, fmt.Sprintf("session %s is not active (current status: %s)", sessionID, status), nil, ) } // ErrInvalidSessionTransition indicates an invalid session status transition func ErrInvalidSessionTransition(from, to types.SessionStatus) *DomainError { return NewDomainError( ErrCodeInvalidSessionTransition, fmt.Sprintf("invalid session transition from %s to %s", from, to), nil, ) } // ErrUserAlreadyHasActiveSession indicates user already has an active session func ErrUserAlreadyHasActiveSession(userID types.UserID, existingSessionID types.GameSessionID) *DomainError { return NewDomainError( ErrCodeUserAlreadyHasActiveSession, fmt.Sprintf("user %s already has active session: %s", userID, existingSessionID), nil, ) } // Attempt-related errors // ErrMaxAttemptsExceeded indicates maximum attempts for a question have been exceeded func ErrMaxAttemptsExceeded(questionID types.QuestionID, maxAttempts int) *DomainError { return NewDomainError( ErrCodeMaxAttemptsExceeded, fmt.Sprintf("maximum attempts (%d) exceeded for question: %s", maxAttempts, questionID), nil, ) } // ErrAttemptTooFast indicates an attempt was made too quickly (anti-cheat) func ErrAttemptTooFast(minInterval string) *DomainError { return NewDomainError( ErrCodeAttemptTooFast, fmt.Sprintf("attempt made too quickly, minimum interval: %s", minInterval), nil, ) } // Question-related errors // ErrQuestionNotFound indicates a question was not found func ErrQuestionNotFound(questionID types.QuestionID) *DomainError { return NewDomainError( ErrCodeQuestionNotFound, fmt.Sprintf("question not found: %s", questionID), nil, ) } // ErrQuestionAlreadyAnswered indicates question was already answered in this session func ErrQuestionAlreadyAnswered(questionID types.QuestionID, sessionID types.GameSessionID) *DomainError { return NewDomainError( ErrCodeQuestionAlreadyAnswered, fmt.Sprintf("question %s already answered in session %s", questionID, sessionID), nil, ) } // ErrNoQuestionsAvailable indicates no questions are available for selection func ErrNoQuestionsAvailable() *DomainError { return NewDomainError( ErrCodeNoQuestionsAvailable, "no questions available for selection", nil, ) } // Theme-related errors // ErrThemeNotFound indicates a theme was not found func ErrThemeNotFound(themeID types.ThemeID) *DomainError { return NewDomainError( ErrCodeThemeNotFound, fmt.Sprintf("theme not found: %s", themeID), nil, ) } // ErrInvalidTheme indicates an invalid theme func ErrInvalidTheme(reason string) *DomainError { return NewDomainError( ErrCodeInvalidTheme, fmt.Sprintf("invalid theme: %s", reason), nil, ) } // Answer-related errors // ErrInvalidAnswer indicates an answer is invalid func ErrInvalidAnswer(reason string) *DomainError { return NewDomainError( ErrCodeInvalidAnswer, fmt.Sprintf("invalid answer: %s", reason), nil, ) } // ErrEmptyAnswer indicates an empty answer was provided func ErrEmptyAnswer() *DomainError { return NewDomainError( ErrCodeEmptyAnswer, "answer cannot be empty", nil, ) } // ErrInvalidQuestion indicates an invalid question func ErrInvalidQuestion(reason string) *DomainError { return NewDomainError( ErrCodeInvalidQuestion, fmt.Sprintf("invalid question: %s", reason), nil, ) } // Hint-related errors // ErrHintNotAvailable indicates no hint is available for the question func ErrHintNotAvailable(questionID types.QuestionID) *DomainError { return NewDomainError( ErrCodeHintNotAvailable, fmt.Sprintf("no hint available for question: %s", questionID), nil, ) } // ErrHintAlreadyUsed indicates hint was already used for this question func ErrHintAlreadyUsed(questionID types.QuestionID) *DomainError { return NewDomainError( ErrCodeHintAlreadyUsed, fmt.Sprintf("hint already used for question: %s", questionID), nil, ) } // Scoring-related errors // ErrScoringFailed indicates a scoring operation failed func ErrScoringFailed(reason string) *DomainError { return NewDomainError( ErrCodeScoringFailed, fmt.Sprintf("scoring failed: %s", reason), nil, ) } // Validation-related errors // ErrValidationFailed indicates domain validation failed func ErrValidationFailed(field string, reason string) *DomainError { return NewDomainError( ErrCodeValidationFailed, fmt.Sprintf("validation failed for %s: %s", field, reason), nil, ) } // ErrBusinessRuleViolation indicates a business rule was violated func ErrBusinessRuleViolation(rule string, details string) *DomainError { return NewDomainError( ErrCodeBusinessRuleViolation, fmt.Sprintf("business rule violation - %s: %s", rule, details), nil, ) } // General errors // ErrOperationNotAllowed indicates the requested operation is not allowed func ErrOperationNotAllowed(operation string, reason string) *DomainError { return NewDomainError( ErrCodeOperationNotAllowed, fmt.Sprintf("operation '%s' not allowed: %s", operation, reason), nil, ) } // ErrInternalError indicates an internal domain error occurred func ErrInternalError(reason string, cause error) *DomainError { return NewDomainError( ErrCodeInternalError, fmt.Sprintf("internal error: %s", reason), cause, ) } // Error checking functions // IsErrorOfType checks if an error is of a specific domain error type func IsErrorOfType(err error, code string) bool { if domainErr, ok := err.(*DomainError); ok { return domainErr.Code == code } return false } // IsUserError checks if error is user-related func IsUserError(err error) bool { return IsErrorOfType(err, ErrCodeInvalidPlayerName) || IsErrorOfType(err, ErrCodeUserNotFound) || IsErrorOfType(err, ErrCodeUserAlreadyExists) } // IsSessionError checks if error is session-related func IsSessionError(err error) bool { return IsErrorOfType(err, ErrCodeSessionNotFound) || IsErrorOfType(err, ErrCodeSessionExpired) || IsErrorOfType(err, ErrCodeSessionNotActive) || IsErrorOfType(err, ErrCodeInvalidSessionTransition) || IsErrorOfType(err, ErrCodeUserAlreadyHasActiveSession) } // IsValidationError checks if error is validation-related func IsValidationError(err error) bool { return IsErrorOfType(err, ErrCodeValidationFailed) || IsErrorOfType(err, ErrCodeInvalidPlayerName) || IsErrorOfType(err, ErrCodeInvalidAnswer) } // IsBusinessRuleError checks if error is business rule violation func IsBusinessRuleError(err error) bool { return IsErrorOfType(err, ErrCodeBusinessRuleViolation) || IsErrorOfType(err, ErrCodeMaxAttemptsExceeded) || IsErrorOfType(err, ErrCodeAttemptTooFast) } // Leaderboard-related error variables var ( ErrInvalidLeaderboardID = NewDomainError(ErrCodeInvalidLeaderboardID, "invalid leaderboard ID", nil) ErrLeaderboardNotFound = NewDomainError(ErrCodeLeaderboardNotFound, "leaderboard not found", nil) ErrLeaderboardAlreadyExists = NewDomainError(ErrCodeLeaderboardAlreadyExists, "leaderboard already exists", nil) ErrLeaderboardFrozen = NewDomainError(ErrCodeLeaderboardFrozen, "leaderboard is frozen", nil) ErrLeaderboardArchived = NewDomainError(ErrCodeLeaderboardArchived, "leaderboard is archived", nil) ErrInvalidRank = NewDomainError(ErrCodeInvalidRank, "invalid rank", nil) ErrPlayerNotFound = NewDomainError(ErrCodePlayerNotFound, "player not found", nil) ErrInvalidResetFrequency = NewDomainError(ErrCodeInvalidResetFrequency, "invalid reset frequency", nil) ErrInvalidResetAction = NewDomainError(ErrCodeInvalidResetAction, "invalid reset action", nil) ErrInvalidResetInterval = NewDomainError(ErrCodeInvalidResetInterval, "invalid reset interval", nil) ErrInvalidTimezone = NewDomainError(ErrCodeInvalidTimezone, "invalid timezone", nil) ErrInvalidPreserveCount = NewDomainError(ErrCodeInvalidPreserveCount, "invalid preserve count", nil) ErrInvalidResetTime = NewDomainError(ErrCodeInvalidResetTime, "invalid reset time", nil) ErrInvalidNotificationTime = NewDomainError(ErrCodeInvalidNotificationTime, "invalid notification time", nil) ErrInvalidDisplayFormat = NewDomainError(ErrCodeInvalidDisplayFormat, "invalid display format", nil) ErrInvalidSortOrder = NewDomainError(ErrCodeInvalidSortOrder, "invalid sort order", nil) ErrInvalidRankDisplay = NewDomainError(ErrCodeInvalidRankDisplay, "invalid rank display", nil) ErrInvalidEntriesPerPage = NewDomainError(ErrCodeInvalidEntriesPerPage, "invalid entries per page", nil) ErrInvalidHighlightCount = NewDomainError(ErrCodeInvalidHighlightCount, "invalid highlight count", nil) ErrInvalidColorKey = NewDomainError(ErrCodeInvalidColorKey, "invalid color key", nil) ErrInvalidColorValue = NewDomainError(ErrCodeInvalidColorValue, "invalid color value", nil) ErrInvalidLabelKey = NewDomainError(ErrCodeInvalidLabelKey, "invalid label key", nil) ErrInvalidLabelValue = NewDomainError(ErrCodeInvalidLabelValue, "invalid label value", nil) ErrInvalidMetadata = NewDomainError(ErrCodeInvalidMetadata, "invalid metadata", nil) ErrInvalidGameCount = NewDomainError(ErrCodeInvalidGameCount, "invalid game count", nil) ErrInvalidPlayerCount = NewDomainError(ErrCodeInvalidPlayerCount, "invalid player count", nil) ErrInvalidStreak = NewDomainError(ErrCodeInvalidStreak, "invalid streak", nil) ErrInvalidPlayerID = NewDomainError(ErrCodeInvalidPlayerID, "invalid player ID", nil) ErrInvalidSessionID = NewDomainError(ErrCodeInvalidSessionID, "invalid session ID", nil) ErrInvalidScore = NewDomainError(ErrCodeInvalidScore, "invalid score", nil) ErrInvalidRankRange = NewDomainError(ErrCodeInvalidRankRange, "invalid rank range", nil) ErrInvalidTopCount = NewDomainError(ErrCodeInvalidTopCount, "invalid top count", nil) ErrInvalidContext = NewDomainError(ErrCodeInvalidContext, "invalid context", nil) ErrInvalidCompetitionName = NewDomainError(ErrCodeInvalidCompetitionName, "invalid competition name", nil) ErrInvalidCompetitionStatus = NewDomainError(ErrCodeInvalidCompetitionStatus, "invalid competition status", nil) ErrCompetitionNotYetStarted = NewDomainError(ErrCodeCompetitionNotYetStarted, "competition not yet started", nil) ErrCompetitionNotActive = NewDomainError(ErrCodeCompetitionNotActive, "competition not active", nil) ErrCompetitionAlreadyCompleted = NewDomainError(ErrCodeCompetitionAlreadyCompleted, "competition already completed", nil) ErrPlayerNotQualified = NewDomainError(ErrCodePlayerNotQualified, "player not qualified", nil) ErrCompetitionFull = NewDomainError(ErrCodeCompetitionFull, "competition full", nil) ErrScoreBelowMinimum = NewDomainError(ErrCodeScoreBelowMinimum, "score below minimum", nil) ErrTooManyEntriesPerPlayer = NewDomainError(ErrCodeTooManyEntriesPerPlayer, "too many entries per player", nil) ErrEntryNotFound = NewDomainError(ErrCodeEntryNotFound, "entry not found", nil) ErrInvalidStartTime = NewDomainError(ErrCodeInvalidStartTime, "invalid start time", nil) ErrInvalidEndTime = NewDomainError(ErrCodeInvalidEndTime, "invalid end time", nil) )