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:
@@ -80,7 +80,7 @@ enum GameDAGRouter {
|
||||
|
||||
/// Composite key for exact deduplication
|
||||
var uniqueKey: String {
|
||||
route.map { $0.id.uuidString }.joined(separator: "-")
|
||||
route.map { $0.id }.joined(separator: "-")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +106,9 @@ enum GameDAGRouter {
|
||||
///
|
||||
static func findRoutes(
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
stadiums: [String: Stadium],
|
||||
constraints: DrivingConstraints,
|
||||
anchorGameIds: Set<UUID> = [],
|
||||
anchorGameIds: Set<String> = [],
|
||||
allowRepeatCities: Bool = true,
|
||||
beamWidth: Int = defaultBeamWidth
|
||||
) -> [[Game]] {
|
||||
@@ -219,10 +219,10 @@ enum GameDAGRouter {
|
||||
/// Compatibility wrapper that matches GeographicRouteExplorer's interface.
|
||||
static func findAllSensibleRoutes(
|
||||
from games: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
anchorGameIds: Set<UUID> = [],
|
||||
stadiums: [String: Stadium],
|
||||
anchorGameIds: Set<String> = [],
|
||||
allowRepeatCities: Bool = true,
|
||||
stopBuilder: ([Game], [UUID: Stadium]) -> [ItineraryStop]
|
||||
stopBuilder: ([Game], [String: Stadium]) -> [ItineraryStop]
|
||||
) -> [[Game]] {
|
||||
let constraints = DrivingConstraints.default
|
||||
return findRoutes(
|
||||
@@ -244,7 +244,7 @@ enum GameDAGRouter {
|
||||
/// - Short duration AND long duration
|
||||
private static func selectDiverseRoutes(
|
||||
_ routes: [[Game]],
|
||||
stadiums: [UUID: Stadium],
|
||||
stadiums: [String: Stadium],
|
||||
maxCount: Int
|
||||
) -> [[Game]] {
|
||||
guard !routes.isEmpty else { return [] }
|
||||
@@ -362,14 +362,14 @@ enum GameDAGRouter {
|
||||
/// Keeps routes that span the diversity space rather than just high-scoring ones.
|
||||
private static func diversityPrune(
|
||||
_ paths: [[Game]],
|
||||
stadiums: [UUID: Stadium],
|
||||
stadiums: [String: Stadium],
|
||||
targetCount: Int
|
||||
) -> [[Game]] {
|
||||
// Remove exact duplicates first
|
||||
var uniquePaths: [[Game]] = []
|
||||
var seen = Set<String>()
|
||||
for path in paths {
|
||||
let key = path.map { $0.id.uuidString }.joined(separator: "-")
|
||||
let key = path.map { $0.id }.joined(separator: "-")
|
||||
if !seen.contains(key) {
|
||||
seen.insert(key)
|
||||
uniquePaths.append(path)
|
||||
@@ -425,7 +425,7 @@ enum GameDAGRouter {
|
||||
}
|
||||
|
||||
/// Builds a profile for a route.
|
||||
private static func buildProfile(for route: [Game], stadiums: [UUID: Stadium]) -> RouteProfile {
|
||||
private static func buildProfile(for route: [Game], stadiums: [String: Stadium]) -> RouteProfile {
|
||||
let gameCount = route.count
|
||||
let cities = Set(route.compactMap { stadiums[$0.stadiumId]?.city })
|
||||
let cityCount = cities.count
|
||||
@@ -488,7 +488,7 @@ enum GameDAGRouter {
|
||||
private static func canTransition(
|
||||
from: Game,
|
||||
to: Game,
|
||||
stadiums: [UUID: Stadium],
|
||||
stadiums: [String: Stadium],
|
||||
constraints: DrivingConstraints
|
||||
) -> Bool {
|
||||
// Time must move forward
|
||||
@@ -562,7 +562,7 @@ enum GameDAGRouter {
|
||||
private static func estimateDistanceMiles(
|
||||
from: Game,
|
||||
to: Game,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> Double {
|
||||
if from.stadiumId == to.stadiumId { return 0 }
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
||||
///
|
||||
private func buildStops(
|
||||
from games: [Game],
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [ItineraryStop] {
|
||||
guard !games.isEmpty else { return [] }
|
||||
|
||||
@@ -308,7 +308,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
||||
// Group consecutive games at the same stadium into stops
|
||||
// If you visit A, then B, then A again, that's 3 stops (A, B, A)
|
||||
var stops: [ItineraryStop] = []
|
||||
var currentStadiumId: UUID? = nil
|
||||
var currentStadiumId: String? = nil
|
||||
var currentGames: [Game] = []
|
||||
|
||||
for game in sortedGames {
|
||||
@@ -340,8 +340,8 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||
private func createStop(
|
||||
from games: [Game],
|
||||
stadiumId: UUID,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiumId: String,
|
||||
stadiums: [String: Stadium]
|
||||
) -> ItineraryStop? {
|
||||
guard !games.isEmpty else { return nil }
|
||||
|
||||
@@ -380,7 +380,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
||||
var unique: [[Game]] = []
|
||||
|
||||
for route in routes {
|
||||
let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-")
|
||||
let key = route.map { $0.id }.sorted().joined(separator: "-")
|
||||
if !seen.contains(key) {
|
||||
seen.insert(key)
|
||||
unique.append(route)
|
||||
@@ -396,7 +396,7 @@ final class ScenarioAPlanner: ScenarioPlanner {
|
||||
/// This ensures we get diverse options from East, Central, and West coasts.
|
||||
private func findRoutesPerRegion(
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
stadiums: [String: Stadium],
|
||||
allowRepeatCities: Bool
|
||||
) -> [[Game]] {
|
||||
// Partition games by region
|
||||
|
||||
@@ -316,7 +316,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
||||
/// Creates separate stops when visiting the same city with other cities in between.
|
||||
private func buildStops(
|
||||
from games: [Game],
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [ItineraryStop] {
|
||||
guard !games.isEmpty else { return [] }
|
||||
|
||||
@@ -325,7 +325,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
||||
|
||||
// Group consecutive games at the same stadium
|
||||
var stops: [ItineraryStop] = []
|
||||
var currentStadiumId: UUID? = nil
|
||||
var currentStadiumId: String? = nil
|
||||
var currentGames: [Game] = []
|
||||
|
||||
for game in sortedGames {
|
||||
@@ -357,8 +357,8 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||
private func createStop(
|
||||
from games: [Game],
|
||||
stadiumId: UUID,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiumId: String,
|
||||
stadiums: [String: Stadium]
|
||||
) -> ItineraryStop? {
|
||||
guard !games.isEmpty else { return nil }
|
||||
|
||||
@@ -396,8 +396,8 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
||||
/// For Scenario B, routes must still contain all anchor games.
|
||||
private func findRoutesPerRegion(
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium],
|
||||
anchorGameIds: Set<UUID>,
|
||||
stadiums: [String: Stadium],
|
||||
anchorGameIds: Set<String>,
|
||||
allowRepeatCities: Bool
|
||||
) -> [[Game]] {
|
||||
// First, determine which region(s) the anchor games are in
|
||||
@@ -459,7 +459,7 @@ final class ScenarioBPlanner: ScenarioPlanner {
|
||||
var unique: [[Game]] = []
|
||||
|
||||
for route in routes {
|
||||
let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-")
|
||||
let key = route.map { $0.id }.sorted().joined(separator: "-")
|
||||
if !seen.contains(key) {
|
||||
seen.insert(key)
|
||||
unique.append(route)
|
||||
|
||||
@@ -272,7 +272,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
/// Finds all stadiums in a given city (case-insensitive match).
|
||||
private func findStadiumsInCity(
|
||||
cityName: String,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [Stadium] {
|
||||
let normalizedCity = cityName.lowercased().trimmingCharacters(in: .whitespaces)
|
||||
return stadiums.values.filter { stadium in
|
||||
@@ -296,14 +296,14 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
private func findDirectionalStadiums(
|
||||
from start: CLLocationCoordinate2D,
|
||||
to end: CLLocationCoordinate2D,
|
||||
stadiums: [UUID: Stadium]
|
||||
) -> Set<UUID> {
|
||||
stadiums: [String: Stadium]
|
||||
) -> Set<String> {
|
||||
let directDistance = distanceBetween(start, end)
|
||||
|
||||
// Allow detours up to 50% longer than direct distance
|
||||
let maxDetourDistance = directDistance * 1.5
|
||||
|
||||
var directionalIds: Set<UUID> = []
|
||||
var directionalIds: Set<String> = []
|
||||
|
||||
for (id, stadium) in stadiums {
|
||||
let stadiumCoord = stadium.coordinate
|
||||
@@ -349,8 +349,8 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
/// Create a date range from start_game.date to end_game.date
|
||||
///
|
||||
private func generateDateRanges(
|
||||
startStadiumIds: Set<UUID>,
|
||||
endStadiumIds: Set<UUID>,
|
||||
startStadiumIds: Set<String>,
|
||||
endStadiumIds: Set<String>,
|
||||
allGames: [Game],
|
||||
request: PlanningRequest
|
||||
) -> [DateInterval] {
|
||||
@@ -417,7 +417,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
/// Creates separate stops when visiting the same city with other cities in between.
|
||||
private func buildStops(
|
||||
from games: [Game],
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [ItineraryStop] {
|
||||
guard !games.isEmpty else { return [] }
|
||||
|
||||
@@ -426,7 +426,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
|
||||
// Group consecutive games at the same stadium
|
||||
var stops: [ItineraryStop] = []
|
||||
var currentStadiumId: UUID? = nil
|
||||
var currentStadiumId: String? = nil
|
||||
var currentGames: [Game] = []
|
||||
|
||||
for game in sortedGames {
|
||||
@@ -458,8 +458,8 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||
private func createStop(
|
||||
from games: [Game],
|
||||
stadiumId: UUID,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiumId: String,
|
||||
stadiums: [String: Stadium]
|
||||
) -> ItineraryStop? {
|
||||
guard !games.isEmpty else { return nil }
|
||||
|
||||
@@ -496,7 +496,7 @@ final class ScenarioCPlanner: ScenarioPlanner {
|
||||
start: LocationInput,
|
||||
end: LocationInput,
|
||||
games: [Game],
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [ItineraryStop] {
|
||||
|
||||
var stops: [ItineraryStop] = []
|
||||
|
||||
@@ -275,7 +275,7 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
// MARK: - Team Filtering
|
||||
|
||||
/// Filters games to those involving the followed team (home or away).
|
||||
private func filterToTeam(_ games: [Game], teamId: UUID) -> [Game] {
|
||||
private func filterToTeam(_ games: [Game], teamId: String) -> [Game] {
|
||||
games.filter { game in
|
||||
game.homeTeamId == teamId || game.awayTeamId == teamId
|
||||
}
|
||||
@@ -287,7 +287,7 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
private func applyRepeatCityFilter(
|
||||
_ games: [Game],
|
||||
allowRepeat: Bool,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [Game] {
|
||||
guard !allowRepeat else {
|
||||
print("🔍 applyRepeatCityFilter: allowRepeat=true, returning all \(games.count) games")
|
||||
@@ -317,14 +317,14 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
/// Same logic as ScenarioAPlanner.
|
||||
private func buildStops(
|
||||
from games: [Game],
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiums: [String: Stadium]
|
||||
) -> [ItineraryStop] {
|
||||
guard !games.isEmpty else { return [] }
|
||||
|
||||
let sortedGames = games.sorted { $0.startTime < $1.startTime }
|
||||
|
||||
var stops: [ItineraryStop] = []
|
||||
var currentStadiumId: UUID? = nil
|
||||
var currentStadiumId: String? = nil
|
||||
var currentGames: [Game] = []
|
||||
|
||||
for game in sortedGames {
|
||||
@@ -354,8 +354,8 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
/// Creates an ItineraryStop from a group of games at the same stadium.
|
||||
private func createStop(
|
||||
from games: [Game],
|
||||
stadiumId: UUID,
|
||||
stadiums: [UUID: Stadium]
|
||||
stadiumId: String,
|
||||
stadiums: [String: Stadium]
|
||||
) -> ItineraryStop? {
|
||||
guard !games.isEmpty else { return nil }
|
||||
|
||||
@@ -393,7 +393,7 @@ final class ScenarioDPlanner: ScenarioPlanner {
|
||||
var unique: [[Game]] = []
|
||||
|
||||
for route in routes {
|
||||
let key = route.map { $0.id.uuidString }.sorted().joined(separator: "-")
|
||||
let key = route.map { $0.id }.sorted().joined(separator: "-")
|
||||
if !seen.contains(key) {
|
||||
seen.insert(key)
|
||||
unique.append(route)
|
||||
|
||||
@@ -23,7 +23,7 @@ enum ScenarioPlannerFactory {
|
||||
/// Creates the appropriate planner based on the request inputs
|
||||
static func planner(for request: PlanningRequest) -> ScenarioPlanner {
|
||||
print("🔍 ScenarioPlannerFactory: Selecting planner...")
|
||||
print(" - followTeamId: \(request.preferences.followTeamId?.uuidString ?? "nil")")
|
||||
print(" - followTeamId: \(request.preferences.followTeamId ?? "nil")")
|
||||
print(" - selectedGames.count: \(request.selectedGames.count)")
|
||||
print(" - startLocation: \(request.startLocation?.name ?? "nil")")
|
||||
print(" - endLocation: \(request.endLocation?.name ?? "nil")")
|
||||
|
||||
@@ -251,7 +251,7 @@ struct ItineraryStop: Identifiable, Hashable {
|
||||
let city: String
|
||||
let state: String
|
||||
let coordinate: CLLocationCoordinate2D?
|
||||
let games: [UUID]
|
||||
let games: [String] // Canonical game IDs
|
||||
let arrivalDate: Date
|
||||
let departureDate: Date
|
||||
let location: LocationInput
|
||||
@@ -490,8 +490,8 @@ extension ItineraryOption {
|
||||
struct PlanningRequest {
|
||||
let preferences: TripPreferences
|
||||
let availableGames: [Game]
|
||||
let teams: [UUID: Team]
|
||||
let stadiums: [UUID: Stadium]
|
||||
let teams: [String: Team] // Keyed by canonical ID
|
||||
let stadiums: [String: Stadium] // Keyed by canonical ID
|
||||
|
||||
// MARK: - Computed Properties for Engine
|
||||
|
||||
|
||||
Reference in New Issue
Block a user