// Package redisstate defines the frozen Redis logical keyspace and pagination // helpers used by future User Service storage adapters. package redisstate import ( "encoding/base64" "fmt" "strings" "time" "galaxy/user/internal/domain/common" "galaxy/user/internal/domain/entitlement" "galaxy/user/internal/domain/policy" ) const defaultPrefix = "user:" // Keyspace builds the frozen Redis logical keys used by future storage // adapters. The package intentionally exposes key construction only and does // not depend on any Redis client. type Keyspace struct { // Prefix stores the namespace prefix applied to every key. The zero value // uses `user:`. Prefix string } // Account returns the primary user-account key for userID. func (k Keyspace) Account(userID common.UserID) string { return k.prefix() + "account:" + encodeKeyComponent(userID.String()) } // EmailLookup returns the exact normalized e-mail lookup key. func (k Keyspace) EmailLookup(email common.Email) string { return k.prefix() + "lookup:email:" + encodeKeyComponent(email.String()) } // UserNameLookup returns the exact stored user-name lookup key. func (k Keyspace) UserNameLookup(userName common.UserName) string { return k.prefix() + "lookup:user-name:" + encodeKeyComponent(userName.String()) } // BlockedEmailSubject returns the dedicated blocked-email-subject key. func (k Keyspace) BlockedEmailSubject(email common.Email) string { return k.prefix() + "blocked-email:" + encodeKeyComponent(email.String()) } // EntitlementRecord returns the primary entitlement history-record key. func (k Keyspace) EntitlementRecord(recordID entitlement.EntitlementRecordID) string { return k.prefix() + "entitlement:record:" + encodeKeyComponent(recordID.String()) } // EntitlementHistory returns the per-user entitlement-history index key. func (k Keyspace) EntitlementHistory(userID common.UserID) string { return k.prefix() + "entitlement:history:" + encodeKeyComponent(userID.String()) } // EntitlementSnapshot returns the current entitlement-snapshot key. func (k Keyspace) EntitlementSnapshot(userID common.UserID) string { return k.prefix() + "entitlement:snapshot:" + encodeKeyComponent(userID.String()) } // SanctionRecord returns the primary sanction history-record key. func (k Keyspace) SanctionRecord(recordID policy.SanctionRecordID) string { return k.prefix() + "sanction:record:" + encodeKeyComponent(recordID.String()) } // SanctionHistory returns the per-user sanction-history index key. func (k Keyspace) SanctionHistory(userID common.UserID) string { return k.prefix() + "sanction:history:" + encodeKeyComponent(userID.String()) } // ActiveSanction returns the per-user active-sanction slot for one sanction // code. The slot guarantees at most one active sanction per `user_id + // sanction_code`. func (k Keyspace) ActiveSanction(userID common.UserID, code policy.SanctionCode) string { return k.prefix() + "sanction:active:" + encodeKeyComponent(userID.String()) + ":" + encodeKeyComponent(string(code)) } // LimitRecord returns the primary limit history-record key. func (k Keyspace) LimitRecord(recordID policy.LimitRecordID) string { return k.prefix() + "limit:record:" + encodeKeyComponent(recordID.String()) } // LimitHistory returns the per-user limit-history index key. func (k Keyspace) LimitHistory(userID common.UserID) string { return k.prefix() + "limit:history:" + encodeKeyComponent(userID.String()) } // ActiveLimit returns the per-user active-limit slot for one limit code. The // slot guarantees at most one active limit per `user_id + limit_code`. func (k Keyspace) ActiveLimit(userID common.UserID, code policy.LimitCode) string { return k.prefix() + "limit:active:" + encodeKeyComponent(userID.String()) + ":" + encodeKeyComponent(string(code)) } // CreatedAtIndex returns the deterministic newest-first user-ordering index. func (k Keyspace) CreatedAtIndex() string { return k.prefix() + "index:created-at" } // PaidStateIndex returns the coarse free-versus-paid index key. func (k Keyspace) PaidStateIndex(state entitlement.PaidState) string { return k.prefix() + "index:paid-state:" + encodeKeyComponent(string(state)) } // FinitePaidExpiryIndex returns the finite paid-expiry index key. Lifetime // plans intentionally do not participate in this index. func (k Keyspace) FinitePaidExpiryIndex() string { return k.prefix() + "index:paid-expiry:finite" } // DeclaredCountryIndex returns the current declared-country reverse-lookup // index key. func (k Keyspace) DeclaredCountryIndex(code common.CountryCode) string { return k.prefix() + "index:declared-country:" + encodeKeyComponent(code.String()) } // ActiveSanctionCodeIndex returns the reverse-lookup index key for users with // an active sanction code. func (k Keyspace) ActiveSanctionCodeIndex(code policy.SanctionCode) string { return k.prefix() + "index:active-sanction:" + encodeKeyComponent(string(code)) } // ActiveLimitCodeIndex returns the reverse-lookup index key for users with an // active limit code. func (k Keyspace) ActiveLimitCodeIndex(code policy.LimitCode) string { return k.prefix() + "index:active-limit:" + encodeKeyComponent(string(code)) } // EligibilityMarkerIndex returns the reverse-lookup index key for one derived // eligibility marker boolean. func (k Keyspace) EligibilityMarkerIndex(marker policy.EligibilityMarker, value bool) string { return fmt.Sprintf("%sindex:eligibility:%s:%t", k.prefix(), encodeKeyComponent(string(marker)), value) } // CreatedAtScore returns the frozen ZSET score representation for created-at // ordering and deterministic pagination. func CreatedAtScore(createdAt time.Time) float64 { return float64(createdAt.UTC().UnixMicro()) } // ExpiryScore returns the frozen ZSET score representation for finite paid // expiry ordering. func ExpiryScore(expiresAt time.Time) float64 { return float64(expiresAt.UTC().UnixMicro()) } // PageCursor identifies the last seen `(created_at, user_id)` tuple used by // deterministic newest-first pagination. type PageCursor struct { // CreatedAt stores the created-at component of the last seen row. CreatedAt time.Time // UserID stores the user-id tiebreaker component of the last seen row. UserID common.UserID } // Validate reports whether PageCursor contains a complete cursor tuple. func (cursor PageCursor) Validate() error { if err := common.ValidateTimestamp("page cursor created at", cursor.CreatedAt); err != nil { return err } if err := cursor.UserID.Validate(); err != nil { return fmt.Errorf("page cursor user id: %w", err) } return nil } // ComparePageOrder compares two listing positions using the frozen ordering: // `created_at desc`, then `user_id desc`. func ComparePageOrder(left PageCursor, right PageCursor) int { switch { case left.CreatedAt.After(right.CreatedAt): return -1 case left.CreatedAt.Before(right.CreatedAt): return 1 default: return -strings.Compare(left.UserID.String(), right.UserID.String()) } } func (k Keyspace) prefix() string { prefix := strings.TrimSpace(k.Prefix) if prefix == "" { return defaultPrefix } return prefix } func encodeKeyComponent(value string) string { return base64.RawURLEncoding.EncodeToString([]byte(value)) }