fix: 12 planning engine bugs + App Store preview export at 886x1920

Planning engine fixes (from adversarial code review):
- Bug #1: sortByLeisure tie-breaking uses totalDrivingHours
- Bug #2: allDates/calculateRestDays guard-let-break prevents infinite loop
- Bug #3: same-day trip no longer rejected (>= in dateRange guard)
- Bug #4: ScenarioD rationale shows game count not stop count
- Bug #5: ScenarioD departureDate advanced to next day after last game
- Bug #6: ScenarioC date range boundary uses <= instead of <
- Bug #7: DrivingConstraints clamps maxHoursPerDriverPerDay via max(1.0,...)
- Bug #8: effectiveTripDuration uses inclusive day counting (+1)
- Bug #9: TripWizardViewModel validates endDate >= startDate
- Bug #10: allDates() uses min/max instead of first/last for robustness
- Bug #12: arrivalBeforeGameStart accounts for game end time at departure
- Bug #15: ScenarioBPlanner replaces force unwraps with safe unwrapping

Tests: 16 regression test suites + updated existing test expectations
Marketing: Remotion canvas set to 886x1920 for App Store preview spec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-15 17:08:50 -06:00
parent b320a773aa
commit 787a0f795e
14 changed files with 820 additions and 27 deletions

View File

@@ -317,7 +317,7 @@ struct DrivingConstraints {
init(from preferences: TripPreferences) {
self.numberOfDrivers = max(1, preferences.numberOfDrivers)
self.maxHoursPerDriverPerDay = preferences.maxDrivingHoursPerDriver ?? 8.0
self.maxHoursPerDriverPerDay = max(1.0, preferences.maxDrivingHoursPerDriver ?? 8.0)
}
}
@@ -472,7 +472,8 @@ extension ItineraryOption {
)
restDays.append(restDay)
}
currentDay = calendar.date(byAdding: .day, value: 1, to: currentDay) ?? currentDay
guard let nextDay = calendar.date(byAdding: .day, value: 1, to: currentDay) else { break }
currentDay = nextDay
}
return restDays
@@ -497,16 +498,17 @@ extension ItineraryOption {
/// All dates covered by the itinerary.
func allDates() -> [Date] {
let calendar = Calendar.current
guard let firstStop = stops.first,
let lastStop = stops.last else { return [] }
guard let earliestArrival = stops.map(\.arrivalDate).min(),
let latestDeparture = stops.map(\.departureDate).max() else { return [] }
var dates: [Date] = []
var currentDate = calendar.startOfDay(for: firstStop.arrivalDate)
let endDate = calendar.startOfDay(for: lastStop.departureDate)
var currentDate = calendar.startOfDay(for: earliestArrival)
let endDate = calendar.startOfDay(for: latestDeparture)
while currentDate <= endDate {
dates.append(currentDate)
currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate
guard let nextDate = calendar.date(byAdding: .day, value: 1, to: currentDate) else { break }
currentDate = nextDate
}
return dates
@@ -548,7 +550,7 @@ struct PlanningRequest {
/// Note: End date is extended to end-of-day to include all games on the last day,
/// since DateInterval.contains() uses exclusive end boundary.
var dateRange: DateInterval? {
guard preferences.endDate > preferences.startDate else { return nil }
guard preferences.endDate >= preferences.startDate else { return nil }
// Extend end date to end of day (23:59:59) to include games on the last day
let calendar = Calendar.current
let endOfDay = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: preferences.endDate)