refactor: change domain model IDs from UUID to String canonical IDs
This refactor fixes the achievement system by using stable canonical string IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures stadium mappings for achievements are consistent across app launches and CloudKit sync operations. Changes: - Stadium, Team, Game: id property changed from UUID to String - Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums - CKModels: removed UUID parsing, use canonical IDs directly - AchievementEngine: now matches against canonical stadium IDs - All test files updated to use String IDs instead of UUID() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ final class AchievementEngine {
|
||||
func recalculateAllAchievements() async throws -> AchievementDelta {
|
||||
// Get all visits
|
||||
let visits = try fetchAllVisits()
|
||||
let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId })
|
||||
let visitedStadiumIds = Set(visits.map { $0.stadiumId })
|
||||
|
||||
// Get currently earned achievements
|
||||
let currentAchievements = try fetchEarnedAchievements()
|
||||
@@ -112,7 +112,7 @@ final class AchievementEngine {
|
||||
/// Quick check after new visit (incremental)
|
||||
func checkAchievementsForNewVisit(_ visit: StadiumVisit) async throws -> [AchievementDefinition] {
|
||||
let visits = try fetchAllVisits()
|
||||
let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId })
|
||||
let visitedStadiumIds = Set(visits.map { $0.stadiumId })
|
||||
|
||||
let currentAchievements = try fetchEarnedAchievements()
|
||||
let currentAchievementIds = Set(currentAchievements.map { $0.achievementTypeId })
|
||||
@@ -152,7 +152,7 @@ final class AchievementEngine {
|
||||
/// Get progress toward all achievements
|
||||
func getProgress() async throws -> [AchievementProgress] {
|
||||
let visits = try fetchAllVisits()
|
||||
let visitedStadiumIds = Set(visits.map { $0.canonicalStadiumId })
|
||||
let visitedStadiumIds = Set(visits.map { $0.stadiumId })
|
||||
let earnedAchievements = try fetchEarnedAchievements()
|
||||
let earnedIds = Set(earnedAchievements.map { $0.achievementTypeId })
|
||||
|
||||
@@ -196,7 +196,7 @@ final class AchievementEngine {
|
||||
|
||||
case .visitCountForSport(let count, let sport):
|
||||
let sportVisits = visits.filter { $0.sport == sport.rawValue }
|
||||
let sportStadiums = Set(sportVisits.map { $0.canonicalStadiumId })
|
||||
let sportStadiums = Set(sportVisits.map { $0.stadiumId })
|
||||
return sportStadiums.count >= count
|
||||
|
||||
case .completeDivision(let divisionId):
|
||||
@@ -214,41 +214,12 @@ final class AchievementEngine {
|
||||
case .multipleLeagues(let leagueCount):
|
||||
return checkMultipleLeagues(visits: visits, requiredLeagues: leagueCount)
|
||||
|
||||
case .specificStadium(let symbolicId):
|
||||
// Resolve symbolic ID (e.g., "stadium_mlb_bos") to actual UUID string
|
||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return false }
|
||||
return visitedStadiumIds.contains(resolvedId)
|
||||
case .specificStadium(let stadiumId):
|
||||
// Direct comparison - canonical IDs match everywhere
|
||||
return visitedStadiumIds.contains(stadiumId)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves symbolic stadium IDs (e.g., "stadium_mlb_bos") to actual stadium UUID strings
|
||||
private func resolveSymbolicStadiumId(_ symbolicId: String) -> String? {
|
||||
// Parse symbolic ID format: "stadium_{sport}_{teamAbbrev}"
|
||||
let parts = symbolicId.split(separator: "_")
|
||||
guard parts.count == 3,
|
||||
parts[0] == "stadium" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sport raw values are uppercase (e.g., "MLB"), but symbolic IDs use lowercase
|
||||
let sportString = String(parts[1]).uppercased()
|
||||
guard let sport = Sport(rawValue: sportString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let teamAbbrev = String(parts[2]).uppercased()
|
||||
|
||||
// Find team by abbreviation and sport
|
||||
guard let team = dataProvider.teams.first(where: {
|
||||
$0.abbreviation.uppercased() == teamAbbrev && $0.sport == sport
|
||||
}) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the stadium UUID as string (matches visit's canonicalStadiumId format)
|
||||
return team.stadiumId.uuidString
|
||||
}
|
||||
|
||||
private func checkDivisionComplete(_ divisionId: String, visitedStadiumIds: Set<String>) -> Bool {
|
||||
guard let division = LeagueStructure.division(byId: divisionId) else { return false }
|
||||
|
||||
@@ -291,7 +262,7 @@ final class AchievementEngine {
|
||||
if daysDiff < withinDays {
|
||||
// Check unique stadiums in window
|
||||
let windowVisits = Array(sortedVisits[i..<(i + requiredVisits)])
|
||||
let uniqueStadiums = Set(windowVisits.map { $0.canonicalStadiumId })
|
||||
let uniqueStadiums = Set(windowVisits.map { $0.stadiumId })
|
||||
if uniqueStadiums.count >= requiredVisits {
|
||||
return true
|
||||
}
|
||||
@@ -322,7 +293,7 @@ final class AchievementEngine {
|
||||
|
||||
case .visitCountForSport(let count, let sport):
|
||||
let sportVisits = visits.filter { $0.sport == sport.rawValue }
|
||||
let sportStadiums = Set(sportVisits.map { $0.canonicalStadiumId })
|
||||
let sportStadiums = Set(sportVisits.map { $0.stadiumId })
|
||||
return (sportStadiums.count, count)
|
||||
|
||||
case .completeDivision(let divisionId):
|
||||
@@ -348,10 +319,9 @@ final class AchievementEngine {
|
||||
let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) })
|
||||
return (leagues.count, leagueCount)
|
||||
|
||||
case .specificStadium(let symbolicId):
|
||||
// Resolve symbolic ID to actual UUID string
|
||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return (0, 1) }
|
||||
return (visitedStadiumIds.contains(resolvedId) ? 1 : 0, 1)
|
||||
case .specificStadium(let stadiumId):
|
||||
// Direct comparison - canonical IDs match everywhere
|
||||
return (visitedStadiumIds.contains(stadiumId) ? 1 : 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,15 +338,15 @@ final class AchievementEngine {
|
||||
|
||||
case .completeDivision(let divisionId):
|
||||
let stadiumIds = Set(getStadiumIdsForDivision(divisionId))
|
||||
return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id }
|
||||
return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }
|
||||
|
||||
case .completeConference(let conferenceId):
|
||||
let stadiumIds = Set(getStadiumIdsForConference(conferenceId))
|
||||
return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id }
|
||||
return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }
|
||||
|
||||
case .completeLeague(let sport):
|
||||
let stadiumIds = Set(getStadiumIdsForLeague(sport))
|
||||
return visits.filter { stadiumIds.contains($0.canonicalStadiumId) }.map { $0.id }
|
||||
return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }
|
||||
|
||||
case .visitsInDays(let requiredVisits, let days):
|
||||
// Find the qualifying window of visits
|
||||
@@ -391,10 +361,9 @@ final class AchievementEngine {
|
||||
}
|
||||
return []
|
||||
|
||||
case .specificStadium(let symbolicId):
|
||||
// Resolve symbolic ID to actual UUID string
|
||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return [] }
|
||||
return visits.filter { $0.canonicalStadiumId == resolvedId }.map { $0.id }
|
||||
case .specificStadium(let stadiumId):
|
||||
// Direct comparison - canonical IDs match everywhere
|
||||
return visits.filter { $0.stadiumId == stadiumId }.map { $0.id }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,17 +379,8 @@ final class AchievementEngine {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get stadium UUIDs for these teams
|
||||
// CanonicalTeam has stadiumCanonicalId, we need to find the corresponding Stadium UUID
|
||||
var stadiumIds: [String] = []
|
||||
for canonicalTeam in canonicalTeams {
|
||||
// Find the domain team by matching name/abbreviation to get stadium UUID
|
||||
if let team = dataProvider.teams.first(where: { $0.abbreviation == canonicalTeam.abbreviation && $0.sport.rawValue == canonicalTeam.sport }) {
|
||||
stadiumIds.append(team.stadiumId.uuidString)
|
||||
}
|
||||
}
|
||||
|
||||
return stadiumIds
|
||||
// Get canonical stadium IDs for these teams
|
||||
return canonicalTeams.map { $0.stadiumCanonicalId }
|
||||
}
|
||||
|
||||
private func getStadiumIdsForConference(_ conferenceId: String) -> [String] {
|
||||
@@ -434,7 +394,7 @@ final class AchievementEngine {
|
||||
}
|
||||
|
||||
private func getStadiumIdsForLeague(_ sport: Sport) -> [String] {
|
||||
// Get all stadiums for this sport - return UUID strings to match visit format
|
||||
// Get all stadium canonical IDs for this sport
|
||||
return dataProvider.stadiums
|
||||
.filter { stadium in
|
||||
// Check if stadium hosts teams of this sport
|
||||
@@ -442,7 +402,7 @@ final class AchievementEngine {
|
||||
team.stadiumId == stadium.id && team.sport == sport
|
||||
}
|
||||
}
|
||||
.map { $0.id.uuidString }
|
||||
.map { $0.id }
|
||||
}
|
||||
|
||||
// MARK: - Data Fetching
|
||||
|
||||
Reference in New Issue
Block a user