package controllers import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "fotbal-club/internal/models" "fotbal-club/internal/services" "fotbal-club/pkg/email" ) type SweepstakesController struct { DB *gorm.DB Email email.EmailService } func NewSweepstakesController(db *gorm.DB, es email.EmailService) *SweepstakesController { return &SweepstakesController{DB: db, Email: es} } // Public: visualization data for a specific sweepstake (within visibility window) // GET /api/v1/sweepstakes/:id/visual func (sc *SweepstakesController) PublicVisualData(c *gin.Context) { id := strings.TrimSpace(c.Param("id")) var s models.Sweepstake if err := sc.DB.First(&s, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } now := time.Now() if s.VisibilityUntil == nil || now.After(*s.VisibilityUntil) || now.Before(s.EndAt) { c.JSON(http.StatusNotFound, gin.H{"error": "Not available"}) return } var winners []struct { UserID uint `json:"user_id"` PrizeName string `json:"prize_name"` } _ = sc.DB.Table("sweepstake_winners").Select("user_id, prize_name").Where("sweepstake_id = ?", id).Order("id ASC").Scan(&winners).Error type entryRow struct { UserID uint `json:"user_id"` DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` } var entries []entryRow q := sc.DB.Table("sweepstake_entries AS e"). Select("e.user_id, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS display_name, COALESCE(up.animated_avatar_url, up.avatar_url, '') AS avatar_url"). Joins("JOIN users u ON u.id = e.user_id"). Joins("LEFT JOIN user_profiles up ON up.user_id = u.id"). Where("e.sweepstake_id = ?", id) _ = q.Scan(&entries).Error c.JSON(http.StatusOK, gin.H{"sweepstake": s, "entries": entries, "winners": winners}) } // Admin: set or change prize for a specific winner // PATCH /api/v1/admin/sweepstakes/:id/winners/:winner_id/prize { "prize_id": 123 } func (sc *SweepstakesController) AdminSetWinnerPrize(c *gin.Context) { wid := strings.TrimSpace(c.Param("winner_id")) var body struct { PrizeID uint `json:"prize_id"` } if err := c.ShouldBindJSON(&body); err != nil || body.PrizeID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid prize"}) return } // Load prize name var p models.SweepstakePrize if err := sc.DB.First(&p, body.PrizeID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Prize not found"}) return } updates := map[string]interface{}{"prize_id": p.ID, "prize_name": strings.TrimSpace(p.Name)} if err := sc.DB.Model(&models.SweepstakeWinner{}).Where("id = ?", wid).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: update winner status (claim/delivered/pending) // PATCH /api/v1/admin/sweepstakes/:id/winners/:winner_id { "claim_status": "claimed|delivered|pending", "claim_note":"..." } func (sc *SweepstakesController) AdminUpdateWinner(c *gin.Context) { wid := strings.TrimSpace(c.Param("winner_id")) var body struct { ClaimStatus string `json:"claim_status"` ClaimNote string `json:"claim_note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } st := strings.ToLower(strings.TrimSpace(body.ClaimStatus)) if st == "" { st = "pending" } switch st { case "pending", "claimed", "delivered": default: c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status"}) return } // Load winner to evaluate prize awarding var w models.SweepstakeWinner if err := sc.DB.First(&w, wid).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } // Update fields updates := map[string]interface{}{"claim_status": st} if strings.TrimSpace(body.ClaimNote) != "" { updates["claim_note"] = strings.TrimSpace(body.ClaimNote) } // Award non-physical prizes only once when moving to claimed/delivered shouldAward := (st == "claimed" || st == "delivered") && (w.AwardedAt == nil) if shouldAward && w.PrizeID != nil { var p models.SweepstakePrize if err := sc.DB.First(&p, *w.PrizeID).Error; err == nil { if p.Kind == "points" || p.Kind == "xp" || p.Kind == "points_xp" { svc := services.NewEngagementService(sc.DB) var pointsDelta, xpDelta int64 switch p.Kind { case "points": pointsDelta, xpDelta = p.Points, 0 case "xp": pointsDelta, xpDelta = 0, p.XP case "points_xp": pointsDelta, xpDelta = p.Points, p.XP } if pointsDelta != 0 || xpDelta != 0 { _, _ = svc.AwardPointsAndXP(w.UserID, pointsDelta, xpDelta, "sweepstake_prize", map[string]interface{}{"prize_id": p.ID, "sweepstake_id": w.SweepstakeID}) } now := time.Now() updates["awarded_at"] = &now } } } if err := sc.DB.Model(&models.SweepstakeWinner{}).Where("id = ?", wid).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: list prizes // GET /api/v1/admin/sweepstakes/:id/prizes func (sc *SweepstakesController) AdminListPrizes(c *gin.Context) { id := c.Param("id") var items []models.SweepstakePrize if err := sc.DB.Where("sweepstake_id = ?", id).Order("display_order ASC, id ASC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Admin: create prize // POST /api/v1/admin/sweepstakes/:id/prizes func (sc *SweepstakesController) AdminCreatePrize(c *gin.Context) { sid := strings.TrimSpace(c.Param("id")) var s models.Sweepstake if err := sc.DB.First(&s, sid).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Sweepstake not found"}) return } var body struct { Name string `json:"name"` Description string `json:"description"` ImageURL string `json:"image_url"` Value string `json:"value"` Quantity int `json:"quantity"` DisplayOrder int `json:"display_order"` Kind string `json:"kind"` Points int64 `json:"points"` XP int64 `json:"xp"` } if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.Name) == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } // Normalize prize kind/values kind := strings.ToLower(strings.TrimSpace(body.Kind)) switch kind { case "", "physical", "points", "xp", "points_xp": if kind == "" { kind = "physical" } default: c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid prize kind"}) return } if body.Points < 0 { body.Points = 0 } if body.XP < 0 { body.XP = 0 } p := models.SweepstakePrize{SweepstakeID: s.ID, Name: strings.TrimSpace(body.Name), Description: strings.TrimSpace(body.Description), ImageURL: strings.TrimSpace(body.ImageURL), Value: strings.TrimSpace(body.Value), Quantity: body.Quantity, DisplayOrder: body.DisplayOrder, Kind: kind, Points: body.Points, XP: body.XP} if err := sc.DB.Create(&p).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, p) } // Admin: update prize // PUT /api/v1/admin/sweepstakes/:id/prizes/:prize_id func (sc *SweepstakesController) AdminUpdatePrize(c *gin.Context) { pid := strings.TrimSpace(c.Param("prize_id")) var body map[string]interface{} if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } allowed := map[string]bool{"name": true, "description": true, "image_url": true, "value": true, "quantity": true, "display_order": true, "kind": true, "points": true, "xp": true} upd := map[string]interface{}{} for k, v := range body { if allowed[k] { upd[k] = v } } // Validate kind if present if v, ok := upd["kind"]; ok { sv := strings.ToLower(strings.TrimSpace(toString(v))) switch sv { case "physical", "points", "xp", "points_xp": upd["kind"] = sv default: c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid prize kind"}) return } } // Coerce points/xp to non-negative integers if present if v, ok := upd["points"]; ok { upd["points"] = toNonNegInt64(v) } if v, ok := upd["xp"]; ok { upd["xp"] = toNonNegInt64(v) } if len(upd) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "No changes"}) return } if err := sc.DB.Model(&models.SweepstakePrize{}).Where("id = ?", pid).Updates(upd).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: delete prize // DELETE /api/v1/admin/sweepstakes/:id/prizes/:prize_id func (sc *SweepstakesController) AdminDeletePrize(c *gin.Context) { pid := strings.TrimSpace(c.Param("prize_id")) if err := sc.DB.Delete(&models.SweepstakePrize{}, "id = ?", pid).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: reorder prizes // POST /api/v1/admin/sweepstakes/:id/prizes/reorder { "order": [prize_id...] } func (sc *SweepstakesController) AdminReorderPrizes(c *gin.Context) { sid := strings.TrimSpace(c.Param("id")) var body struct { Order []uint `json:"order"` } if err := c.ShouldBindJSON(&body); err != nil || len(body.Order) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid order"}) return } tx := sc.DB.Begin() for i, id := range body.Order { if err := tx.Model(&models.SweepstakePrize{}).Where("id = ? AND sweepstake_id = ?", id, sid).Update("display_order", i).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } } if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: visualization data for sweepstake (participants and winners) // GET /api/v1/admin/sweepstakes/:id/visual func (sc *SweepstakesController) AdminVisualData(c *gin.Context) { id := strings.TrimSpace(c.Param("id")) var s models.Sweepstake if err := sc.DB.First(&s, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } // Winners in stable order var winners []struct { ID uint `json:"id"` UserID uint `json:"user_id"` PrizeName string `json:"prize_name"` ClaimStatus string `json:"claim_status"` } _ = sc.DB.Table("sweepstake_winners").Select("id, user_id, prize_name, claim_status").Where("sweepstake_id = ?", id).Order("id ASC").Scan(&winners).Error // Entries with display names and avatars type entryRow struct { UserID uint `json:"user_id"` DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` } var entries []entryRow q := sc.DB.Table("sweepstake_entries AS e"). Select("e.user_id, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS display_name, COALESCE(up.animated_avatar_url, up.avatar_url, '') AS avatar_url"). Joins("JOIN users u ON u.id = e.user_id"). Joins("LEFT JOIN user_profiles up ON up.user_id = u.id"). Where("e.sweepstake_id = ?", id) _ = q.Scan(&entries).Error c.JSON(http.StatusOK, gin.H{ "sweepstake": s, "entries": entries, "winners": winners, }) } // Admin: list sweepstakes with optional status filter func (sc *SweepstakesController) AdminList(c *gin.Context) { status := strings.TrimSpace(c.Query("status")) var items []models.Sweepstake q := sc.DB.Model(&models.Sweepstake{}).Order("start_at DESC, id DESC") if status != "" { q = q.Where("status = ?", status) } if err := q.Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Admin: create sweepstake func (sc *SweepstakesController) AdminCreate(c *gin.Context) { var body struct { Title string `json:"title"` Description string `json:"description"` ImageURL string `json:"image_url"` RulesURL string `json:"rules_url"` StartAt time.Time `json:"start_at"` EndAt time.Time `json:"end_at"` PickerStyle string `json:"picker_style"` TotalPrizes int `json:"total_prizes"` PrizeSummary string `json:"prize_summary"` EntryCostPoints int `json:"entry_cost_points"` EntryFeeCZK float64 `json:"entry_fee_czk"` MaxEntriesPerUser int `json:"max_entries_per_user"` } if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.Title) == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } item := models.Sweepstake{ Title: strings.TrimSpace(body.Title), Description: strings.TrimSpace(body.Description), ImageURL: strings.TrimSpace(body.ImageURL), RulesURL: strings.TrimSpace(body.RulesURL), StartAt: body.StartAt, EndAt: body.EndAt, PickerStyle: ifEmpty(body.PickerStyle, "wheel"), TotalPrizes: func(v int) int { if v < 1 { return 1 } if v > 100 { return 100 } return v }(ifZero(body.TotalPrizes, 1)), PrizeSummary: strings.TrimSpace(body.PrizeSummary), EntryCostPoints: func(v int) int { if v < 0 { return 0 } return v }(body.EntryCostPoints), EntryFeeCZK: func(v float64) float64 { if v < 0 { return 0 } return v }(body.EntryFeeCZK), MaxEntriesPerUser: func(v int) int { if v <= 0 { return 1 } return v }(body.MaxEntriesPerUser), Status: "scheduled", } if time.Now().After(item.StartAt) { item.Status = "active" } if err := sc.DB.Create(&item).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create"}) return } c.JSON(http.StatusOK, item) } // Admin: update sweepstake func (sc *SweepstakesController) AdminUpdate(c *gin.Context) { id := c.Param("id") var body map[string]interface{} if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } allowed := map[string]bool{"title": true, "description": true, "image_url": true, "rules_url": true, "start_at": true, "end_at": true, "picker_style": true, "total_prizes": true, "prize_summary": true, "status": true, "entry_cost_points": true, "entry_fee_czk": true, "max_entries_per_user": true} upd := map[string]interface{}{} for k, v := range body { if allowed[k] { upd[k] = v } } if len(upd) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "No changes"}) return } // Clamp total_prizes if provided if v, ok := upd["total_prizes"]; ok { // Coerce to integer first vv := 1 switch t := v.(type) { case int: vv = t case int64: vv = int(t) case float64: vv = int(t) case string: if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil { vv = n } default: // leave default 1 } if vv < 1 { vv = 1 } if vv > 100 { vv = 100 } upd["total_prizes"] = vv } if err := sc.DB.Model(&models.Sweepstake{}).Where("id = ?", id).Updates(upd).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Admin: delete sweepstake func (sc *SweepstakesController) AdminDelete(c *gin.Context) { id := c.Param("id") if err := sc.DB.Delete(&models.Sweepstake{}, "id = ?", id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Protected: enter sweepstake (deduct points if needed, enforce max entries) func (sc *SweepstakesController) Enter(c *gin.Context) { id := strings.TrimSpace(c.Param("id")) uid, _ := c.Get("userID") userID := uid.(uint) var s models.Sweepstake if err := sc.DB.First(&s, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load"}) return } now := time.Now() if !(now.After(s.StartAt) && now.Before(s.EndAt)) { c.JSON(http.StatusBadRequest, gin.H{"error": "Soutěž není aktivní"}) return } maxPerUser := s.MaxEntriesPerUser if maxPerUser <= 0 { maxPerUser = 1 } var existingCount int64 if err := sc.DB.Model(&models.SweepstakeEntry{}).Where("sweepstake_id = ? AND user_id = ? AND status = ?", s.ID, userID, "valid").Count(&existingCount).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check entries"}) return } if existingCount >= int64(maxPerUser) { c.JSON(http.StatusBadRequest, gin.H{"error": "Dosáhli jste limitu účastí v této soutěži"}) return } costPoints := s.EntryCostPoints if costPoints < 0 { costPoints = 0 } if costPoints > 0 { svc := services.NewEngagementService(sc.DB) up, err := svc.EnsureProfile(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze načíst profil"}) return } if up.Points < int64(costPoints) { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Nemáte dostatek bodů (potřeba: %d)", costPoints)}) return } if _, err := svc.AwardPointsAndXP(userID, -int64(costPoints), 0, "sweepstake_entry", map[string]interface{}{"sweepstake_id": s.ID}); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze odečíst body"}) return } e := models.SweepstakeEntry{SweepstakeID: s.ID, UserID: userID, Status: "valid"} if err := sc.DB.Create(&e).Error; err != nil { _, _ = svc.AwardPointsAndXP(userID, int64(costPoints), 0, "sweepstake_entry_refund", map[string]interface{}{"sweepstake_id": s.ID}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Nelze vytvořit účast"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) return } entry := models.SweepstakeEntry{SweepstakeID: s.ID, UserID: userID, Status: "valid"} if existingCount == 0 { if err := sc.DB.Where("sweepstake_id = ? AND user_id = ?", s.ID, userID).FirstOrCreate(&entry).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join"}) return } } else { if err := sc.DB.Create(&entry).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to join"}) return } } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Protected: mark visual played time for current user's entry func (sc *SweepstakesController) MarkVisualPlayed(c *gin.Context) { id := strings.TrimSpace(c.Param("id")) uid, _ := c.Get("userID") userID := uid.(uint) now := time.Now() var e models.SweepstakeEntry if err := sc.DB.Where("sweepstake_id = ? AND user_id = ?", id, userID).Order("id ASC").First(&e).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Entry not found"}) return } _ = sc.DB.Model(&models.SweepstakeEntry{}).Where("id = ?", e.ID).Update("visual_played_at", &now).Error c.JSON(http.StatusOK, gin.H{"ok": true}) } // Protected: list my winnings func (sc *SweepstakesController) MyWinnings(c *gin.Context) { uid, _ := c.Get("userID") userID := uid.(uint) var items []models.SweepstakeWinner if err := sc.DB.Where("user_id = ?", userID).Order("created_at DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Public: get current visible sweepstake (upcoming/active/finalized within visibility window) func (sc *SweepstakesController) GetCurrent(c *gin.Context) { now := time.Now() var s models.Sweepstake q := sc.DB.Where("start_at <= ? AND (visibility_until IS NULL OR visibility_until >= ?)", now, now).Order("start_at DESC") if err := q.First(&s).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusOK, gin.H{"sweepstake": nil}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load sweepstake"}) return } state := "upcoming" if now.After(s.StartAt) && now.Before(s.EndAt) { state = "active" } else if now.After(s.EndAt) { state = "finalized" } var prizes []models.SweepstakePrize _ = sc.DB.Where("sweepstake_id = ?", s.ID).Order("display_order ASC, id ASC").Find(&prizes).Error var winners []models.SweepstakeWinner if s.WinnersSelectedAt != nil { _ = sc.DB.Where("sweepstake_id = ?", s.ID).Find(&winners).Error } hasEntered := false visualPlayedAt := (*time.Time)(nil) myEntriesCount := int64(0) canEnter := false if uid, ok := c.Get("userID"); ok && uid != nil { // Count valid entries for current user _ = sc.DB.Model(&models.SweepstakeEntry{}).Where("sweepstake_id = ? AND user_id = ? AND status = ?", s.ID, uid.(uint), "valid").Count(&myEntriesCount).Error hasEntered = myEntriesCount > 0 // Determine if user can still enter (within time window and below per-user limit) maxPer := s.MaxEntriesPerUser if maxPer <= 0 { maxPer = 1 } canEnter = now.After(s.StartAt) && now.Before(s.EndAt) && myEntriesCount < int64(maxPer) // Keep the first entry's visual flag if exists var e models.SweepstakeEntry if err := sc.DB.Where("sweepstake_id = ? AND user_id = ?", s.ID, uid.(uint)).Order("id ASC").First(&e).Error; err == nil { visualPlayedAt = e.VisualPlayedAt } } c.JSON(http.StatusOK, gin.H{ "sweepstake": s, "prizes": prizes, "winners": winners, "state": state, "has_entered": hasEntered, "visual_played_at": visualPlayedAt, "my_entries_count": myEntriesCount, "can_enter": canEnter, }) } // Admin: list entries func (sc *SweepstakesController) AdminEntries(c *gin.Context) { id := c.Param("id") var items []models.SweepstakeEntry if err := sc.DB.Where("sweepstake_id = ?", id).Order("created_at DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Admin: list winners func (sc *SweepstakesController) AdminWinners(c *gin.Context) { id := c.Param("id") var items []models.SweepstakeWinner if err := sc.DB.Where("sweepstake_id = ?", id).Order("created_at ASC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed"}) return } c.JSON(http.StatusOK, gin.H{"items": items}) } // Admin: finalize (pick winners now) func (sc *SweepstakesController) AdminFinalize(c *gin.Context) { id := c.Param("id") var s models.Sweepstake if err := sc.DB.First(&s, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } var body struct { Seed string `json:"seed"` } _ = c.ShouldBindJSON(&body) svc := services.NewSweepstakesService(sc.DB, sc.Email) if err := svc.FinalizeSweepstake(&s, strings.TrimSpace(body.Seed)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to finalize"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // Helpers func ifEmpty(v string, d string) string { if strings.TrimSpace(v) == "" { return d } return strings.TrimSpace(v) } func ifZero(v int, d int) int { if v == 0 { return d } return v } // Helpers for update coercion func toString(v interface{}) string { switch t := v.(type) { case string: return t case []byte: return string(t) default: return strings.TrimSpace(strings.ReplaceAll(strings.TrimSpace(fmt.Sprintf("%v", v)), "\n", " ")) } } func toNonNegInt64(v interface{}) int64 { switch n := v.(type) { case int64: if n < 0 { return 0 } return n case int: if n < 0 { return 0 } return int64(n) case float64: if n < 0 { return 0 } return int64(n) case float32: if n < 0 { return 0 } return int64(n) case string: if strings.TrimSpace(n) == "" { return 0 } if f, err := strconv.ParseFloat(n, 64); err == nil { if f < 0 { return 0 } return int64(f) } return 0 default: return 0 } }