Remove Spirit Airlines (defunct — merged into Frontier)
Spirit ceased operations, so the fetchSpiritStatus path and all NK references are dead code. Pulled out: - AirlineLoadService: drop `case "NK"` from the router, delete fetchSpiritStatus (the GetFlightInfoBI POST that was returning 403 even after our APIM key was accepted). - FlightLoadDetailView: drop the `schedule.airline.iata == "NK"` branch and the spiritUnavailableView placeholder. - FlightLoad model: update the airlineCode comment. - AirlineLoadIntegrationTests: remove test_NK_spirit and drop "NK" from statusOnlyAirlines / knownDailyFlights fallback table. - AIRLINE_INTEGRATION_GUIDE.md: tombstone the Spirit section and remove it from the cheat-sheets and recommendations. Test suite now: 6 airlines passing (AA, AS, B6, EK, KE, UA), 1 skipped (XE — WKWebView host required), 0 failures, runs in ~10s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +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 |
|
||||
| EK | ✅ Status-only | Confirms flight exists; load data requires PNR |
|
||||
| KE | ✅ Working | Returns seat count only (no capacity) |
|
||||
| NK | ❌ Broken (2026-05-26) | `GetFlightInfoBI` returns HTTP 403 with `{"getFlightInfoBIResult":null}`. APIM key is still accepted (401 without it), but the call itself is now rejected. Likely endpoint deprecation or new auth requirement. Spirit APK in `airlines/com.spirit.*` may have the new shape. |
|
||||
| ~~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 |
|
||||
|
||||
---
|
||||
@@ -208,23 +208,9 @@ Script ready at `scripts/jsx_availability.js`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Spirit Airlines — PARTIAL (status only, no standby)
|
||||
## 5. ~~Spirit Airlines~~ — DEFUNCT
|
||||
|
||||
**What you get:** Flight status, station/route data. **No standby — Spirit is a ULCC and doesn't run standby lists.**
|
||||
|
||||
**Auth:** Static APIM key (decrypted). Plain curl for GETs; POSTs mostly blocked by Akamai CyberFend sensor.
|
||||
|
||||
**Key:** `c6567af50d544dfbb3bc5dd99c6bb177`
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI" \
|
||||
-H "Ocp-Apim-Subscription-Key: c6567af50d544dfbb3bc5dd99c6bb177" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Platform: Android" \
|
||||
-d '{"departureStation":"FLL","arrivalStation":"ATL","departureDate":"2026-04-08"}'
|
||||
```
|
||||
|
||||
Seat maps require JWT + CyberFend sensor data (real device + Frida hook only).
|
||||
Spirit ceased operations and merged into Frontier. Removed from the codebase entirely. Section retained as a placeholder so the numbering below doesn't shift.
|
||||
|
||||
---
|
||||
|
||||
@@ -356,7 +342,6 @@ Same backend powers Lufthansa, SWISS, Austrian, Brussels.
|
||||
|------------|--------------------|---------|
|
||||
| Alaska | APIM key header | Lowest (curl works) |
|
||||
| Emirates | none | Lowest (curl works) |
|
||||
| Spirit | APIM key (GET only)| Low (curl works) |
|
||||
| JetBlue | apikey header | Low (curl works) |
|
||||
| Korean Air | `channel` header | Low (Playwright or curl) |
|
||||
| JSX | Playwright → JWT | Medium |
|
||||
@@ -382,7 +367,6 @@ These four are the core of any flight-load product. Alaska is the easiest to int
|
||||
|
||||
## Tier 2: Status only (useful, but no seat data)
|
||||
|
||||
- **Spirit** — status/routes, no standby (ULCC)
|
||||
- **Emirates** — status, zero auth
|
||||
- **Korean Air** — status; `flightSeatCount` returns 0 far out
|
||||
- **JetBlue** — status + route DB; loads need PNR
|
||||
@@ -399,7 +383,7 @@ These four are the core of any flight-load product. Alaska is the easiest to int
|
||||
3. Layer in American for the third major US carrier.
|
||||
4. JSX as a bonus — only route pairs that JSX serves (private terminals).
|
||||
5. For Delta/JetBlue: show flight status only, note "seat data unavailable" unless you have a PNR.
|
||||
6. Use Emirates/Korean Air/Spirit for status on international/ULCC routes.
|
||||
6. Use Emirates/Korean Air for status on international routes.
|
||||
|
||||
## Shared integration notes
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
/// Flight load data from airline APIs
|
||||
struct FlightLoad: Sendable {
|
||||
let airlineCode: String // "UA", "AA", "KE", "NK"
|
||||
let airlineCode: String // "UA", "AA", "KE", etc.
|
||||
let flightNumber: String // "UA2238"
|
||||
let cabins: [CabinLoad] // Full cabin data (United)
|
||||
let standbyList: [StandbyPassenger]
|
||||
|
||||
@@ -42,7 +42,6 @@ actor AirlineLoadService {
|
||||
switch code {
|
||||
case "UA": return await fetchUnitedLoad(flightNumber: flightNumber, date: date, origin: origin)
|
||||
case "AA": return await fetchAmericanLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
||||
case "NK": return await fetchSpiritStatus(origin: origin, destination: destination, date: date)
|
||||
case "KE": return await fetchKoreanAirLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
||||
case "B6": return await fetchJetBlueStatus(flightNumber: flightNumber, date: date, origin: origin)
|
||||
case "AS": return await fetchAlaskaLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
||||
@@ -413,59 +412,6 @@ actor AirlineLoadService {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Spirit Airlines
|
||||
|
||||
private func fetchSpiritStatus(origin: String, destination: String, date: Date) async -> FlightLoad? {
|
||||
guard let url = URL(string: "https://api.spirit.com/customermobileprod/2.8.0/v3/GetFlightInfoBI") else {
|
||||
print("[NK] Invalid URL")
|
||||
return nil
|
||||
}
|
||||
|
||||
let dateStr = dayString(from: date, originIATA: origin)
|
||||
let body: [String: String] = [
|
||||
"departureStation": origin.uppercased(),
|
||||
"arrivalStation": destination.uppercased(),
|
||||
"departureDate": dateStr
|
||||
]
|
||||
|
||||
print("[NK] POST \(url) body: \(body)")
|
||||
|
||||
do {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("c6567af50d544dfbb3bc5dd99c6bb177", forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
|
||||
request.setValue("Android", forHTTPHeaderField: "Platform")
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
let http = response as? HTTPURLResponse
|
||||
print("[NK] HTTP status: \(http?.statusCode ?? -1)")
|
||||
|
||||
if let bodyStr = String(data: data, encoding: .utf8) {
|
||||
print("[NK] Response body: \(bodyStr.prefix(500))")
|
||||
}
|
||||
|
||||
guard http?.statusCode == 200 else {
|
||||
print("[NK] Non-200 response")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Spirit is a ULCC with no standby program.
|
||||
return FlightLoad(
|
||||
airlineCode: "NK",
|
||||
flightNumber: "NK",
|
||||
cabins: [],
|
||||
standbyList: [],
|
||||
upgradeList: [],
|
||||
seatAvailability: []
|
||||
)
|
||||
} catch {
|
||||
print("[NK] Error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Korean Air
|
||||
|
||||
private func fetchKoreanAirLoad(
|
||||
@@ -1069,7 +1015,7 @@ actor AirlineLoadService {
|
||||
return result.isEmpty ? trimmed : result
|
||||
}
|
||||
|
||||
/// "yyyy-MM-dd" formatter for United, American, Spirit.
|
||||
/// "yyyy-MM-dd" formatter for United and American.
|
||||
/// NOTE: this is UTC-pinned and will cross the day boundary for users in
|
||||
/// non-UTC timezones. Prefer `dayString(from:originIATA:)` which resolves
|
||||
/// the departure airport's approximate local timezone.
|
||||
|
||||
@@ -25,8 +25,6 @@ struct FlightLoadDetailView: View {
|
||||
.tint(FlightTheme.accent)
|
||||
} else if let error {
|
||||
errorView(error)
|
||||
} else if schedule.airline.iata.uppercased() == "NK" {
|
||||
spiritUnavailableView
|
||||
} else if let load {
|
||||
loadContent(load)
|
||||
} else {
|
||||
@@ -114,16 +112,6 @@ struct FlightLoadDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Spirit Unavailable
|
||||
|
||||
private var spiritUnavailableView: some View {
|
||||
ContentUnavailableView {
|
||||
Label("Not Available", systemImage: "info.circle")
|
||||
} description: {
|
||||
Text("Spirit Airlines does not provide standby or load data.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Unsupported Airline
|
||||
|
||||
/// Shown when fetchLoad returns nil. That can be either:
|
||||
|
||||
@@ -19,8 +19,6 @@ import XCTest
|
||||
/// Pre-existing limitations (NOT bugs in these tests):
|
||||
/// - JSX (XE) uses a WKWebView path and can't run from unit tests on the
|
||||
/// simulator without a host scene. Skipped with a `XCTSkip`.
|
||||
/// - Spirit (NK) intentionally returns an empty FlightLoad — they have no
|
||||
/// standby/load program. Test just asserts non-nil.
|
||||
/// - Some carriers (notably AA, AS waitlist) only open the load endpoint
|
||||
/// close to departure. Tests prefer flights leaving < 24h out and skip
|
||||
/// with a helpful message if nothing's findable.
|
||||
@@ -39,16 +37,15 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
||||
/// Airlines whose load endpoint deliberately returns only flight
|
||||
/// status (no seat/standby data). We assert non-nil for these and
|
||||
/// stop short of the "must have data" check.
|
||||
private static let statusOnlyAirlines: Set<String> = ["NK", "B6", "EK"]
|
||||
private static let statusOnlyAirlines: Set<String> = ["B6", "EK"]
|
||||
|
||||
/// Hardcoded daily flights used as fallbacks when route-explorer's
|
||||
/// `/departures` data doesn't include the carrier we're looking for
|
||||
/// (notably ULCCs like NK and some international carriers like EK/KE
|
||||
/// that aren't in route-explorer's schedule feed). Each entry is a
|
||||
/// well-known daily operation that's been stable over time; if any
|
||||
/// of these stop operating, update the entry.
|
||||
/// (notably some international carriers like EK/KE that aren't in
|
||||
/// route-explorer's schedule feed). Each entry is a well-known daily
|
||||
/// operation that's been stable over time; if any of these stop
|
||||
/// operating, update the entry.
|
||||
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [
|
||||
"NK": ("401", "LAS", "FLL"), // Spirit Las Vegas → Fort Lauderdale, daily
|
||||
"EK": ("201", "JFK", "DXB"), // Emirates JFK → Dubai, daily flagship
|
||||
"KE": ("82", "JFK", "ICN"), // Korean Air JFK → Incheon, daily
|
||||
]
|
||||
@@ -83,13 +80,6 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func test_NK_spirit() async throws {
|
||||
try await runAirlineLoadTest(
|
||||
carrier: "NK",
|
||||
hubs: ["FLL", "MCO", "LAS", "DTW", "ORD"]
|
||||
)
|
||||
}
|
||||
|
||||
func test_KE_koreanAir() async throws {
|
||||
try await runAirlineLoadTest(
|
||||
carrier: "KE",
|
||||
|
||||
Reference in New Issue
Block a user