// Package accountTicketHandlers // Path: /internal/handlers/account/tickets/ // File: add.go // // Purpose // Renders & processes the Add Ticket form for authenticated users. // // Responsibilities // 1) Validate user input (game type, draw date, balls and optional bonuses) // 2) Convert string form values into typed model fields // 3) Save through storage layer (InsertTicket) // 4) Prevent DB access from unauthenticated contexts // 5) Use PRG pattern (POST/Redirect/GET) // // Notes // - No direct SQL here — storage package enforces constraints // - CSRF provided via nosurf // - TODO: Replace inline session key with central sessionkeys.UserID package accountTicketHandlers import ( "net/http" "strconv" "time" templateHandlers "synlotto-website/internal/handlers/template" templateHelpers "synlotto-website/internal/helpers/template" ticketStorage "synlotto-website/internal/storage/tickets" "synlotto-website/internal/models" "synlotto-website/internal/platform/bootstrap" "github.com/gin-gonic/gin" "github.com/justinas/nosurf" ) // TODO: Replace with centralized key from sessionkeys package const sessionKeyUserID = "UserID" func AddGet(c *gin.Context) { app := c.MustGet("app").(*bootstrap.App) data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request) ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data) tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/tickets/add_ticket.html") c.Header("Content-Type", "text/html; charset=utf-8") if err := tmpl.ExecuteTemplate(c.Writer, "account/tickets/add_ticket.html", ctx); err != nil { c.String(http.StatusInternalServerError, "render error: %v", err) return } c.Status(http.StatusOK) } func AddPost(c *gin.Context) { app := c.MustGet("app").(*bootstrap.App) var f addForm _ = c.ShouldBind(&f) f.Errors = map[string]string{} // Validate required fields if f.GameType == "" { f.Errors["game"] = "Game type is required." } if f.DrawDate == "" { f.Errors["draw_date"] = "Draw date is required." } balls, ballErrs := parseBalls(f.Ball1, f.Ball2, f.Ball3, f.Ball4, f.Ball5) for k, v := range ballErrs { f.Errors[k] = v } var drawDate time.Time if f.DrawDate != "" { if d, err := time.Parse("2006-01-02", f.DrawDate); err == nil { drawDate = d } else { f.Errors["draw_date"] = "Invalid date (use YYYY-MM-DD)." } } var bonus1Ptr, bonus2Ptr *int if f.Bonus1 != "" { if n, err := strconv.Atoi(f.Bonus1); err == nil { bonus1Ptr = &n } else { f.Errors["bonus1"] = "Bonus 1 must be a number." } } if f.Bonus2 != "" { if n, err := strconv.Atoi(f.Bonus2); err == nil { bonus2Ptr = &n } else { f.Errors["bonus2"] = "Bonus 2 must be a number." } } if len(f.Errors) > 0 { f.CSRFToken = nosurf.Token(c.Request) c.HTML(http.StatusUnprocessableEntity, "account/tickets/add_ticket.html", gin.H{ "title": "Add Ticket", "form": f, }) return } // Build the ticket model expected by ticketStorage.InsertTicket ticket := models.Ticket{ GameType: f.GameType, DrawDate: drawDate, Ball1: balls[0], Ball2: balls[1], Ball3: balls[2], Ball4: balls[3], Ball5: balls[4], Bonus1: bonus1Ptr, Bonus2: bonus2Ptr, // TODO: populate UserID from session when per-user tickets enabled } if err := ticketStorage.InsertTicket(app.DB, ticket); err != nil { // optional: set flash and re-render f.Errors["form"] = "Could not save ticket. Please try again." f.CSRFToken = nosurf.Token(c.Request) c.HTML(http.StatusInternalServerError, "account/tickets/add_ticket.html", gin.H{ "title": "Add Ticket", "form": f, }) return } c.Redirect(http.StatusSeeOther, "/account/tickets") } // helpers func parseBalls(b1, b2, b3, b4, b5 string) ([5]int, map[string]string) { errs := map[string]string{} toInt := func(name, v string) (int, bool) { n, err := strconv.Atoi(v) if err != nil { errs[name] = "Must be a number." return 0, false } return n, true } var out [5]int ok := true if out[0], ok = toInt("ball1", b1); !ok { } if out[1], ok = toInt("ball2", b2); !ok { } if out[2], ok = toInt("ball3", b3); !ok { } if out[3], ok = toInt("ball4", b4); !ok { } if out[4], ok = toInt("ball5", b5); !ok { } return out, errs }