mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
737 lines
24 KiB
Go
737 lines
24 KiB
Go
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
|
|
}
|
|
}
|