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:
@@ -12,6 +12,7 @@ Drop-in reference for integrating flight load / seat availability data from 11 a
|
|||||||
| B6 | ✅ Status-only | Confirms flight exists; no load data without check-in session |
|
| B6 | ✅ Status-only | Confirms flight exists; no load data without check-in session |
|
||||||
| EK | ✅ Status-only | Confirms flight exists; load data requires PNR |
|
| EK | ✅ Status-only | Confirms flight exists; load data requires PNR |
|
||||||
| KE | ✅ Working | Returns seat count only (no capacity) |
|
| KE | ✅ Working | Returns seat count only (no capacity) |
|
||||||
|
| AM | ✅ Working | Public AWS gateway Sabre proxy. Returns per-cabin `authorized`+`available` + full standby/upgrade passenger lists with `isStaff` flag and priority. Snapshot window: T-1d to T+2d. |
|
||||||
| ~~NK~~ | Removed | Spirit Airlines ceased operations (merged into Frontier). Removed from `AirlineLoadService` and tests. |
|
| ~~NK~~ | Removed | Spirit Airlines ceased operations (merged into Frontier). Removed from `AirlineLoadService` and tests. |
|
||||||
| XE | Manual only | WKWebView path; unit tests can't exercise it |
|
| XE | Manual only | WKWebView path; unit tests can't exercise it |
|
||||||
|
|
||||||
@@ -214,6 +215,71 @@ Spirit ceased operations and merged into Frontier. Removed from the codebase ent
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 5b. Aeromexico — WORKING (richer than AA in some ways)
|
||||||
|
|
||||||
|
**What you get:** per-cabin `authorized` (capacity) + `available` (open seats), full standby + upgrade passenger lists with `isStaff` flag, numeric priority, fare class, booking class, ascendsToClass, original/new position, check-in / board status, PII (firstName, lastName, reservationCode/PNR).
|
||||||
|
|
||||||
|
**Auth:** None. Public AWS API Gateway. Headers required: `channel: web`, `flow: CHECKIN`, `x-transaction-id: <uuid>`. Values extracted from `com.aeromexico.aeromexico.amwidgets.utils.Constant` in the APK.
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
```
|
||||||
|
GET https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/passengerliststandby
|
||||||
|
?departureAirport=<IATA>
|
||||||
|
&code=<4-digit, zero-padded>
|
||||||
|
&departureDate=<YYYY-MM-DD>
|
||||||
|
&operatingCarrier=AM
|
||||||
|
&operatingFlightCode=<4-digit, zero-padded>
|
||||||
|
|
||||||
|
GET https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/passengerlistupgrade
|
||||||
|
?<same params>
|
||||||
|
```
|
||||||
|
|
||||||
|
`operatingFlightCode` is validated against `^[0-9]{4}$` — zero-pad short flight numbers.
|
||||||
|
|
||||||
|
**Response shape:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"itineraryInfo": {"airline":"AM","flight":"0058","origin":"MEX","destination":"MTY","aircraftType":"789"},
|
||||||
|
"cabinInfoList": [{"cabin":"Y","authorized":238,"available":0}],
|
||||||
|
"totalListed": 1,
|
||||||
|
"passengers": [{
|
||||||
|
"isStaff": true,
|
||||||
|
"rawPriorityCode": "SAE",
|
||||||
|
"priorityCode": {"id":"SAE","priority":21},
|
||||||
|
"status": "STB",
|
||||||
|
"bookingClass": "H",
|
||||||
|
"ascendsToClass": "Y",
|
||||||
|
"firstName": "RAMSITO",
|
||||||
|
"lastName": "UNO",
|
||||||
|
"reservationCode": "OBLWDT",
|
||||||
|
"passengerId": "0A6612610001",
|
||||||
|
"seat": null,
|
||||||
|
"originalPosition": 2,
|
||||||
|
"newPosition": 1,
|
||||||
|
"checkInStatus": false,
|
||||||
|
"boardStatus": false,
|
||||||
|
"boardingPassFlag": false
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Snapshot window** (empirical, AM0058 MEX-MTY):
|
||||||
|
- T-3 days and earlier → `NONE LISTED` (data purged)
|
||||||
|
- **T-1 day → T+0** → snapshot live, `passengers[]` populates when listed
|
||||||
|
- T+1, T+2 → `NONE LISTED` (flight known but no snapshot)
|
||||||
|
- T+3 and beyond → `FLIGHT NOT INITIALIZED`
|
||||||
|
|
||||||
|
**Failure modes** to watch for in the response body:
|
||||||
|
- `NONE LISTED` → params valid, no passengers / no snapshot yet
|
||||||
|
- `FLIGHT NOT INITIALIZED - INVALID DATE OR CITY` → flight number doesn't match a real AM operation on that date+airport, OR snapshot window not open
|
||||||
|
- The `code` query param is ignored — only `operatingCarrier` + `operatingFlightCode` + `departureAirport` + `departureDate` are discriminating
|
||||||
|
|
||||||
|
**Cabin codes:** `Y` = Economy, `C` = Clase Premier (business), `P` = Premier One (long-haul biz/first), `F` = First. Mapped in `aeromexicoCabinName(code:)`.
|
||||||
|
|
||||||
|
**AM Connect / regional flights** (e.g. AM1460 MEX-QRO) often return `FLIGHT NOT INITIALIZED` — they're not in AM's Sabre system. The integration falls back to a known-daily mainline flight (AM0058 MEX-MTY) when route-explorer surfaces a regional that the load endpoint doesn't recognise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 6. JetBlue — PARTIAL (status yes, loads need PNR)
|
## 6. JetBlue — PARTIAL (status yes, loads need PNR)
|
||||||
|
|
||||||
**What you get without PNR:** Flight status, full route database (12MB of origin/dest pairs, Mint/seasonal flags).
|
**What you get without PNR:** Flight status, full route database (12MB of origin/dest pairs, Mint/seasonal flags).
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ actor AirlineLoadService {
|
|||||||
case "B6": return await fetchJetBlueStatus(flightNumber: flightNumber, date: date, origin: origin)
|
case "B6": return await fetchJetBlueStatus(flightNumber: flightNumber, date: date, origin: origin)
|
||||||
case "AS": return await fetchAlaskaLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
case "AS": return await fetchAlaskaLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
||||||
case "EK": return await fetchEmiratesStatus(flightNumber: flightNumber, date: date, origin: origin)
|
case "EK": return await fetchEmiratesStatus(flightNumber: flightNumber, date: date, origin: origin)
|
||||||
|
case "AM": return await fetchAeromexicoLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
||||||
case "XE": return await fetchJSXLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination, departureTime: departureTime)
|
case "XE": return await fetchJSXLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination, departureTime: departureTime)
|
||||||
default:
|
default:
|
||||||
print("[LoadService] Unsupported airline: \(code)")
|
print("[LoadService] Unsupported airline: \(code)")
|
||||||
@@ -850,6 +851,213 @@ actor AirlineLoadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Aeromexico
|
||||||
|
|
||||||
|
/// Aeromexico exposes a Sabre `GetPassengerListRQ` proxy on a public AWS
|
||||||
|
/// API Gateway used by their consumer app's flight-status widget. The
|
||||||
|
/// endpoint requires no API key — just a `channel: web` / `flow: CHECKIN`
|
||||||
|
/// header pair (constants extracted from the AM Android APK).
|
||||||
|
///
|
||||||
|
/// Response includes:
|
||||||
|
/// - `cabinInfoList[].authorized` (capacity) + `.available` (open seats)
|
||||||
|
/// - `passengers[]` with full PII, priority, `isStaff` flag, positions
|
||||||
|
///
|
||||||
|
/// Snapshot persists at least T-1d through T+2d; outside that the gateway
|
||||||
|
/// answers `FLIGHT NOT INITIALIZED` or `NONE LISTED`.
|
||||||
|
private func fetchAeromexicoLoad(
|
||||||
|
flightNumber: String,
|
||||||
|
date: Date,
|
||||||
|
origin: String,
|
||||||
|
destination: String
|
||||||
|
) async -> FlightLoad? {
|
||||||
|
let num = stripAirlinePrefix(flightNumber)
|
||||||
|
// Endpoint validates flight code against ^[0-9]{4}$.
|
||||||
|
let padded = String(format: "%04d", Int(num) ?? 0)
|
||||||
|
let dateStr = dayString(from: date, originIATA: origin)
|
||||||
|
|
||||||
|
async let standbyResp = fetchAeromexicoList(
|
||||||
|
endpoint: "passengerliststandby",
|
||||||
|
flightCode: padded,
|
||||||
|
origin: origin,
|
||||||
|
departureDate: dateStr
|
||||||
|
)
|
||||||
|
async let upgradeResp = fetchAeromexicoList(
|
||||||
|
endpoint: "passengerlistupgrade",
|
||||||
|
flightCode: padded,
|
||||||
|
origin: origin,
|
||||||
|
departureDate: dateStr
|
||||||
|
)
|
||||||
|
|
||||||
|
let sb = await standbyResp
|
||||||
|
let up = await upgradeResp
|
||||||
|
|
||||||
|
// If both calls failed entirely we have nothing.
|
||||||
|
guard sb != nil || up != nil else {
|
||||||
|
print("[AM] Both standby and upgrade calls returned nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cabin info: same per leg, either response carries it.
|
||||||
|
var cabins: [CabinLoad] = []
|
||||||
|
if let cabinList = sb?["cabinInfoList"] as? [[String: Any]] {
|
||||||
|
cabins = Self.parseAeromexicoCabins(cabinList)
|
||||||
|
} else if let cabinList = up?["cabinInfoList"] as? [[String: Any]] {
|
||||||
|
cabins = Self.parseAeromexicoCabins(cabinList)
|
||||||
|
}
|
||||||
|
|
||||||
|
let standbyList = Self.parseAeromexicoPassengers(
|
||||||
|
sb?["passengers"] as? [[String: Any]] ?? [],
|
||||||
|
listName: "Standby"
|
||||||
|
)
|
||||||
|
let upgradeList = Self.parseAeromexicoPassengers(
|
||||||
|
up?["passengers"] as? [[String: Any]] ?? [],
|
||||||
|
listName: "Upgrade"
|
||||||
|
)
|
||||||
|
|
||||||
|
let sbTotal = sb?["totalListed"] as? Int ?? 0
|
||||||
|
let upTotal = up?["totalListed"] as? Int ?? 0
|
||||||
|
print("[AM] parsed cabins=\(cabins.count) standby=\(standbyList.count)/\(sbTotal) upgrade=\(upgradeList.count)/\(upTotal)")
|
||||||
|
|
||||||
|
// Surface "no data yet" cleanly: if every response was NONE LISTED and
|
||||||
|
// we got nothing back, return nil so the detail view shows the
|
||||||
|
// "Load data not available" state rather than an empty card.
|
||||||
|
if cabins.isEmpty && standbyList.isEmpty && upgradeList.isEmpty {
|
||||||
|
print("[AM] No usable data in response (snapshot likely outside T-1d / T+2d window)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlightLoad(
|
||||||
|
airlineCode: "AM",
|
||||||
|
flightNumber: "AM\(num)",
|
||||||
|
cabins: cabins,
|
||||||
|
standbyList: standbyList,
|
||||||
|
upgradeList: upgradeList,
|
||||||
|
seatAvailability: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single GET against the AM passenger-list gateway. Returns the parsed
|
||||||
|
/// JSON dict on success (with or without populated lists), or nil if the
|
||||||
|
/// request failed at the transport layer.
|
||||||
|
private func fetchAeromexicoList(
|
||||||
|
endpoint: String,
|
||||||
|
flightCode: String,
|
||||||
|
origin: String,
|
||||||
|
departureDate: String
|
||||||
|
) async -> [String: Any]? {
|
||||||
|
var components = URLComponents(string: "https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/\(endpoint)")
|
||||||
|
components?.queryItems = [
|
||||||
|
URLQueryItem(name: "departureAirport", value: origin.uppercased()),
|
||||||
|
URLQueryItem(name: "code", value: flightCode),
|
||||||
|
URLQueryItem(name: "departureDate", value: departureDate),
|
||||||
|
URLQueryItem(name: "operatingCarrier", value: "AM"),
|
||||||
|
URLQueryItem(name: "operatingFlightCode", value: flightCode)
|
||||||
|
]
|
||||||
|
guard let url = components?.url else {
|
||||||
|
print("[AM] Invalid URL for \(endpoint)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
print("[AM] GET \(url.absoluteString)")
|
||||||
|
|
||||||
|
do {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue("web", forHTTPHeaderField: "channel")
|
||||||
|
request.setValue("CHECKIN", forHTTPHeaderField: "flow")
|
||||||
|
request.setValue(UUID().uuidString, forHTTPHeaderField: "x-transaction-id")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
let status = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||||
|
print("[AM] \(endpoint) HTTP \(status), \(data.count) bytes")
|
||||||
|
guard status == 200 else {
|
||||||
|
if let body = String(data: data, encoding: .utf8) {
|
||||||
|
print("[AM] \(endpoint) non-200 body: \(body.prefix(300))")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||||
|
print("[AM] \(endpoint) JSON parse failed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Log the warning surface so future failures are diagnosable.
|
||||||
|
if let warnings = json["warnings"] as? [[String: Any]], !warnings.isEmpty {
|
||||||
|
let msgs = warnings.compactMap { $0["errorMessage"] as? String }.joined(separator: " | ")
|
||||||
|
print("[AM] \(endpoint) warnings: \(msgs)")
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
} catch {
|
||||||
|
print("[AM] \(endpoint) error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func parseAeromexicoCabins(_ raw: [[String: Any]]) -> [CabinLoad] {
|
||||||
|
raw.compactMap { entry in
|
||||||
|
let cabinCode = entry["cabin"] as? String ?? "?"
|
||||||
|
let authorized = entry["authorized"] as? Int ?? 0
|
||||||
|
let available = entry["available"] as? Int ?? 0
|
||||||
|
// Skip placeholder rows with no capacity at all.
|
||||||
|
guard authorized > 0 || available > 0 else { return nil }
|
||||||
|
let booked = max(0, authorized - available)
|
||||||
|
return CabinLoad(
|
||||||
|
name: aeromexicoCabinName(code: cabinCode),
|
||||||
|
capacity: authorized,
|
||||||
|
booked: booked,
|
||||||
|
revenueStandby: 0,
|
||||||
|
nonRevStandby: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map AM's single-letter cabin codes to user-readable names. AM uses
|
||||||
|
/// `Y` for economy, `C` (Clase Premier) for business, and `P` for
|
||||||
|
/// Premier One (their first/long-haul biz). Anything unknown falls
|
||||||
|
/// through with the raw code.
|
||||||
|
private static func aeromexicoCabinName(code: String) -> String {
|
||||||
|
switch code.uppercased() {
|
||||||
|
case "Y": return "Economy"
|
||||||
|
case "C": return "Clase Premier"
|
||||||
|
case "P": return "Premier One"
|
||||||
|
case "F": return "First"
|
||||||
|
default: return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func parseAeromexicoPassengers(
|
||||||
|
_ raw: [[String: Any]],
|
||||||
|
listName: String
|
||||||
|
) -> [StandbyPassenger] {
|
||||||
|
raw.enumerated().compactMap { (index, entry) in
|
||||||
|
let first = entry["firstName"] as? String ?? ""
|
||||||
|
let last = entry["lastName"] as? String ?? ""
|
||||||
|
// AM lists names in full, redact for display the way AA does
|
||||||
|
// (last name + first initial). If either piece is missing,
|
||||||
|
// fall back gracefully.
|
||||||
|
let display: String
|
||||||
|
if !last.isEmpty, !first.isEmpty {
|
||||||
|
display = "\(last), \(first.prefix(1))"
|
||||||
|
} else {
|
||||||
|
display = (last.isEmpty ? first : last)
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = (entry["newPosition"] as? Int)
|
||||||
|
?? (entry["originalPosition"] as? Int)
|
||||||
|
?? (index + 1)
|
||||||
|
|
||||||
|
let cleared = (entry["boardingPassFlag"] as? Bool ?? false)
|
||||||
|
|| (entry["boardStatus"] as? Bool ?? false)
|
||||||
|
|| ((entry["seat"] as? String).map { !$0.isEmpty } ?? false)
|
||||||
|
|
||||||
|
return StandbyPassenger(
|
||||||
|
order: position,
|
||||||
|
displayName: display,
|
||||||
|
cleared: cleared,
|
||||||
|
seat: entry["seat"] as? String,
|
||||||
|
listName: listName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - JSX (JetSuiteX)
|
// MARK: - JSX (JetSuiteX)
|
||||||
|
|
||||||
private func fetchJSXLoad(
|
private func fetchJSXLoad(
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
|||||||
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [
|
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [
|
||||||
"EK": ("201", "JFK", "DXB"), // Emirates JFK → Dubai, daily flagship
|
"EK": ("201", "JFK", "DXB"), // Emirates JFK → Dubai, daily flagship
|
||||||
"KE": ("82", "JFK", "ICN"), // Korean Air JFK → Incheon, daily
|
"KE": ("82", "JFK", "ICN"), // Korean Air JFK → Incheon, daily
|
||||||
|
"AM": ("58", "MEX", "MTY"), // Aeromexico MEX → Monterrey, multiple daily
|
||||||
]
|
]
|
||||||
|
|
||||||
// MARK: - Per-airline tests
|
// 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 {
|
func test_XE_jsx() async throws {
|
||||||
// JSX uses a WKWebView path that needs a host scene / main thread.
|
// JSX uses a WKWebView path that needs a host scene / main thread.
|
||||||
// Skipped here; manual verification via the app remains.
|
// 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
|
// Try the discovered flight first when route-explorer found one.
|
||||||
// this carrier (typical for ULCCs and some international ops).
|
if let flight = pickedFlight, let hub = pickedHub {
|
||||||
// Use a known-good daily flight if we have one configured.
|
NSLog("[\(carrier)Test] Using \(carrier)\(flight.flightNumber) \(flight.departure.airportIata)→\(flight.arrival.airportIata) departing \(flight.departure.dateTime) (hub queried: \(hub))")
|
||||||
if pickedFlight == nil, let known = Self.knownDailyFlights[carrier] {
|
let load = await loadService.fetchLoad(
|
||||||
NSLog("[\(carrier)Test] No \(carrier) flight in route-explorer data; using known daily \(carrier)\(known.flightNumber) \(known.origin)→\(known.destination)")
|
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(
|
let load = await loadService.fetchLoad(
|
||||||
airlineCode: carrier,
|
airlineCode: carrier,
|
||||||
flightNumber: known.flightNumber,
|
flightNumber: known.flightNumber,
|
||||||
@@ -144,23 +173,7 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let flight = pickedFlight, let hub = pickedHub else {
|
throw XCTSkip("Could not find a working \(carrier) flight in the next 24h from any of: \(hubs.joined(separator: ", "))")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared assertion path for both the dynamic-discovery and
|
/// Shared assertion path for both the dynamic-discovery and
|
||||||
|
|||||||
Reference in New Issue
Block a user