New admin triggers for db maintenance, updating display of prize results and logic fix

This commit is contained in:
2025-03-28 22:52:54 +00:00
parent 593dbb598e
commit 322b4877ed
8 changed files with 269 additions and 21 deletions

View File

@@ -2,9 +2,13 @@ package handlers
import ( import (
"database/sql" "database/sql"
"fmt"
"html/template" "html/template"
"log"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"synlotto-website/helpers" "synlotto-website/helpers"
services "synlotto-website/services/tickets" services "synlotto-website/services/tickets"
) )
@@ -13,7 +17,70 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
context := helpers.TemplateContext(w, r) context := helpers.TemplateContext(w, r)
// Inject flash message if available
if flash := r.URL.Query().Get("flash"); flash != "" {
context["Flash"] = flash
}
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
action := r.FormValue("action")
flashMsg := ""
switch action {
case "match":
stats, err := services.RunTicketMatching(db, "manual")
if err != nil {
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
return
}
flashMsg = fmt.Sprintf("✅ Matched %d tickets, %d winners.", stats.TicketsMatched, stats.WinnersFound)
case "prizes":
err := services.UpdateMissingPrizes(db)
if err != nil {
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
return
}
flashMsg = "✅ Missing prizes updated."
case "run_all":
stats, err := services.RunTicketMatching(db, "manual")
if err != nil {
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
return
}
err = services.UpdateMissingPrizes(db)
if err != nil {
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
return
}
flashMsg = fmt.Sprintf("✅ Matched %d tickets, %d winners. Prizes updated.", stats.TicketsMatched, stats.WinnersFound)
default:
flashMsg = "⚠️ Unknown action."
}
// Redirect back with flash message
http.Redirect(w, r, "/admin/triggers?flash="+url.QueryEscape(flashMsg), http.StatusSeeOther)
return
}
// Render the admin trigger page
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
"templates/layout.html",
"templates/admin/triggers.html",
))
err := tmpl.ExecuteTemplate(w, "layout", context)
if err != nil {
log.Println("Template error:", err)
http.Error(w, "Failed to load page", http.StatusInternalServerError)
}
}
}
func MatchTicketsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stats, err := services.RunTicketMatching(db, "manual") stats, err := services.RunTicketMatching(db, "manual")
if err != nil { if err != nil {
http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
@@ -23,17 +90,37 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
http.Redirect(w, r, "/admin/triggers?flash=Matched "+ http.Redirect(w, r, "/admin/triggers?flash=Matched "+
strconv.Itoa(stats.TicketsMatched)+" tickets, "+ strconv.Itoa(stats.TicketsMatched)+" tickets, "+
strconv.Itoa(stats.WinnersFound)+" winners.", http.StatusSeeOther) strconv.Itoa(stats.WinnersFound)+" winners.", http.StatusSeeOther)
}
}
func UpdateMissingPrizesHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := services.UpdateMissingPrizes(db)
if err != nil {
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
return return
} }
// Render admin trigger page http.Redirect(w, r, "/admin/triggers?flash=Updated missing prize data.", http.StatusSeeOther)
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( }
"templates/layout.html", }
"templates/admin/triggers.html",
)) func RunAllHandler(db *sql.DB) http.HandlerFunc {
err := tmpl.ExecuteTemplate(w, "layout", context) return func(w http.ResponseWriter, r *http.Request) {
stats, err := services.RunTicketMatching(db, "manual")
if err != nil { if err != nil {
http.Error(w, "Failed to load page", http.StatusInternalServerError) http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError)
} return
}
err = services.UpdateMissingPrizes(db)
if err != nil {
http.Error(w, "Prize update failed: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/admin/triggers?flash=Matched "+
strconv.Itoa(stats.TicketsMatched)+" tickets, "+
strconv.Itoa(stats.WinnersFound)+" winners. Prizes updated.", http.StatusSeeOther)
} }
} }

View File

