// internal/handlers/lottery/tickets/ticket_handler.go package handlers import ( "database/sql" "fmt" "io" "log" "net/http" "os" "strconv" "time" templateHandlers "synlotto-website/internal/handlers/template" securityHelpers "synlotto-website/internal/helpers/security" templateHelpers "synlotto-website/internal/helpers/template" draws "synlotto-website/internal/services/draws" "synlotto-website/internal/helpers" "synlotto-website/internal/models" "synlotto-website/internal/platform/bootstrap" "github.com/justinas/nosurf" ) // AddTicket renders the add-ticket form (GET) and handles multi-line ticket submission (POST). func AddTicket(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { rows, err := app.DB.Query(` SELECT DISTINCT draw_date FROM results_thunderball ORDER BY draw_date DESC `) if err != nil { log.Println("❌ Failed to load draw dates:", err) http.Error(w, "Unable to load draw dates", http.StatusInternalServerError) return } defer rows.Close() var drawDates []string for rows.Next() { var date string if err := rows.Scan(&date); err == nil { drawDates = append(drawDates, date) } } // Use shared template data builder (expects *bootstrap.App) data := templateHandlers.BuildTemplateData(app, w, r) context := templateHelpers.TemplateContext(w, r, data) context["CSRFToken"] = nosurf.Token(r) context["DrawDates"] = drawDates tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "web/templates/account/tickets/add_ticket.html") if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { log.Println("❌ Template render error:", err) http.Error(w, "Error rendering form", http.StatusInternalServerError) } return } if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "Invalid form", http.StatusBadRequest) log.Println("❌ Failed to parse form:", err) return } userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { http.Redirect(w, r, "/account/login", http.StatusSeeOther) return } game := r.FormValue("game_type") drawDateStr := r.FormValue("draw_date") purchaseMethod := r.FormValue("purchase_method") purchaseDate := r.FormValue("purchase_date") purchaseTime := r.FormValue("purchase_time") dt, err := helpers.ParseDrawDate(drawDateStr) if err != nil { http.Error(w, "Invalid draw date", http.StatusBadRequest) return } drawDateDB := helpers.FormatDrawDate(dt) // "YYYY-MM-DD" if purchaseTime != "" { purchaseDate += "T" + purchaseTime } imagePath := "" file, handler, err := r.FormFile("ticket_image") if err == nil && handler != nil { defer file.Close() filename := fmt.Sprintf("uploads/ticket_%d_%s", time.Now().UnixNano(), handler.Filename) out, err := os.Create(filename) if err == nil { defer out.Close() _, _ = io.Copy(out, file) imagePath = filename } } var ballCount, bonusCount int switch game { case "Thunderball": ballCount, bonusCount = 5, 1 case "Lotto": ballCount, bonusCount = 6, 0 case "EuroMillions": ballCount, bonusCount = 5, 2 case "SetForLife": ballCount, bonusCount = 5, 1 default: http.Error(w, "Unsupported game type", http.StatusBadRequest) return } balls := make([][]int, ballCount) bonuses := make([][]int, bonusCount) for i := 1; i <= ballCount; i++ { field := fmt.Sprintf("ball%d[]", i) balls[i-1] = helpers.ParseIntSlice(r.Form[field]) log.Printf("🔢 %s: %v", field, balls[i-1]) } for i := 1; i <= bonusCount; i++ { field := fmt.Sprintf("bonus%d[]", i) bonuses[i-1] = helpers.ParseIntSlice(r.Form[field]) log.Printf("🎯 %s: %v", field, bonuses[i-1]) } lineCount := 0 if len(balls) > 0 { lineCount = len(balls[0]) } log.Println("🧾 Total lines to insert:", lineCount) for i := 0; i < lineCount; i++ { b := make([]int, 6) bo := make([]int, 2) valid := true for j := 0; j < ballCount; j++ { if j < len(balls) && i < len(balls[j]) { b[j] = balls[j][i] if b[j] == 0 { valid = false } } } for j := 0; j < bonusCount; j++ { if j < len(bonuses) && i < len(bonuses[j]) { bo[j] = bonuses[j][i] if bo[j] == 0 { valid = false } } } if !valid { log.Printf("⚠️ Skipping invalid line %d (incomplete values)", i+1) continue } if _, err := app.DB.Exec(` INSERT INTO my_tickets ( userId, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6, bonus1, bonus2, purchase_method, purchase_date, image_path ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, userID, game, drawDateDB, b[0], b[1], b[2], b[3], b[4], b[5], bo[0], bo[1], purchaseMethod, purchaseDate, imagePath, ); err != nil { log.Println("❌ Failed to insert ticket line:", err) } else { log.Printf("✅ Ticket line %d saved", i+1) } } http.Redirect(w, r, "/tickets", http.StatusSeeOther) } } // SubmitTicket handles alternate multipart ticket submission (POST-only). func SubmitTicket(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "Invalid form", http.StatusBadRequest) return } userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { http.Redirect(w, r, "/account/login", http.StatusSeeOther) return } game := r.FormValue("game_type") drawDateStr := r.FormValue("draw_date") purchaseMethod := r.FormValue("purchase_method") purchaseDate := r.FormValue("purchase_date") purchaseTime := r.FormValue("purchase_time") dt, err := helpers.ParseDrawDate(drawDateStr) if err != nil { http.Error(w, "Invalid draw date", http.StatusBadRequest) return } drawDateDB := helpers.FormatDrawDate(dt) if purchaseTime != "" { purchaseDate += "T" + purchaseTime } imagePath := "" file, handler, err := r.FormFile("ticket_image") if err == nil && handler != nil { defer file.Close() filename := fmt.Sprintf("uploads/ticket_%d_%s", time.Now().UnixNano(), handler.Filename) out, err := os.Create(filename) if err == nil { defer out.Close() _, _ = io.Copy(out, file) imagePath = filename } } const ballCount = 6 const bonusCount = 2 balls := make([][]int, ballCount) bonuses := make([][]int, bonusCount) for i := 1; i <= ballCount; i++ { balls[i-1] = helpers.ParseIntSlice(r.Form["ball"+strconv.Itoa(i)]) } for i := 1; i <= bonusCount; i++ { bonuses[i-1] = helpers.ParseIntSlice(r.Form["bonus"+strconv.Itoa(i)]) } lineCount := len(balls[0]) for i := 0; i < lineCount; i++ { var b [6]int var bo [2]int for j := 0; j < ballCount; j++ { if j < len(balls) && i < len(balls[j]) { b[j] = balls[j][i] } } for j := 0; j < bonusCount; j++ { if j < len(bonuses) && i < len(bonuses[j]) { bo[j] = bonuses[j][i] } } if _, err := app.DB.Exec(` INSERT INTO my_tickets ( user_id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6, bonus1, bonus2, purchase_method, purchase_date, image_path ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, userID, game, drawDateDB, b[0], b[1], b[2], b[3], b[4], b[5], bo[0], bo[1], purchaseMethod, purchaseDate, imagePath, ); err != nil { log.Println("❌ Insert failed:", err) } } http.Redirect(w, r, "/tickets", http.StatusSeeOther) } } // GetMyTickets lists the current user's tickets. func GetMyTickets(app *bootstrap.App) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Use shared template data builder (ensures user/flash/notifications present) data := templateHandlers.BuildTemplateData(app, w, r) context := templateHelpers.TemplateContext(w, r, data) userID, ok := securityHelpers.GetCurrentUserID(app.SessionManager, r) if !ok { http.Redirect(w, r, "/account/login", http.StatusSeeOther) return } var tickets []models.Ticket rows, err := app.DB.Query(` SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6, bonus1, bonus2, purchase_method, purchase_date, image_path, duplicate, matched_main, matched_bonus, prize_tier, is_winner, prize_label, prize_amount FROM my_tickets WHERE userid = ? ORDER BY draw_date DESC, created_at DESC `, userID) if err != nil { log.Println("❌ Query failed:", err) http.Error(w, "Could not load tickets", http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var t models.Ticket var drawDateStr string // ← add var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64 var matchedMain, matchedBonus sql.NullInt64 var prizeTier sql.NullString var isWinner sql.NullBool var prizeLabel sql.NullString var prizeAmount sql.NullFloat64 if err := rows.Scan( &t.Id, &t.GameType, &drawDateStr, // ← was &t.DrawDate &b1, &b2, &b3, &b4, &b5, &b6, &bo1, &bo2, &t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate, &matchedMain, &matchedBonus, &prizeTier, &isWinner, &prizeLabel, &prizeAmount, ); err != nil { log.Println("⚠️ Failed to scan ticket row:", err) continue } // Parse into time.Time (UTC) if dt, err := helpers.ParseDrawDate(drawDateStr); err == nil { t.DrawDate = dt } // Normalize fields t.Ball1 = int(b1.Int64) t.Ball2 = int(b2.Int64) t.Ball3 = int(b3.Int64) t.Ball4 = int(b4.Int64) t.Ball5 = int(b5.Int64) t.Ball6 = int(b6.Int64) t.Bonus1 = helpers.IntPtrIfValid(bo1) t.Bonus2 = helpers.IntPtrIfValid(bo2) if matchedMain.Valid { t.MatchedMain = int(matchedMain.Int64) } if matchedBonus.Valid { t.MatchedBonus = int(matchedBonus.Int64) } if prizeTier.Valid { t.PrizeTier = prizeTier.String } if isWinner.Valid { t.IsWinner = isWinner.Bool } if prizeLabel.Valid { t.PrizeLabel = prizeLabel.String } if prizeAmount.Valid { t.PrizeAmount = prizeAmount.Float64 } // Derived fields for templates t.Balls = helpers.BuildBallsSlice(t) t.BonusBalls = helpers.BuildBonusSlice(t) // Fetch matching draw info draw := draws.GetDrawResultForTicket(app.DB, t.GameType, helpers.FormatDrawDate(t.DrawDate)) t.MatchedDraw = draw tickets = append(tickets, t) } context["Tickets"] = tickets tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "web/templates/account/tickets/my_tickets.html") if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil { log.Println("❌ Template error:", err) http.Error(w, "Error rendering page", http.StatusInternalServerError) } } } // ToDo // http: superfluous response.WriteHeader call (from SCS) //This happens when headers are written twice in a request. With SCS, it sets cookies in WriteHeader. If something else already wrote the headers (or wrote them again), you see this warning. //Common culprits & fixes: //Use Gin’s redirect instead of the stdlib one: // Replace: //http.Redirect(w, r, "/account/login", http.StatusSeeOther) // With: //c.Redirect(http.StatusSeeOther, "/account/login") //c.Abort() // stop further handlers writing //Do this everywhere you redirect (signup, login, logout). //Don’t call two status-writes. For template GETs, this is fine: //c.Status(http.StatusOK) //_ = tmpl.ExecuteTemplate(c.Writer, "layout", ctx) // writes body once //Just make sure you never write another header after that. //Keep your wrapping order as you have it (it’s correct): //Gin → SCS.LoadAndSave → NoSurf → http.Server //If you still get the warning after switching to c.Redirect + c.Abort(), tell me which handler it’s coming from and I’ll point to the exact double-write.