Add Aeromexico (AM) load integration

AM exposes a public Sabre GetPassengerListRQ proxy via AWS API Gateway —
no auth, no API key — used by the consumer app's flight-status widget.
The endpoint returns per-cabin authorized/available plus full standby +
upgrade passenger lists with isStaff flag, numeric priority, fare class,
position movement, and PII (matching what we get from AA but with
better cabin capacity data).

Implementation:
- AirlineLoadService.fetchAeromexicoLoad: parallel GETs against
  /rb/passengerliststandby and /rb/passengerlistupgrade, merging
  cabin info + per-list passengers into a single FlightLoad. Headers
  channel=web / flow=CHECKIN extracted from the AM APK Constant.smali.
  Cabin codes Y/C/P/F mapped to readable names (Economy / Clase Premier /
  Premier One / First).
- 4-digit zero-padding of the operating flight code (server validates
  ^[0-9]{4}$).
- "NONE LISTED" warning treated as nil (snapshot outside T-1d/T+2d
  window or no pax yet); explicit log so future failures are
  diagnosable.

Test infrastructure:
- Added test_AM_aeromexico using MEX/GDL/MTY/CUN hubs.
- Cascading fallback in runAirlineLoadTest: try the route-explorer
  discovered flight first; if it returns nil (typical for AM Connect
  regionals that aren't in Sabre), fall back to the known-daily flight
  (AM0058 MEX-MTY). Pattern useful for any future carrier whose
  regional ops don't show up in the load system.
- knownDailyFlights extended with AM0058 MEX-MTY.

Docs:
- AIRLINE_INTEGRATION_GUIDE: AM status row + full section 5b with
  endpoint params, response shape, snapshot window timing, failure
  modes, cabin code mapping, regional carrier caveat.

Test run 2026-05-26:
   AA, AM (cabins=1 upgrade=1), AS, B6, EK, KE, UA  ⏭️ XE
  7 passing, 1 skipped, 0 failures, 12s total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-26 15:31:59 -05:00
parent 4a939340a2
commit 398862e88b
3 changed files with 309 additions and 22 deletions
+35 -22
View File
@@ -48,6 +48,7 @@ final class AirlineLoadIntegrationTests: XCTestCase {
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [
"EK": ("201", "JFK", "DXB"), // Emirates JFK Dubai, daily flagship
"KE": ("82", "JFK", "ICN"), // Korean Air JFK Incheon, daily
"AM": ("58", "MEX", "MTY"), // Aeromexico MEX Monterrey, multiple daily
]
// MARK: - Per-airline tests
@@ -94,6 +95,15 @@ final class AirlineLoadIntegrationTests: XCTestCase {
)
}
func test_AM_aeromexico() async throws {
// Route-explorer doesn't include AM in /departures data, so this
// always falls through to the known-daily fallback (AM0058 MEX-MTY).
try await runAirlineLoadTest(
carrier: "AM",
hubs: ["MEX", "GDL", "MTY", "CUN"]
)
}
func test_XE_jsx() async throws {
// JSX uses a WKWebView path that needs a host scene / main thread.
// Skipped here; manual verification via the app remains.
@@ -127,11 +137,30 @@ final class AirlineLoadIntegrationTests: XCTestCase {
}
}
// Fallback path: route-explorer didn't return any flights for
// this carrier (typical for ULCCs and some international ops).
// Use a known-good daily flight if we have one configured.
if pickedFlight == nil, let known = Self.knownDailyFlights[carrier] {
NSLog("[\(carrier)Test] No \(carrier) flight in route-explorer data; using known daily \(carrier)\(known.flightNumber) \(known.origin)\(known.destination)")
// Try the discovered flight first when route-explorer found one.
if let flight = pickedFlight, let hub = pickedHub {
NSLog("[\(carrier)Test] Using \(carrier)\(flight.flightNumber) \(flight.departure.airportIata)\(flight.arrival.airportIata) departing \(flight.departure.dateTime) (hub queried: \(hub))")
let load = await loadService.fetchLoad(
airlineCode: flight.carrierIata,
flightNumber: "\(flight.flightNumber)",
date: flight.departure.dateTime,
origin: flight.departure.airportIata,
destination: flight.arrival.airportIata,
departureTime: nil
)
if load != nil {
let flightLabel = "\(carrier)\(flight.flightNumber) \(flight.departure.airportIata)\(flight.arrival.airportIata)"
try assertLoad(load, carrier: carrier, flightLabel: flightLabel, file: file, line: line)
return
}
NSLog("[\(carrier)Test] Discovered flight returned nil; trying known-daily fallback if available")
}
// Fallback: known-good daily flight. Triggers when route-explorer
// found nothing OR when the discovered flight returned nil (e.g. a
// regional carrier op that isn't in the upstream load system).
if let known = Self.knownDailyFlights[carrier] {
NSLog("[\(carrier)Test] Using known daily \(carrier)\(known.flightNumber) \(known.origin)\(known.destination)")
let load = await loadService.fetchLoad(
airlineCode: carrier,
flightNumber: known.flightNumber,
@@ -144,23 +173,7 @@ final class AirlineLoadIntegrationTests: XCTestCase {
return
}
guard let flight = pickedFlight, let hub = pickedHub else {
throw XCTSkip("Could not find a \(carrier) flight in the next 24h from any of: \(hubs.joined(separator: ", "))")
}
NSLog("[\(carrier)Test] Using \(carrier)\(flight.flightNumber) \(flight.departure.airportIata)\(flight.arrival.airportIata) departing \(flight.departure.dateTime) (hub queried: \(hub))")
let load = await loadService.fetchLoad(
airlineCode: flight.carrierIata,
flightNumber: "\(flight.flightNumber)",
date: flight.departure.dateTime,
origin: flight.departure.airportIata,
destination: flight.arrival.airportIata,
departureTime: nil
)
let flightLabel = "\(carrier)\(flight.flightNumber) \(flight.departure.airportIata)\(flight.arrival.airportIata) departing \(flight.departure.dateTime)"
try assertLoad(load, carrier: carrier, flightLabel: flightLabel, file: file, line: line)
throw XCTSkip("Could not find a working \(carrier) flight in the next 24h from any of: \(hubs.joined(separator: ", "))")
}
/// Shared assertion path for both the dynamic-discovery and