fix(itinerary): add city to game items for proper constraint validation

Travel constraint validation was not working because ItineraryConstraints
had no game items to validate against - games came from RichGame objects
but were never converted to ItineraryItem for constraint checking.

Changes:
- Add city parameter to ItemKind.game enum case
- Create game ItineraryItems from RichGame data in buildItineraryData()
- Update isValidTravelPosition to compare against actual game sortOrders
- Fix tests to use appropriate game sortOrder conventions

Now travel is properly constrained to appear before arrival city games
and after departure city games.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-18 22:46:40 -06:00
parent 72447c61fe
commit e72da7c5a7
11 changed files with 247 additions and 254 deletions

View File

@@ -243,24 +243,27 @@ final class ItineraryReorderingLogicTests: XCTestCase {
// MARK: - travelRow Tests
func test_travelRow_findsCorrectRow() {
// Semantic model: travelRow finds travel in the section AFTER the day header
// Travel must be positioned within its correct day section
let items = buildFlatItems([
.day(1),
.game("Detroit", day: 1),
.travel(from: "Detroit", to: "Chicago", day: 2),
.day(2),
.travel(from: "Chicago", to: "Milwaukee", day: 3),
.day(3)
.travel(from: "Detroit", to: "Chicago", day: 2), // Row 3: in day 2 section
.day(3),
.travel(from: "Chicago", to: "Milwaukee", day: 3) // Row 5: in day 3 section
])
XCTAssertEqual(Logic.travelRow(in: items, forDay: 2), 2)
XCTAssertEqual(Logic.travelRow(in: items, forDay: 3), 4)
XCTAssertEqual(Logic.travelRow(in: items, forDay: 2), 3)
XCTAssertEqual(Logic.travelRow(in: items, forDay: 3), 5)
}
func test_travelRow_noTravelOnDay_returnsNil() {
// Travel is in day 2 section, so day 1 has no travel
let items = buildFlatItems([
.day(1),
.travel(from: "Detroit", to: "Chicago", day: 2),
.day(2)
.day(2),
.travel(from: "Detroit", to: "Chicago", day: 2) // In day 2 section
])
XCTAssertNil(Logic.travelRow(in: items, forDay: 1))

View File

@@ -108,7 +108,7 @@ enum ItineraryTestHelpers {
tripId: testTripId,
day: day,
sortOrder: sortOrder,
kind: .game(gameId: "game-\(city)-\(UUID().uuidString.prefix(4))")
kind: .game(gameId: "game-\(city)-\(UUID().uuidString.prefix(4))", city: city)
)
}

View File

@@ -194,11 +194,15 @@ final class ItineraryTravelConstraintTests: XCTestCase {
// MARK: - Travel Movement Tests
func test_travel_moveToValidDay_callsCallback() {
// Given: Travel with valid range 2-4
// Given: Travel with valid range 2-3
let travel = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let travelItem = ItineraryRowItem.travel(travel, dayNumber: 2)
// Travel model item for sortOrder lookup
let travelModelItem = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 1.0)
let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil)
let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [], travelBefore: travel)
let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [travelItem], travelBefore: nil)
let day3 = ItineraryDayData(id: 3, dayNumber: 3, date: H.dayAfter(H.dayAfter(testDate)), games: [], items: [], travelBefore: nil)
var capturedTravelId: String = ""
@@ -211,12 +215,12 @@ final class ItineraryTravelConstraintTests: XCTestCase {
controller.reloadData(
days: [day1, day2, day3],
travelValidRanges: ["travel:chicago->detroit": 2...3],
itineraryItems: []
itineraryItems: [travelModelItem]
)
// Rows: 0=Day1 header, 1=travel, 2=Day2 header, 3=Day3 header
// Move travel (row 1) to row 3 (after Day2, before Day3 header means Day 3)
controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 1, section: 0), to: IndexPath(row: 3, section: 0))
// Rows: 0=Day1 header, 1=Day2 header, 2=travel, 3=Day3 header
// Move travel (row 2) to row 3 (after Day3 header = Day 3)
controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 2, section: 0), to: IndexPath(row: 3, section: 0))
XCTAssertEqual(capturedTravelId, "travel:chicago->detroit")
XCTAssertEqual(capturedDay, 3, "Travel should now be on Day 3")
@@ -228,18 +232,21 @@ final class ItineraryTravelConstraintTests: XCTestCase {
// Given: Travel with valid range Days 2-3
let travel = H.makeTravelSegment(from: "Chicago", to: "Detroit")
let travelId = "travel:chicago->detroit"
let travelItem = ItineraryRowItem.travel(travel, dayNumber: 2)
let travelModelItem = H.makeTravelItem(from: "Chicago", to: "Detroit", day: 2, sortOrder: 1.0)
let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil)
let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [], travelBefore: travel)
let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [travelItem], travelBefore: nil)
let day3 = ItineraryDayData(id: 3, dayNumber: 3, date: H.dayAfter(H.dayAfter(testDate)), games: [], items: [], travelBefore: nil)
let controller = ItineraryTableViewController(style: .plain)
let validRanges = [travelId: 2...3]
controller.reloadData(days: [day1, day2, day3], travelValidRanges: validRanges)
controller.reloadData(days: [day1, day2, day3], travelValidRanges: validRanges, itineraryItems: [travelModelItem])
// Travel is at row 1 (after Day1 header at row 0)
// Try to move it to Day 1 area (row 0 or 1) - should snap back to valid range
let source = IndexPath(row: 1, section: 0)
// Rows: 0=Day1 header, 1=Day2 header, 2=travel, 3=Day3 header
// Travel is at row 2 (after Day2 header at row 1)
// Try to move it to Day 1 area (row 0) - should snap back to valid range
let source = IndexPath(row: 2, section: 0)
let proposed = IndexPath(row: 0, section: 0)
let result = controller.tableView(controller.tableView, targetIndexPathForMoveFromRowAt: source, toProposedIndexPath: proposed)