Files
website/internal/handlers/lottery/tickets/ticket_handler.go

422 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 Gins 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).
//Dont 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 (its correct):
//Gin → SCS.LoadAndSave → NoSurf → http.Server
//If you still get the warning after switching to c.Redirect + c.Abort(), tell me which handler its coming from and Ill point to the exact double-write.