package services import ( "database/sql" "fmt" "log" thunderballrules "synlotto-website/internal/rules/thunderball" drawsSvc "synlotto-website/internal/services/draws" "synlotto-website/internal/helpers" "synlotto-website/internal/models" ) // RunTicketMatching finds unmatched tickets, matches them to draw results, // updates match/prize fields, and writes a summary log entry. 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, ball6, bonus1, bonus2 FROM my_tickets WHERE matched_main IS NULL `) if err != nil { return stats, err } defer rows.Close() var pending []models.Ticket for rows.Next() { var t models.Ticket var drawDateStr string if dt, err := helpers.ParseDrawDate(drawDateStr); err == nil { t.DrawDate = dt } var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64 if err := rows.Scan( &t.Id, &t.GameType, &t.DrawDate, &b1, &b2, &b3, &b4, &b5, &b6, &bo1, &bo2, ); err != nil { continue } 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) pending = append(pending, t) } for _, t := range pending { matchTicket := models.MatchTicket{ Balls: helpers.BuildBallsSlice(t), BonusBalls: helpers.BuildBonusSlice(t), } draw := drawsSvc.GetDrawResultForTicket(db, t.GameType, helpers.FormatDrawDate(t.DrawDate)) if draw.DrawID == 0 { // No draw yet → skip continue } mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls) bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls) prizeTier := GetPrizeTier(matchTicket.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules) isWinner := prizeTier != "" if _, err := db.Exec(` UPDATE my_tickets SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ? WHERE id = ? `, mainMatches, bonusMatches, prizeTier, isWinner, t.Id); err != nil { log.Println("⚠️ Failed to update ticket match:", err) continue } stats.TicketsMatched++ if isWinner { stats.WinnersFound++ } } _, _ = db.Exec(` INSERT INTO log_ticket_matching (triggered_by, tickets_matched, winners_found) VALUES (?, ?, ?) `, triggeredBy, stats.TicketsMatched, stats.WinnersFound) return stats, nil } // UpdateMissingPrizes fills in prize labels/amounts for already-matched winners that lack labels. func UpdateMissingPrizes(db *sql.DB) error { type TicketInfo struct { ID int GameType string DrawDate string Main int Bonus int } var tickets []TicketInfo 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) } for _, t := range tickets { if t.GameType != "Thunderball" { continue } idx, ok := thunderballrules.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 if err := db.QueryRow(query, t.DrawDate).Scan(&amount); 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)) } if _, err := db.Exec(` UPDATE my_tickets SET prize_amount = ?, prize_label = ? WHERE id = ? `, float64(amount), label, t.ID); 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 } // RefreshTicketPrizes recomputes and writes prize info for all tickets. func RefreshTicketPrizes(db *sql.DB) error { type TicketRow struct { ID int GameType string DrawDate string B1, B2, B3, B4, B5, B6 sql.NullInt64 Bonus1, Bonus2 sql.NullInt64 } var tickets []TicketRow rows, err := db.Query(` SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6, bonus1, bonus2 FROM my_tickets `) if err != nil { return err } defer rows.Close() for rows.Next() { var t TicketRow if err := rows.Scan(&t.ID, &t.GameType, &t.DrawDate, &t.B1, &t.B2, &t.B3, &t.B4, &t.B5, &t.B6, &t.Bonus1, &t.Bonus2); err != nil { log.Println("⚠️ Failed to scan ticket:", err) continue } tickets = append(tickets, t) } rows.Close() for _, row := range tickets { matchTicket := models.MatchTicket{ Balls: helpers.BuildBallsFromNulls(row.B1, row.B2, row.B3, row.B4, row.B5, row.B6), BonusBalls: helpers.BuildBonusFromNulls(row.Bonus1, row.Bonus2), } draw := drawsSvc.GetDrawResultForTicket(db, row.GameType, row.DrawDate) if draw.DrawID == 0 { log.Printf("❌ No draw result for %s (%s)", row.DrawDate, row.GameType) continue } mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls) bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls) prizeTier := GetPrizeTier(row.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules) isWinner := prizeTier != "" var label string var amount float64 if row.GameType == "Thunderball" { if idx, ok := thunderballrules.GetThunderballPrizeIndex(mainMatches, bonusMatches); ok { query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx) var val int if err := db.QueryRow(query, row.DrawDate).Scan(&val); err == nil { amount = float64(val) if val > 0 { label = fmt.Sprintf("£%.2f", amount) } else { label = "Free Ticket" } } } } log.Printf("🧪 Ticket %d → Matches: %d+%d, Tier: %s, Winner: %v, Label: %s, Amount: %.2f", row.ID, mainMatches, bonusMatches, prizeTier, isWinner, label, amount) res, err := db.Exec(` UPDATE my_tickets SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ?, prize_amount = ?, prize_label = ? WHERE id = ? `, mainMatches, bonusMatches, prizeTier, isWinner, amount, label, row.ID) if err != nil { log.Printf("❌ Failed to update ticket %d: %v", row.ID, err) continue } if rowsAffected, _ := res.RowsAffected(); rowsAffected > 0 { log.Printf("✅ Ticket %d updated — rows affected: %d | Tier: %s | Label: %s | Matches: %d+%d", row.ID, rowsAffected, prizeTier, label, mainMatches, bonusMatches) } } return nil }