@@ -1,24 +1,46 @@
package matcher package matcher
import ( import (
"database/sql"
"fmt"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/models" "synlotto-website/models"
thunderballRules "synlotto-website/rules"
) )
func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult { func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule, db *sql.DB) models.MatchResult {
mainMatches := helpers.CountMatches(ticket.Balls, draw.Balls) mainMatches := helpers.CountMatches(ticket.Balls, draw.Balls)
bonusMatches := helpers.CountMatches(ticket.BonusBalls, draw.BonusBalls) bonusMatches := helpers.CountMatches(ticket.BonusBalls, draw.BonusBalls)
prizeTier := getPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules) prizeTier := getPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules)
isWinner := prizeTier != "" isWinner := prizeTier != ""
return models.MatchResult{ result := models.MatchResult{
MatchedDrawID: draw.DrawID, MatchedDrawID: draw.DrawID,
MatchedMain: mainMatches, MatchedMain: mainMatches,
MatchedBonus: bonusMatches, MatchedBonus: bonusMatches,
PrizeTier: prizeTier, PrizeTier: prizeTier,
IsWinner: isWinner, IsWinner: isWinner,
} }
if ticket.GameType == "Thunderball" && isWinner {
if idx, ok := thunderballRules.GetThunderballPrizeIndex(mainMatches, bonusMatches); ok {
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
var amount int
err := db.QueryRow(query, draw.DrawDate).Scan(&amount)
if err == nil {
result.PrizeAmount = float64(amount)
if amount == 0 {
result.PrizeLabel = "Free Ticket"
} else {
result.PrizeLabel = fmt.Sprintf("£%.2f", float64(amount))
}
}
}
}
return result
} }
func getPrizeTier(game string, main, bonus int, rules []models.PrizeRule) string { func getPrizeTier(game string, main, bonus int, rules []models.PrizeRule) string {

View File

@@ -22,6 +22,9 @@ type MatchResult struct {
PrizeTier string PrizeTier string
IsWinner bool IsWinner bool
MatchedDrawID int MatchedDrawID int
PrizeAmount float64
PrizeLabel string
} }
type PrizeRule struct { type PrizeRule struct {

View File

@@ -25,4 +25,6 @@ type Ticket struct {
Balls []int Balls []int
BonusBalls []int BonusBalls []int
MatchedDraw DrawResult MatchedDraw DrawResult
PrizeAmount float64 `db:"prize_amount"`
PrizeLabel string `db:"prize_label"`
} }

View File

@@ -2,8 +2,45 @@ package rules
import "synlotto-website/models" import "synlotto-website/models"
var ThunderballPrizeRules = []models.PrizeRule{ type PrizeInfo struct {
{Game: "Thunderball", MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"}, Tier string
{Game: "Thunderball", MainMatches: 5, BonusMatches: 0, Tier: "Second"}, Amount float64
{Game: "Thunderball", MainMatches: 4, BonusMatches: 1, Tier: "Third"}, Label string
}
var ThunderballPrizeRules = []models.PrizeRule{
{Game: "Thunderball", MainMatches: 0, BonusMatches: 1, Tier: "Tier 1"},
{Game: "Thunderball", MainMatches: 1, BonusMatches: 1, Tier: "Tier 2"},
{Game: "Thunderball", MainMatches: 2, BonusMatches: 1, Tier: "Tier 3"},
{Game: "Thunderball", MainMatches: 3, BonusMatches: 0, Tier: "Tier 4"},
{Game: "Thunderball", MainMatches: 3, BonusMatches: 1, Tier: "Tier 5"},
{Game: "Thunderball", MainMatches: 4, BonusMatches: 0, Tier: "Tier 6"},
{Game: "Thunderball", MainMatches: 4, BonusMatches: 1, Tier: "Tier 7"},
{Game: "Thunderball", MainMatches: 5, BonusMatches: 0, Tier: "Second"},
{Game: "Thunderball", MainMatches: 5, BonusMatches: 1, Tier: "Jackpot"},
}
func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
switch {
case main == 5 && bonus == 1:
return 9, true
case main == 5 && bonus == 0:
return 8, true
case main == 4 && bonus == 1:
return 7, true
case main == 4 && bonus == 0:
return 6, true
case main == 3 && bonus == 1:
return 5, true
case main == 3 && bonus == 0:
return 4, true
case main == 2 && bonus == 1:
return 3, true
case main == 1 && bonus == 1:
return 2, true
case main == 0 && bonus == 1:
return 1, true
default:
return 0, false
}
} }

View File

@@ -2,6 +2,7 @@ package services
import ( import (
"database/sql" "database/sql"
"fmt"
"log" "log"
"synlotto-website/handlers" "synlotto-website/handlers"
"synlotto-website/helpers" "synlotto-website/helpers"
@@ -91,3 +92,74 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
return stats, nil return stats, nil
} }
func UpdateMissingPrizes(db *sql.DB) error {
type TicketInfo struct {
ID int
GameType string
DrawDate string
Main int
Bonus int
}
var tickets []TicketInfo
// Step 1: Load all relevant tickets
rows, err := db.Query(`
SELECT id, game_type, draw_date, matched_main, matched_bonus
FROM my_tickets
WHERE is_winner = 1 AND (prize_label IS NULL OR prize_label = '')
`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var t TicketInfo
if err := rows.Scan(&t.ID, &t.GameType, &t.DrawDate, &t.Main, &t.Bonus); err != nil {
log.Println("⚠️ Failed to scan row:", err)
continue
}
tickets = append(tickets, t)
}
// Step 2: Now that the reader is closed, perform updates
for _, t := range tickets {
if t.GameType != "Thunderball" {
continue
}
idx, ok := rules.GetThunderballPrizeIndex(t.Main, t.Bonus)
if !ok {
log.Printf("❌ No index for %d main, %d bonus", t.Main, t.Bonus)
continue
}
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
var amount int
err := db.QueryRow(query, t.DrawDate).Scan(&amount)
if err != nil {
log.Printf("❌ Prize lookup failed for ticket %d: %v", t.ID, err)
continue
}
label := "Free Ticket"
if amount > 0 {
label = fmt.Sprintf("£%.2f", float64(amount))
}
_, err = db.Exec(`
UPDATE my_tickets SET prize_amount = ?, prize_label = ? WHERE id = ?
`, float64(amount), label, t.ID)
if err != nil {
log.Printf("❌ Failed to update ticket %d: %v", t.ID, err)
} else {
log.Printf("✅ Updated ticket %d → %s", t.ID, label)
}
}
return nil
}

View File

@@ -15,6 +15,7 @@
<th>Purchased</th> <th>Purchased</th>
<th>Via</th> <th>Via</th>
<th>Image</th> <th>Image</th>
<th>Prize</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -70,6 +71,21 @@
<a href="/{{ .ImagePath }}" target="_blank">View</a> <a href="/{{ .ImagePath }}" target="_blank">View</a>
{{ else }}{{ end }} {{ else }}{{ end }}
</td> </td>
<td>
{{ if $ticket.IsWinner }}
{{ if $ticket.PrizeLabel }}
{{ if eq $ticket.PrizeLabel "Free Ticket" }}
🎟️ {{ $ticket.PrizeLabel }}
{{ else }}
💷 {{ $ticket.PrizeLabel }}
{{ end }}
{{ else }}
<span class="text-gray-400 italic">pending</span>
{{ end }}
{{ else }}
{{ end }}
</td>
</tr> </tr>
{{ end }} {{ end }}
</tbody> </tbody>

View File

@@ -1,16 +1,25 @@
{{ define "content" }} {{ define "content" }}
<h2>Manual Admin Triggers</h2> <h2>Manual Admin Triggers</h2>
<p>This page lets you manually run backend processes like result matching.</p>
<form method="POST" action="/admin/triggers"> <form method="POST" action="/admin/triggers">
{{ .CSRFField }} {{ .CSRFField }}
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"> <input type="hidden" name="action" value="match">
Run Ticket Matching <button>Run Ticket Matching</button>
</button> </form>
<form method="POST" action="/admin/triggers" class="mt-4">
{{ .CSRFField }}
<input type="hidden" name="action" value="prizes">
<button>Update Missing Prizes</button>
</form>
<form method="POST" action="/admin/triggers" class="mt-4">
{{ .CSRFField }}
<input type="hidden" name="action" value="run_all">
<button>Run All</button>
</form> </form>
{{ if .Flash }} {{ if .Flash }}
<p class="mt-4 text-green-600 font-medium">{{ .Flash }}</p> <p class="text-green-600 mt-4">{{ .Flash }}</p>
{{ end }} {{ end }}
{{ end }} {{ end }}