400 lines
11 KiB
Go
400 lines
11 KiB
Go
// 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")
|
||
drawDate := r.FormValue("draw_date")
|
||
purchaseMethod := r.FormValue("purchase_method")
|
||
purchaseDate := r.FormValue("purchase_date")
|
||
purchaseTime := r.FormValue("purchase_time")
|
||
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, drawDate,
|
||
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")
|
||
drawDate := r.FormValue("draw_date")
|
||
purchaseMethod := r.FormValue("purchase_method")
|
||
purchaseDate := r.FormValue("purchase_date")
|
||
purchaseTime := r.FormValue("purchase_time")
|
||
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, drawDate,
|
||
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 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, &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
|
||
}
|
||
|
||
// 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, 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.
|