Refactor and remove sqlite and replace with MySQL

This commit is contained in:
2025-10-23 18:43:31 +01:00
parent d53e27eea8
commit 21ebc9c34b
139 changed files with 1013 additions and 529 deletions

View File

@@ -0,0 +1,60 @@
package handlers
import (
"database/sql"
"log"
"net/http"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/models"
"synlotto-website/storage"
)
func NewDraw(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := models.TemplateData{}
context := templateHelpers.TemplateContext(w, r, data)
context["Page"] = "new_draw"
context["Data"] = nil
tmpl := templateHelpers.LoadTemplateFiles("new_draw.html", "templates/admin/draws/new_draw.html") // ToDo: may need removing or moving add draw should be admin functionality and only when manually required. Potential live drawing of numbers in the future.
err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil {
log.Println("❌ Template error:", err)
http.Error(w, "Error rendering form", http.StatusInternalServerError)
}
}
}
func Submit(db *sql.DB, w http.ResponseWriter, r *http.Request) {
log.Println("📝 Form submission received")
_ = r.ParseForm()
draw := models.ThunderballResult{
DrawDate: r.FormValue("date"),
Machine: r.FormValue("machine"),
BallSet: helpers.Atoi(r.FormValue("ballSet")),
Ball1: helpers.Atoi(r.FormValue("ball1")),
Ball2: helpers.Atoi(r.FormValue("ball2")),
Ball3: helpers.Atoi(r.FormValue("ball3")),
Ball4: helpers.Atoi(r.FormValue("ball4")),
Ball5: helpers.Atoi(r.FormValue("ball5")),
Thunderball: helpers.Atoi(r.FormValue("thunderball")),
}
err := storage.InsertThunderballResult(db, draw)
if err != nil {
log.Println("❌ Failed to insert draw:", err)
http.Error(w, "Failed to save draw", http.StatusInternalServerError)
return
}
log.Printf("📅 %s | 🛠 %s | 🎱 %d | 🔢 %d,%d,%d,%d,%d | ⚡ %d\n",
draw.DrawDate, draw.Machine, draw.BallSet,
draw.Ball1, draw.Ball2, draw.Ball3, draw.Ball4, draw.Ball5, draw.Thunderball)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

View File

@@ -0,0 +1,203 @@
package handlers
import (
"database/sql"
"fmt"
"log"
"net/http"
templateHandlers "synlotto-website/handlers/template"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/models"
"synlotto-website/storage"
)
func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
tmpl := templateHelpers.LoadTemplateFiles("create-syndicate.html", "templates/syndicate/create.html")
tmpl.ExecuteTemplate(w, "layout", context)
case http.MethodPost:
name := r.FormValue("name")
description := r.FormValue("description")
userId, ok := securityHelpers.GetCurrentUserID(r)
if !ok || name == "" {
templateHelpers.SetFlash(w, r, "Invalid data submitted")
http.Redirect(w, r, "/syndicate/create", http.StatusSeeOther)
return
}
_, err := storage.CreateSyndicate(db, userId, name, description)
if err != nil {
log.Printf("❌ CreateSyndicate failed: %v", err)
templateHelpers.SetFlash(w, r, "Failed to create syndicate")
} else {
templateHelpers.SetFlash(w, r, "Syndicate created successfully")
}
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
default:
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
}
}
}
func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors.
return
}
managed := storage.GetSyndicatesByOwner(db, userID)
member := storage.GetSyndicatesByMember(db, userID)
managedMap := make(map[int]bool)
for _, s := range managed {
managedMap[s.ID] = true
}
var filteredJoined []models.Syndicate
for _, s := range member {
if !managedMap[s.ID] {
filteredJoined = append(filteredJoined, s)
}
}
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["ManagedSyndicates"] = managed
context["JoinedSyndicates"] = filteredJoined
tmpl := templateHelpers.LoadTemplateFiles("syndicates.html", "templates/syndicate/index.html")
tmpl.ExecuteTemplate(w, "layout", context)
}
}
func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
syndicate, err := storage.GetSyndicateByID(db, syndicateID)
if err != nil || syndicate == nil {
templateHelpers.RenderError(w, r, 404)
return
}
isManager := userID == syndicate.OwnerID
isMember := storage.IsSyndicateMember(db, syndicateID, userID)
if !isManager && !isMember {
templateHelpers.RenderError(w, r, 403)
return
}
members := storage.GetSyndicateMembers(db, syndicateID)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["Syndicate"] = syndicate
context["Members"] = members
context["IsManager"] = isManager
tmpl := templateHelpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicate/view.html")
tmpl.ExecuteTemplate(w, "layout", context)
}
}
func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403)
return
}
syndicateId := helpers.Atoi(r.URL.Query().Get("id"))
syndicate, err := storage.GetSyndicateByID(db, syndicateId)
if err != nil || syndicate.OwnerID != userID {
templateHelpers.RenderError(w, r, 403)
return
}
switch r.Method {
case http.MethodGet:
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["Syndicate"] = syndicate
tmpl := templateHelpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicate/log_ticket.html")
tmpl.ExecuteTemplate(w, "layout", context)
case http.MethodPost:
gameType := r.FormValue("game_type")
drawDate := r.FormValue("draw_date")
method := r.FormValue("purchase_method")
err := storage.InsertTicket(db, models.Ticket{
UserId: userID,
GameType: gameType,
DrawDate: drawDate,
PurchaseMethod: method,
SyndicateId: &syndicateId,
// ToDo image path
})
if err != nil {
templateHelpers.SetFlash(w, r, "Failed to add ticket.")
} else {
templateHelpers.SetFlash(w, r, "Ticket added for syndicate.")
}
http.Redirect(w, r, fmt.Sprintf("/syndicate/view?id=%d", syndicateId), http.StatusSeeOther)
default:
templateHelpers.RenderError(w, r, 405)
}
}
}
func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
if syndicateID == 0 {
templateHelpers.RenderError(w, r, 400)
return
}
if !storage.IsSyndicateMember(db, syndicateID, userID) {
templateHelpers.RenderError(w, r, 403)
return
}
tickets := storage.GetSyndicateTickets(db, syndicateID)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["SyndicateID"] = syndicateID
context["Tickets"] = tickets
tmpl := templateHelpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicate/tickets.html")
tmpl.ExecuteTemplate(w, "layout", context)
}
}

