diff --git a/handlers/ticket_matcher.go b/handlers/ticket_matcher.go new file mode 100644 index 0000000..fb8e22a --- /dev/null +++ b/handlers/ticket_matcher.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "synlotto-website/models" +) + +func MatchTicketToDraw(ticket models.Ticket, 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 "" +} diff --git a/main.go b/main.go index cd81b17..4f7cdcb 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "synlotto-website/helpers" "synlotto-website/middleware" "synlotto-website/models" + services "synlotto-website/services/admin" "synlotto-website/storage" "github.com/gorilla/csrf" @@ -38,6 +39,9 @@ func main() { mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) + // Admin Pages + mux.HandleFunc("/admin/match", services.AdminMatchHandler) + log.Println("🌐 Running on http://localhost:8080") http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux))) } diff --git a/models/match.go b/models/match.go index 672a1fc..49389f6 100644 --- a/models/match.go +++ b/models/match.go @@ -23,3 +23,15 @@ type MatchResult struct { IsWinner bool MatchedDrawID int } + +type PrizeRule struct { + Game string + MainMatches int + BonusMatches int + Tier string +} + +type MatchRunStats struct { + TicketsMatched int + WinnersFound int +} diff --git a/rules/thunderball.go b/rules/thunderball.go new file mode 100644 index 0000000..7479006 --- /dev/null +++ b/rules/thunderball.go @@ -0,0 +1,9 @@ +package rules + +import "synlotto-website/models" + +var ThunderballPrizeRules = []models.PrizeRule{ + {Game: "Thunderball", MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"}, + {Game: "Thunderball", MainMatches: 5, BonusMatches: 0, Tier: "Second"}, + {Game: "Thunderball", MainMatches: 4, BonusMatches: 1, Tier: "Third"}, +} diff --git a/services/admin/manualtriggers.go b/services/admin/manualtriggers.go new file mode 100644 index 0000000..8af7f16 --- /dev/null +++ b/services/admin/manualtriggers.go @@ -0,0 +1,21 @@ +package services + +import ( + "database/sql" + "net/http" + "strconv" + "synlotto-website/services" +) + +var db *sql.DB + +func AdminMatchHandler(w http.ResponseWriter, r *http.Request) { + stats, err := services.RunTicketMatching(db, "manual") + if err != nil { + http.Error(w, "Matching failed: "+err.Error(), 500) + return + } + http.Redirect(w, r, "/admin?flash=Matched "+ + strconv.Itoa(stats.TicketsMatched)+" tickets, "+ + strconv.Itoa(stats.WinnersFound)+" winners.", http.StatusSeeOther) +} diff --git a/services/ticketchecker.go b/services/ticketchecker.go new file mode 100644 index 0000000..9c1af4e --- /dev/null +++ b/services/ticketchecker.go @@ -0,0 +1,90 @@ +package services + +import ( + "database/sql" + "log" + "synlotto-website/handlers" + "synlotto-website/models" + "synlotto-website/rules" +) + +func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { + stats := models.MatchRunStats{} + + rows, err := db.Query(` + SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, bonus1 + FROM my_tickets + WHERE matched_draw_id IS NULL + `) + if err != nil { + return stats, err + } + defer rows.Close() + + for rows.Next() { + var t models.Ticket + var b1, b2, b3, b4, b5, bonus sql.NullInt64 + if err := rows.Scan(&t.ID, &t.GameType, &t.DrawDate, &b1, &b2, &b3, &b4, &b5, &bonus); err != nil { + continue + } + + t.Balls = []int{int(b1.Int64), int(b2.Int64), int(b3.Int64), int(b4.Int64), int(b5.Int64)} + if bonus.Valid { + t.BonusBalls = []int{int(bonus.Int64)} + } else { + t.BonusBalls = nil + } + + draw := GetDrawResultForTicket(db, t.GameType, t.DrawDate) + result := handlers.MatchTicketToDraw(t, draw, rules.ThunderballPrizeRules) + + if result.MatchedDrawID == 0 { + continue // no draw found + } + + _, err := db.Exec(` + UPDATE my_tickets SET + matched_draw_id = ?, matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ? + WHERE id = ?`, + result.MatchedDrawID, result.MatchedMain, result.MatchedBonus, result.PrizeTier, result.IsWinner, t.ID, + ) + if err != nil { + continue + } + + stats.TicketsMatched++ + if result.IsWinner { + stats.WinnersFound++ + } + } + + _, _ = db.Exec(` + INSERT INTO matching_log (triggered_by, tickets_matched, winners_found) + VALUES (?, ?, ?)`, + triggeredBy, stats.TicketsMatched, stats.WinnersFound, + ) + + return stats, nil +} + +func GetDrawResultForTicket(db *sql.DB, game string, drawDate string) models.DrawResult { + var result models.DrawResult + query := ` + SELECT id, ball1, ball2, ball3, ball4, ball5, bonus1 + FROM results_thunderball + WHERE draw_date = ? + ` + var b1, b2, b3, b4, b5, bonus sql.NullInt64 + err := db.QueryRow(query, drawDate).Scan(&result.DrawID, &b1, &b2, &b3, &b4, &b5, &bonus) + if err != nil { + log.Printf("No draw found for %s %s: %v", game, drawDate, err) + return result + } + result.GameType = game + result.DrawDate = drawDate + result.Balls = []int{int(b1.Int64), int(b2.Int64), int(b3.Int64), int(b4.Int64), int(b5.Int64)} + if bonus.Valid { + result.BonusBalls = []int{int(bonus.Int64)} + } + return result +} diff --git a/storage/db.go b/storage/db.go index e41c86c..a109bde 100644 --- a/storage/db.go +++ b/storage/db.go @@ -99,5 +99,19 @@ func InitDB(filepath string) *sql.DB { log.Fatal("❌ Failed to create Users table:", err) } + createLogTicketMatchingTable := ` + CREATE TABLE IF NOT EXISTS log_ticket_matching ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + triggered_by TEXT, + run_at DATETIME DEFAULT CURRENT_TIMESTAMP, + tickets_matched INTEGER, + winners_found INTEGER, + notes TEXT + );` + + if _, err := db.Exec(createLogTicketMatchingTable); err != nil { + log.Fatal("❌ Failed to create ticket matching log table:", err) + } + return db } diff --git a/templates/admin/triggers.html b/templates/admin/triggers.html new file mode 100644 index 0000000..4ccfe3d --- /dev/null +++ b/templates/admin/triggers.html @@ -0,0 +1,4 @@ +
\ No newline at end of file