package points import ( "fmt" "math" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/gothinkster/golang-gin-realworld-example-app/common" "github.com/gothinkster/golang-gin-realworld-example-app/models" ) // GetDashboard returns user points dashboard // GET /api/points/dashboard?wallet_address=0x... func GetDashboard(c *gin.Context) { db := common.GetDB() user, found := getUserByWallet(c) if !found { c.JSON(http.StatusNotFound, gin.H{ "success": false, "error": "User not found", }) return } vipLevel := user.VIPLevel if vipLevel < 1 { vipLevel = 1 } var currentTier models.VIPTier db.Where("tier_level = ?", vipLevel).First(¤tTier) var nextTier models.VIPTier var pointsToNextTier int64 var nextTierName string if db.Where("tier_level = ?", vipLevel+1).First(&nextTier).Error == nil { pointsToNextTier = nextTier.MinPoints - user.TotalPoints nextTierName = nextTier.TierName } else { pointsToNextTier = 0 nextTierName = "Max Level" } var season models.Season db.Where("status = ?", "active").First(&season) now := time.Now() timeRemaining := season.EndTime.Sub(now) daysRemaining := int(timeRemaining.Hours() / 24) hoursRemaining := int(timeRemaining.Hours()) % 24 var totalUsers int64 db.Model(&models.User{}).Count(&totalUsers) topPercentage := "N/A" globalRank := 0 if user.GlobalRank != nil { globalRank = *user.GlobalRank if totalUsers > 0 { percentage := float64(*user.GlobalRank) / float64(totalUsers) * 100 topPercentage = fmt.Sprintf("%.0f%%", percentage) } } response := models.DashboardResponse{ TotalPoints: user.TotalPoints, GlobalRank: globalRank, TopPercentage: topPercentage, MemberTier: user.MemberTier, VIPLevel: vipLevel, PointsToNextTier: pointsToNextTier, NextTier: nextTierName, Season: models.SeasonInfo{ SeasonNumber: season.SeasonNumber, SeasonName: season.SeasonName, IsLive: season.Status == "active", EndTime: season.EndTime, DaysRemaining: daysRemaining, HoursRemaining: hoursRemaining, }, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": response, }) } // GetLeaderboard returns top performers leaderboard // GET /api/points/leaderboard?limit=5&wallet_address=0x... func GetLeaderboard(c *gin.Context) { db := common.GetDB() limitStr := c.DefaultQuery("limit", "5") limit, _ := strconv.Atoi(limitStr) var topUsers []models.User db.Where("global_rank IS NOT NULL"). Order("global_rank ASC"). Limit(limit). Find(&topUsers) var leaderboard []models.LeaderboardUser for _, u := range topUsers { rank := 0 if u.GlobalRank != nil { rank = *u.GlobalRank } leaderboard = append(leaderboard, models.LeaderboardUser{ Rank: rank, WalletAddress: formatAddress(u.WalletAddress), Points: u.TotalPoints, }) } currentUser, _ := getUserByWallet(c) myRank := 0 if currentUser.GlobalRank != nil { myRank = *currentUser.GlobalRank } response := models.LeaderboardResponse{ TopUsers: leaderboard, MyRank: myRank, MyPoints: currentUser.TotalPoints, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": response, }) } // GetInviteCode returns user's invite code information // GET /api/points/invite-code?wallet_address=0x... func GetInviteCode(c *gin.Context) { db := common.GetDB() user, found := getUserByWallet(c) if !found { c.JSON(http.StatusNotFound, gin.H{ "success": false, "error": "User not found", }) return } var inviteCode models.InviteCode err := db.Where("wallet_address = ?", user.WalletAddress).First(&inviteCode).Error if err != nil { // Create a system-generated invite code code := strings.ToUpper(common.RandString(8)) defaultExpiry := time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC) inviteCode = models.InviteCode{ WalletAddress: user.WalletAddress, Code: code, MaxUses: -1, UsedCount: 0, IsActive: true, ExpiresAt: models.NullTime{Time: &defaultExpiry}, } if err := db.Create(&inviteCode).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Failed to create invite code", }) return } // Also sync to users.invite_code db.Model(&user).Update("invite_code", code) } response := models.InviteCodeResponse{ Code: inviteCode.Code, UsedCount: inviteCode.UsedCount, MaxUses: inviteCode.MaxUses, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": response, }) } // BindInvite binds an invite code to current user // POST /api/points/bind-invite func BindInvite(c *gin.Context) { db := common.GetDB() var req struct { Code string `json:"code" binding:"required"` Signature string `json:"signature" binding:"required"` WalletAddress string `json:"wallet_address"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Invalid request body", }) return } // Look up user by wallet_address var user models.User walletAddr := strings.ToLower(strings.TrimSpace(req.WalletAddress)) if walletAddr == "" { // Try query param fallback walletAddr = strings.ToLower(strings.TrimSpace(c.Query("wallet_address"))) } if walletAddr == "" || db.Where("wallet_address = ?", walletAddr).First(&user).Error != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "error": "User not found. Please connect your wallet first.", }) return } // Check if user already has a referrer if user.ReferrerWallet != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Already bound to a referrer", }) return } // Find invite code var inviteCode models.InviteCode if err := db.Where("code = ? AND is_active = ?", req.Code, true).First(&inviteCode).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{ "success": false, "error": "Invalid invite code", }) return } // Check if max uses reached if inviteCode.MaxUses != -1 && inviteCode.UsedCount >= inviteCode.MaxUses { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Invite code max uses reached", }) return } // Prevent self-referral if inviteCode.WalletAddress == user.WalletAddress { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "Cannot use your own invite code", }) return } // Create invitation record invitation := models.Invitation{ ReferrerWallet: inviteCode.WalletAddress, RefereeWallet: user.WalletAddress, InviteCode: req.Code, BindSignature: req.Signature, Status: "active", ReferrerRewardPoints: 100, RefereeRewardPoints: 50, BoundAt: time.Now(), } if err := db.Create(&invitation).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Failed to bind invite code", }) return } // Update user referrer db.Model(&user).Update("referrer_wallet", inviteCode.WalletAddress) // Update invite code used count db.Model(&inviteCode).Update("used_count", inviteCode.UsedCount+1) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Invite code bound successfully", }) } // GetTeamTVL returns team TVL statistics // GET /api/points/team?wallet_address=0x... func GetTeamTVL(c *gin.Context) { db := common.GetDB() user, _ := getUserByWallet(c) var team models.UserTeam if err := db.Where("wallet_address = ?", user.WalletAddress).First(&team).Error; err != nil { c.JSON(http.StatusOK, gin.H{ "success": true, "data": models.TeamTVLResponse{ CurrentTVL: "$0", TargetTVL: "$10M", ProgressPercent: 0, TotalMembers: 0, Roles: []models.RoleCount{}, }, }) return } progressPercent := 0.0 if team.TeamTargetTVLUSD > 0 { progressPercent = (team.TeamTVLUSD / team.TeamTargetTVLUSD) * 100 } response := models.TeamTVLResponse{ CurrentTVL: formatUSD(team.TeamTVLUSD), TargetTVL: formatUSD(team.TeamTargetTVLUSD), ProgressPercent: math.Round(progressPercent*100) / 100, TotalMembers: team.TeamMembersCount, Roles: []models.RoleCount{ { Icon: "/icons/user/icon-whale.svg", Label: "Whales", Current: team.WhalesCount, Target: team.WhalesTarget, }, { Icon: "/icons/user/icon-trader.svg", Label: "Traders", Current: team.TradersCount, Target: team.TradersTarget, }, { Icon: "/icons/user/icon-user.svg", Label: "Users", Current: team.UsersCount, Target: team.UsersTarget, }, }, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": response, }) } // GetActivities returns user points activities // GET /api/points/activities?wallet_address=0x...&type=all&page=1&pageSize=10 func GetActivities(c *gin.Context) { db := common.GetDB() activityType := c.DefaultQuery("type", "all") pageStr := c.DefaultQuery("page", "1") pageSizeStr := c.DefaultQuery("pageSize", "10") page, _ := strconv.Atoi(pageStr) pageSize, _ := strconv.Atoi(pageSizeStr) offset := (page - 1) * pageSize user, _ := getUserByWallet(c) query := db.Model(&models.UserPointsRecord{}).Where("wallet_address = ?", user.WalletAddress) if activityType == "referrals" { query = query.Where("source_type = ?", "invitation") } else if activityType == "deposits" { query = query.Where("source_type IN ?", []string{"deposit", "holding"}) } var total int64 query.Count(&total) var records []models.UserPointsRecord query.Order("created_at DESC"). Limit(pageSize). Offset(offset). Find(&records) activities := make([]models.ActivityRecord, 0) for _, record := range records { activity := models.ActivityRecord{ Type: record.SourceType, UserAddress: formatAddress(user.WalletAddress), Points: record.PointsChange, CreatedAt: record.CreatedAt, } if record.SourceType == "invitation" && record.SourceID != nil { var invitation models.Invitation if db.Where("id = ?", *record.SourceID).First(&invitation).Error == nil { activity.FriendAddress = formatAddress(invitation.RefereeWallet) activity.InviteCode = invitation.InviteCode } } activities = append(activities, activity) } totalPages := int(math.Ceil(float64(total) / float64(pageSize))) response := models.ActivitiesResponse{ Activities: activities, Pagination: models.PaginationInfo{ Page: page, PageSize: pageSize, Total: int(total), TotalPage: totalPages, }, } c.JSON(http.StatusOK, gin.H{ "success": true, "data": response, }) } // Helper function to format wallet address for display func formatAddress(address string) string { if len(address) < 10 { return address } return address[:6] + "..." + address[len(address)-4:] } // Helper function to format USD values func formatUSD(value float64) string { if value >= 1000000 { return fmt.Sprintf("$%.1fM", value/1000000) } else if value >= 1000 { return fmt.Sprintf("$%.1fK", value/1000) } return fmt.Sprintf("$%.2f", value) } // getUserByWallet looks up a user by wallet_address query param, // falls back to demo user if not provided func getUserByWallet(c *gin.Context) (models.User, bool) { db := common.GetDB() var user models.User walletAddress := strings.ToLower(strings.TrimSpace(c.Query("wallet_address"))) if walletAddress != "" { if err := db.Where("wallet_address = ?", walletAddress).First(&user).Error; err == nil { return user, true } } // Fall back to first demo user err := db.Order("created_at ASC").First(&user).Error return user, err == nil } // walletAddressToInt64 is no longer needed (no auth_user_id in new schema) // kept for reference only // WalletRegister creates or finds a user by wallet address // POST /api/points/wallet-register func WalletRegister(c *gin.Context) { db := common.GetDB() var req struct { WalletAddress string `json:"wallet_address"` } if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.WalletAddress) == "" { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "error": "wallet_address is required", }) return } walletAddress := strings.ToLower(strings.TrimSpace(req.WalletAddress)) // Check if user already exists var user models.User err := db.Where("wallet_address = ?", walletAddress).First(&user).Error if err != nil { // Create new user with system-generated invite code code := strings.ToUpper(common.RandString(8)) user = models.User{ WalletAddress: walletAddress, InviteCode: code, MemberTier: "Bronze", VIPLevel: 1, TotalPoints: 0, } if createErr := db.Create(&user).Error; createErr != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Failed to create user", }) return } // Create invite_codes record defaultExpiry := time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC) inviteCodeRecord := models.InviteCode{ WalletAddress: walletAddress, Code: code, MaxUses: -1, UsedCount: 0, IsActive: true, ExpiresAt: models.NullTime{Time: &defaultExpiry}, } db.Create(&inviteCodeRecord) } // Get invite code from invite_codes table var inviteCodeRecord models.InviteCode inviteCode := user.InviteCode usedCount := 0 if db.Where("wallet_address = ?", user.WalletAddress).First(&inviteCodeRecord).Error == nil { inviteCode = inviteCodeRecord.Code usedCount = inviteCodeRecord.UsedCount } globalRank := 0 if user.GlobalRank != nil { globalRank = *user.GlobalRank } c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "walletAddress": user.WalletAddress, "inviteCode": inviteCode, "usedCount": usedCount, "memberTier": user.MemberTier, "vipLevel": user.VIPLevel, "totalPoints": user.TotalPoints, "globalRank": globalRank, }, }) }