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:
Trey t
2026-01-12 09:24:33 -06:00
parent 4b2cacaeba
commit 1703ca5b0f
53 changed files with 642 additions and 727 deletions

View File

@@ -21,14 +21,9 @@ final class AppDataProvider: ObservableObject {
@Published private(set) var error: Error?
@Published private(set) var errorMessage: String?
private var teamsById: [UUID: Team] = [:]
private var stadiumsById: [UUID: Stadium] = [:]
private var stadiumsByCanonicalId: [String: Stadium] = [:]
private var teamsByCanonicalId: [String: Team] = [:]
// Canonical ID lookups for game conversion
private var canonicalTeamUUIDs: [String: UUID] = [:]
private var canonicalStadiumUUIDs: [String: UUID] = [:]
// Lookup dictionaries - keyed by canonical ID (String)
private var teamsById: [String: Team] = [:]
private var stadiumsById: [String: Stadium] = [:]
private var modelContext: ModelContext?
@@ -63,11 +58,11 @@ final class AppDataProvider: ObservableObject {
// Convert to domain models and build lookups
var loadedStadiums: [Stadium] = []
var stadiumLookup: [String: Stadium] = [:]
for canonical in canonicalStadiums {
let stadium = canonical.toDomain()
loadedStadiums.append(stadium)
stadiumsByCanonicalId[canonical.canonicalId] = stadium
canonicalStadiumUUIDs[canonical.canonicalId] = stadium.id
stadiumLookup[stadium.id] = stadium
}
// Fetch canonical teams from SwiftData
@@ -78,31 +73,17 @@ final class AppDataProvider: ObservableObject {
// Convert to domain models
var loadedTeams: [Team] = []
var teamLookup: [String: Team] = [:]
for canonical in canonicalTeams {
// Get stadium UUID for this team
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
let team = canonical.toDomain(stadiumUUID: stadiumUUID)
let team = canonical.toDomain()
loadedTeams.append(team)
teamsByCanonicalId[canonical.canonicalId] = team
canonicalTeamUUIDs[canonical.canonicalId] = team.id
teamLookup[team.id] = team
}
self.teams = loadedTeams
self.stadiums = loadedStadiums
// Build lookup dictionaries (use reduce to handle potential duplicates gracefully)
self.teamsById = loadedTeams.reduce(into: [:]) { dict, team in
if dict[team.id] != nil {
print("⚠️ Duplicate team UUID: \(team.id) - \(team.name)")
}
dict[team.id] = team
}
self.stadiumsById = loadedStadiums.reduce(into: [:]) { dict, stadium in
if dict[stadium.id] != nil {
print("⚠️ Duplicate stadium UUID: \(stadium.id) - \(stadium.name)")
}
dict[stadium.id] = stadium
}
self.teamsById = teamLookup
self.stadiumsById = stadiumLookup
} catch {
self.error = error
@@ -123,11 +104,11 @@ final class AppDataProvider: ObservableObject {
// MARK: - Data Access
func team(for id: UUID) -> Team? {
func team(for id: String) -> Team? {
teamsById[id]
}
func stadium(for id: UUID) -> Stadium? {
func stadium(for id: String) -> Stadium? {
stadiumsById[id]
}
@@ -156,47 +137,27 @@ final class AppDataProvider: ObservableObject {
let canonicalGames = try context.fetch(descriptor)
// Filter by sport and convert to domain models
let result = canonicalGames.compactMap { canonical -> Game? in
return canonicalGames.compactMap { canonical -> Game? in
guard sportStrings.contains(canonical.sport) else { return nil }
let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID()
let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID()
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
return canonical.toDomain(
homeTeamUUID: homeTeamUUID,
awayTeamUUID: awayTeamUUID,
stadiumUUID: stadiumUUID
)
return canonical.toDomain()
}
return result
}
/// Fetch a single game by ID
func fetchGame(by id: UUID) async throws -> Game? {
/// Fetch a single game by canonical ID
func fetchGame(by id: String) async throws -> Game? {
guard let context = modelContext else {
throw DataProviderError.contextNotConfigured
}
let idString = id.uuidString
let descriptor = FetchDescriptor<CanonicalGame>(
predicate: #Predicate<CanonicalGame> { $0.canonicalId == idString }
predicate: #Predicate<CanonicalGame> { $0.canonicalId == id }
)
guard let canonical = try context.fetch(descriptor).first else {
return nil
}
let homeTeamUUID = canonicalTeamUUIDs[canonical.homeTeamCanonicalId] ?? UUID()
let awayTeamUUID = canonicalTeamUUIDs[canonical.awayTeamCanonicalId] ?? UUID()
let stadiumUUID = canonicalStadiumUUIDs[canonical.stadiumCanonicalId] ?? UUID()
return canonical.toDomain(
homeTeamUUID: homeTeamUUID,
awayTeamUUID: awayTeamUUID,
stadiumUUID: stadiumUUID
)
return canonical.toDomain()
}
/// Fetch games with full team and stadium data