View File

@@ -0,0 +1,226 @@
package handlers
import (
"database/sql"
"fmt"
"net/http"
"strconv"
"time"
templateHandlers "synlotto-website/handlers/template"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
"synlotto-website/helpers"
"synlotto-website/storage"
)
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
switch r.Method {
case http.MethodGet:
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["SyndicateID"] = syndicateID
tmpl := templateHelpers.LoadTemplateFiles("invite-syndicate.html", "templates/syndicate/invite.html")
err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil {
templateHelpers.RenderError(w, r, 500)
}
case http.MethodPost:
syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
username := r.FormValue("username")
err := storage.InviteToSyndicate(db, userID, syndicateID, username)
if err != nil {
templateHelpers.SetFlash(w, r, "Failed to send invite: "+err.Error())
} else {
templateHelpers.SetFlash(w, r, "Invite sent successfully.")
}
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
default:
templateHelpers.RenderError(w, r, http.StatusMethodNotAllowed)
}
}
}
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403)
return
}
invites := storage.GetPendingInvites(db, userID)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["Invites"] = invites
tmpl := templateHelpers.LoadTemplateFiles("invites.html", "templates/syndicate/invites.html")
tmpl.ExecuteTemplate(w, "layout", context)
}
}
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403)
return
}
err := storage.AcceptInvite(db, inviteID, userID)
if err != nil {
templateHelpers.SetFlash(w, r, "Failed to accept invite")
} else {
templateHelpers.SetFlash(w, r, "You have joined the syndicate")
}
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
}
}
func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
_ = storage.UpdateInviteStatus(db, inviteID, "declined")
http.Redirect(w, r, "/syndicate/invites", http.StatusSeeOther)
}
}
func CreateInviteToken(db *sql.DB, syndicateID, invitedByID int, ttlHours int) (string, error) {
token, err := securityHelpers.GenerateSecureToken()
if err != nil {
return "", err
}
expires := time.Now().Add(time.Duration(ttlHours) * time.Hour)
_, err = db.Exec(`
INSERT INTO syndicate_invite_tokens (syndicate_id, token, invited_by_user_id, expires_at)
VALUES (?, ?, ?, ?)
`, syndicateID, token, invitedByID, expires)
return token, err
}
func AcceptInviteToken(db *sql.DB, token string, userID int) error {
var syndicateID int
var expiresAt, acceptedAt sql.NullTime
err := db.QueryRow(`
SELECT syndicate_id, expires_at, accepted_at
FROM syndicate_invite_tokens
WHERE token = ?
`, token).Scan(&syndicateID, &expiresAt, &acceptedAt)
if err != nil {
return fmt.Errorf("invalid or expired token")
}
if acceptedAt.Valid || expiresAt.Time.Before(time.Now()) {
return fmt.Errorf("token already used or expired")
}
_, err = db.Exec(`
INSERT INTO syndicate_members (syndicate_id, user_id, role, status, joined_at)
VALUES (?, ?, 'member', 'active', CURRENT_TIMESTAMP)
`, syndicateID, userID)
if err != nil {
return err
}
_, err = db.Exec(`
UPDATE syndicate_invite_tokens
SET accepted_by_user_id = ?, accepted_at = CURRENT_TIMESTAMP
WHERE token = ?
`, userID, token)
return err
}
func GenerateInviteLinkHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
token, err := CreateInviteToken(db, syndicateID, userID, 48)
if err != nil {
templateHelpers.SetFlash(w, r, "Failed to generate invite link.")
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
return
}
origin := r.Host
if r.TLS != nil {
origin = "https://" + origin
} else {
origin = "http://" + origin
}
inviteLink := fmt.Sprintf("%s/syndicate/join?token=%s", origin, token)
templateHelpers.SetFlash(w, r, "Invite link created: "+inviteLink)
http.Redirect(w, r, "/syndicate/view?id="+strconv.Itoa(syndicateID), http.StatusSeeOther)
}
}
func JoinSyndicateWithTokenHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, http.StatusForbidden)
return
}
token := r.URL.Query().Get("token")
if token == "" {
templateHelpers.SetFlash(w, r, "Invalid or missing invite token.")
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
return
}
err := AcceptInviteToken(db, token, userID)
if err != nil {
templateHelpers.SetFlash(w, r, "Failed to join syndicate: "+err.Error())
} else {
templateHelpers.SetFlash(w, r, "You have joined the syndicate!")
}
http.Redirect(w, r, "/syndicate", http.StatusSeeOther)
}
}
func ManageInviteTokensHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
templateHelpers.RenderError(w, r, 403)
return
}
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
if !storage.IsSyndicateManager(db, syndicateID, userID) {
templateHelpers.RenderError(w, r, 403)
return
}
tokens := storage.GetInviteTokensForSyndicate(db, syndicateID)
data := templateHandlers.BuildTemplateData(db, w, r)
context := templateHelpers.TemplateContext(w, r, data)
context["Tokens"] = tokens
context["SyndicateID"] = syndicateID
tmpl := templateHelpers.LoadTemplateFiles("invite-links.html", "templates/syndicate/invite_links.html")
tmpl.ExecuteTemplate(w, "layout", context)
}
}

