User specific lottery ticket creation
This commit is contained in:
152
internal/handlers/account/tickets/add.go
Normal file
152
internal/handlers/account/tickets/add.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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"
|
||||
|
||||
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) {
|
||||
c.HTML(http.StatusOK, "account/tickets/add_ticket.html", gin.H{
|
||||
"title": "Add Ticket",
|
||||
"csrfToken": nosurf.Token(c.Request),
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user