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:
Trey T
2026-05-26 14:23:33 -05:00
parent 62729213d7
commit 4a939340a2
5 changed files with 11 additions and 103 deletions
+4 -20
View File
@@ -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
+1 -1
View File
@@ -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]
+1 -55
View File
@@ -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.
-12
View File
@@ -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:
+5 -15
View File
@@ -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",