View File

@@ -0,0 +1,375 @@
package handlers
import (
"database/sql"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
httpHelpers "synlotto-website/helpers/http"
securityHelpers "synlotto-website/helpers/security"
templateHelpers "synlotto-website/helpers/template"
draws "synlotto-website/services/draws"
"synlotto-website/helpers"
"synlotto-website/models"
"github.com/gorilla/csrf"
)
func AddTicket(db *sql.DB) http.HandlerFunc {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
rows, err := 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)
}
}
data := models.TemplateData{}
context := templateHelpers.TemplateContext(w, r, data)
context["csrfField"] = csrf.TemplateField(r)
context["DrawDates"] = drawDates
tmpl := templateHelpers.LoadTemplateFiles("add_ticket.html", "templates/account/tickets/add_ticket.html")
err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil {
log.Println("❌ Template render error:", err)
http.Error(w, "Error rendering form", http.StatusInternalServerError)
}
return
}
err := r.ParseMultipartForm(10 << 20)
if err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
log.Println("❌ Failed to parse form:", err)
return
}
userID, ok := securityHelpers.GetCurrentUserID(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
}
_, err := 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,
)
if err != nil {
log.Println("❌ Failed to insert ticket line:", err)
} else {
log.Printf("✅ Ticket line %d saved", i+1) // ToDo create audit
}
}
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
})
}
func SubmitTicket(db *sql.DB) http.HandlerFunc {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(10 << 20)
if err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
userID, ok := securityHelpers.GetCurrentUserID(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
}
}
ballCount := 6
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]
}
}
_, err := 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,
)
if err != nil {
log.Println("❌ Insert failed:", err)
}
}
http.Redirect(w, r, "/tickets", http.StatusSeeOther)
})
}
func GetMyTickets(db *sql.DB) http.HandlerFunc {
return httpHelpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
data := models.TemplateData{}
var tickets []models.Ticket
context := templateHelpers.TemplateContext(w, r, data)
context["Tickets"] = tickets
userID, ok := securityHelpers.GetCurrentUserID(r)
if !ok {
http.Redirect(w, r, "/account/login", http.StatusSeeOther)
return
}
rows, err := 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
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,
)
if err != nil {
log.Println("⚠️ Failed to scan ticket row:", err)
continue
}
// Build primary number + bonus 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
}
// Build balls slices (for template use)
t.Balls = helpers.BuildBallsSlice(t)
t.BonusBalls = helpers.BuildBonusSlice(t)
// 🎯 Get the actual draw info (used to show which numbers matched)
draw := draws.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
t.MatchedDraw = draw
// ✅ DEBUG
log.Printf("✅ Ticket #%d", t.Id)
log.Printf("Balls: %v", t.Balls)
log.Printf("DrawResult: %+v", draw)
tickets = append(tickets, t)
}
tmpl := templateHelpers.LoadTemplateFiles("my_tickets.html", "templates/account/tickets/my_tickets.html")
err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil {
log.Println("❌ Template error:", err)
http.Error(w, "Error rendering page", http.StatusInternalServerError)
}
})
}

View File

@@ -0,0 +1,44 @@
package handlers
import (
"synlotto-website/models"
)
func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult {
mainMatches := countMatches(ticket.Balls, draw.Balls)
bonusMatches := countMatches(ticket.BonusBalls, draw.BonusBalls)
prizeTier := getPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules)
isWinner := prizeTier != ""
return models.MatchResult{
MatchedDrawID: draw.DrawID,
MatchedMain: mainMatches,
MatchedBonus: bonusMatches,
PrizeTier: prizeTier,
IsWinner: isWinner,
}
}
func countMatches(a, b []int) int {
m := make(map[int]bool)
for _, n := range b {
m[n] = true
}
match := 0
for _, n := range a {
if m[n] {
match++
}
}
return match
}
func getPrizeTier(game string, main, bonus int, rules []models.PrizeRule) string {
for _, rule := range rules {
if rule.Game == game && rule.MainMatches == main && rule.BonusMatches == bonus {
return rule.Tier
}
}
return ""
}