fix: resolve specificStadium achievement ID mismatch
The Green Monster (Fenway) and Ivy League (Wrigley) achievements weren't working because: 1. Symbolic IDs use lowercase sport (stadium_mlb_bos) 2. Sport enum uses uppercase raw values (MLB) 3. Visits store stadium UUIDs, not symbolic IDs Added resolveSymbolicStadiumId() helper that: - Uppercases the sport string before Sport(rawValue:) - Looks up team by abbreviation and sport - Returns the team's stadiumId as UUID string Also fixed: - getStadiumIdsForLeague returns UUID strings (not symbolic IDs) - AchievementProgress.isEarned computed from progress OR stored record - getStadiumIdsForDivision queries CanonicalTeam properly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,10 @@ struct GameMatchCandidate: Identifiable, Sendable {
|
||||
let awayTeam: Team
|
||||
let confidence: PhotoMatchConfidence
|
||||
|
||||
// Scraped score components (stored separately for flexible formatting)
|
||||
let scrapedHomeScore: Int?
|
||||
let scrapedAwayScore: Int?
|
||||
|
||||
init(game: Game, stadium: Stadium, homeTeam: Team, awayTeam: Team, confidence: PhotoMatchConfidence) {
|
||||
self.id = game.id
|
||||
self.game = game
|
||||
@@ -57,6 +61,57 @@ struct GameMatchCandidate: Identifiable, Sendable {
|
||||
self.homeTeam = homeTeam
|
||||
self.awayTeam = awayTeam
|
||||
self.confidence = confidence
|
||||
self.scrapedHomeScore = nil
|
||||
self.scrapedAwayScore = nil
|
||||
}
|
||||
|
||||
/// Initialize from a scraped historical game
|
||||
init(scrapedGame: ScrapedGame, stadium: Stadium) {
|
||||
self.id = UUID()
|
||||
self.stadium = stadium
|
||||
|
||||
// Create synthetic Team objects from scraped names
|
||||
// Scraped names already include city (e.g., "Chicago Cubs"), so we use empty city
|
||||
// to avoid duplication in fullName computed property
|
||||
self.homeTeam = Team(
|
||||
id: UUID(),
|
||||
name: scrapedGame.homeTeam,
|
||||
abbreviation: String(scrapedGame.homeTeam.suffix(3)).uppercased(),
|
||||
sport: scrapedGame.sport,
|
||||
city: "",
|
||||
stadiumId: stadium.id
|
||||
)
|
||||
|
||||
self.awayTeam = Team(
|
||||
id: UUID(),
|
||||
name: scrapedGame.awayTeam,
|
||||
abbreviation: String(scrapedGame.awayTeam.suffix(3)).uppercased(),
|
||||
sport: scrapedGame.sport,
|
||||
city: "",
|
||||
stadiumId: stadium.id
|
||||
)
|
||||
|
||||
// Create synthetic Game object
|
||||
let year = Calendar.current.component(.year, from: scrapedGame.date)
|
||||
self.game = Game(
|
||||
id: self.id,
|
||||
homeTeamId: self.homeTeam.id,
|
||||
awayTeamId: self.awayTeam.id,
|
||||
stadiumId: stadium.id,
|
||||
dateTime: scrapedGame.date,
|
||||
sport: scrapedGame.sport,
|
||||
season: "\(year)"
|
||||
)
|
||||
|
||||
// High confidence since we found the exact game from web
|
||||
self.confidence = PhotoMatchConfidence(
|
||||
spatial: .high,
|
||||
temporal: .exactDay
|
||||
)
|
||||
|
||||
// Store scraped scores separately for flexible formatting
|
||||
self.scrapedHomeScore = scrapedGame.homeScore
|
||||
self.scrapedAwayScore = scrapedGame.awayScore
|
||||
}
|
||||
|
||||
var matchupDescription: String {
|
||||
@@ -67,6 +122,18 @@ struct GameMatchCandidate: Identifiable, Sendable {
|
||||
"\(awayTeam.fullName) at \(homeTeam.fullName)"
|
||||
}
|
||||
|
||||
/// Final score in "away-home" format for storage (e.g., "5-3")
|
||||
var formattedFinalScore: String? {
|
||||
guard let home = scrapedHomeScore, let away = scrapedAwayScore else { return nil }
|
||||
return "\(away)-\(home)"
|
||||
}
|
||||
|
||||
/// Score for display with team names (e.g., "Pirates 5\nCubs 3")
|
||||
var displayScore: String? {
|
||||
guard let home = scrapedHomeScore, let away = scrapedAwayScore else { return nil }
|
||||
return "\(awayTeam.fullName) \(away)\n\(homeTeam.fullName) \(home)"
|
||||
}
|
||||
|
||||
var gameDateTime: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
@@ -220,6 +287,14 @@ final class GameMatcher {
|
||||
metadata: PhotoMetadata,
|
||||
sport: Sport? = nil
|
||||
) async -> PhotoImportCandidate {
|
||||
print("🎯 [GameMatcher] Processing photo for import")
|
||||
print("🎯 [GameMatcher] - hasValidDate: \(metadata.hasValidDate)")
|
||||
print("🎯 [GameMatcher] - captureDate: \(metadata.captureDate?.description ?? "nil")")
|
||||
print("🎯 [GameMatcher] - hasValidLocation: \(metadata.hasValidLocation)")
|
||||
if let coords = metadata.coordinates {
|
||||
print("🎯 [GameMatcher] - coordinates: \(coords.latitude), \(coords.longitude)")
|
||||
}
|
||||
|
||||
// Get stadium matches regardless of game matching
|
||||
var stadiumMatches: [StadiumMatch] = []
|
||||
if let coordinates = metadata.coordinates {
|
||||
@@ -227,9 +302,53 @@ final class GameMatcher {
|
||||
coordinates: coordinates,
|
||||
sport: sport
|
||||
)
|
||||
print("🎯 [GameMatcher] - nearby stadiums found: \(stadiumMatches.count)")
|
||||
for match in stadiumMatches.prefix(3) {
|
||||
print("🎯 [GameMatcher] • \(match.stadium.name) (\(String(format: "%.1f", match.distance / 1609.34)) mi)")
|
||||
}
|
||||
} else {
|
||||
print("🎯 [GameMatcher] - no coordinates, skipping stadium proximity search")
|
||||
}
|
||||
|
||||
let matchResult = await matchGame(metadata: metadata, sport: sport)
|
||||
var matchResult = await matchGame(metadata: metadata, sport: sport)
|
||||
|
||||
switch matchResult {
|
||||
case .singleMatch(let match):
|
||||
print("🎯 [GameMatcher] - result: singleMatch - \(match.homeTeam.fullName) vs \(match.awayTeam.fullName)")
|
||||
case .multipleMatches(let matches):
|
||||
print("🎯 [GameMatcher] - result: multipleMatches (\(matches.count) games)")
|
||||
case .noMatches(let reason):
|
||||
print("🎯 [GameMatcher] - result: noMatches - \(reason)")
|
||||
|
||||
// FALLBACK: Try scraping historical game data if we have stadium + date
|
||||
// Try all nearby stadiums (important for multi-sport venues like American Airlines Center)
|
||||
if let captureDate = metadata.captureDate, !stadiumMatches.isEmpty {
|
||||
print("🎯 [GameMatcher] - Trying historical scraper fallback...")
|
||||
|
||||
for stadiumMatch in stadiumMatches {
|
||||
let stadium = stadiumMatch.stadium
|
||||
print("🎯 [GameMatcher] - Trying \(stadium.name) (\(stadium.sport.rawValue))...")
|
||||
|
||||
if let scrapedGame = await HistoricalGameScraper.shared.scrapeGame(
|
||||
stadium: stadium,
|
||||
date: captureDate
|
||||
) {
|
||||
print("🎯 [GameMatcher] - ✅ Scraper found: \(scrapedGame.awayTeam) @ \(scrapedGame.homeTeam)")
|
||||
|
||||
// Convert ScrapedGame to a match result
|
||||
matchResult = .singleMatch(GameMatchCandidate(
|
||||
scrapedGame: scrapedGame,
|
||||
stadium: stadium
|
||||
))
|
||||
break // Found a game, stop searching
|
||||
}
|
||||
}
|
||||
|
||||
if case .noMatches = matchResult {
|
||||
print("🎯 [GameMatcher] - Scraper found no game at any stadium")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PhotoImportCandidate(
|
||||
metadata: metadata,
|
||||
|
||||
Reference in New Issue
Block a user