Add Sun Country (SY) load integration
Sun Country runs Navitaire (same PSS as JSX) but exposes their public availability search endpoint that returns BETTER load data than AA: per-flight `capacity` AND `sold` (booked passenger count), so we can compute exact load factor. Implementation: - AirlineLoadService.fetchSunCountryLoad: POSTs to syprod-api.suncountry.com/api/nsk/v4/availability/search/simple. Parses results→trips→journeysAvailableByMarket, matches by flight number, pulls capacity + sold + equipmentType from legInfo. - Returns a single Economy CabinLoad with capacity/booked = sold. No standby program — SY is single-cabin Y. - Auth: Azure APIM subscription key + a long-lived dotREZ JWT (both static, captured from suncountry.com network traffic, neither is a user session token). - Anti-bot: Imperva WAF in front of syprod-api.suncountry.com is gated on User-Agent + Referer + Origin headers. applySunCountryBrowserHeaders mirrors the pattern we use for UA / AA. NO WebView needed. - Explicit ⚠️ log when 403 Incapsula response detected, pointing at the header helper. Test infrastructure: - knownDailyFlights now carries a dayOffset (today vs tomorrow) per carrier — different upstreams have different snapshot windows: AM is T-1d..T+0 (today); SY's Navitaire only returns future flights (tomorrow); others default to tomorrow as a safer choice. - Added test_SY_sunCountry with hubs MSP/LAS/MCO/DEN. Fallback is SY104 LAS-MSP tomorrow. Docs: - AIRLINE_INTEGRATION_GUIDE: SY status row + full section 5c covering endpoint, auth, headers, response shape, failure modes, and how to re-capture tokens when they rotate. Reverse-engineering notes: - SY app is Flutter (Dart AOT) — bridge smali is minimal. Strings extracted from libapp.so revealed isNonRevTrip/isStandby/ inventoryControl keywords + the syprod-api hostname. - Token endpoint is PUT (not POST). Returns {"data":null} — token is the existing Authorization JWT, not a session refresh. - Confirmed working from plain curl with browser headers (no Imperva TLS-fingerprint gate beyond UA/Referer/Origin). Test run 2026-05-26 (xcodebuild test): ✅ AA, AM, AS, B6, EK, KE, SY (capacity=186 sold=184 load=99%), UA ⏭️ XE 8 passing, 1 skipped, 0 failures, 11s total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ Drop-in reference for integrating flight load / seat availability data from 11 a
|
||||
| EK | ✅ Status-only | Confirms flight exists; load data requires PNR |
|
||||
| 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. |
|
||||
| SY | ✅ Working | Navitaire availability search returns **`capacity` + `sold` per flight** (true load factor, better than AA). Imperva WAF gated on browser-shaped headers. No standby list (SY is single-class). |
|
||||
| ~~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 |
|
||||
|
||||
@@ -280,6 +281,77 @@ GET https://lw18yj0mhb.execute-api.us-east-1.amazonaws.com/rb/passengerlistupgra
|
||||
|
||||
---
|
||||
|
||||
## 5c. Sun Country — WORKING (true load factor)
|
||||
|
||||
**What you get:** Per-flight `capacity`, **`sold` (booked passenger count)**, equipment type, and per-fare-class `availableCount`. Direct load factor calculation (sold/capacity). No standby list — SY is single-cabin Y, no upgrade program.
|
||||
|
||||
**Auth:** Azure APIM subscription key + a long-lived dotREZ JWT. Both static, both extracted from suncountry.com network traffic. No user session or login required.
|
||||
|
||||
**Anti-bot:** Imperva WAF in front of `syprod-api.suncountry.com`. Gated on `User-Agent` + `Referer: https://www.suncountry.com/` + `Origin: https://www.suncountry.com` headers. Bare curl returns 403 with an Incapsula page; full browser-shaped headers pass cleanly. No WebView needed.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
POST https://syprod-api.suncountry.com/api/nsk/v4/availability/search/simple
|
||||
Headers:
|
||||
Ocp-Apim-Subscription-Key: bc7f707786c44a56859c396102f6cd21
|
||||
Authorization: <dotREZ JWT — eyJhbGc...>
|
||||
User-Agent: Mozilla/5.0 (Macintosh; ...) Chrome/145 Safari/537.36
|
||||
Referer: https://www.suncountry.com/
|
||||
Origin: https://www.suncountry.com
|
||||
Content-Type: application/json
|
||||
|
||||
Body:
|
||||
{
|
||||
"Origin": "MSP",
|
||||
"Destination": "LAX",
|
||||
"BeginDate": "2026-06-15",
|
||||
"EndDate": "2026-06-15",
|
||||
"Passengers": { "Types": [{"Type":"ADT","Count":1}] },
|
||||
"Currency": "USD"
|
||||
}
|
||||
```
|
||||
|
||||
**Response shape (truncated to the load-relevant bits):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"results": [{ "trips": [{
|
||||
"journeysAvailableByMarket": {
|
||||
"MSP|LAX": [{
|
||||
"designator": {"origin":"MSP","destination":"LAX","departure":"...","arrival":"..."},
|
||||
"segments": [{
|
||||
"identifier": {"identifier":"421","carrierCode":"SY"},
|
||||
"legs": [{
|
||||
"legInfo": {
|
||||
"capacity": 186, // total seats
|
||||
"adjustedCapacity": 186,
|
||||
"lid": 186,
|
||||
"sold": 106, // booked passenger count
|
||||
"equipmentType": "78T",
|
||||
"departureTimeUtc": "...",
|
||||
"arrivalTimeUtc": "..."
|
||||
}
|
||||
}]
|
||||
}],
|
||||
"fares": [{ "details": [{ "availableCount": 4 }] }]
|
||||
}]
|
||||
}
|
||||
}]}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is better than AA:** AA returns "seatsAvailable" per cabin without telling you capacity. SY gives both, so load factor = sold/capacity is exact (~57% above for SY421 MSP-LAX).
|
||||
|
||||
**Failure modes:**
|
||||
- HTTP 403 with Incapsula HTML → User-Agent / Referer / Origin headers dropped
|
||||
- HTTP 200 with empty `journeysAvailableByMarket` → flight already departed (Navitaire only returns future flights) or no SY service on that route/date
|
||||
- HTTP 401 → APIM key or JWT no longer valid; re-capture from www.suncountry.com network traffic
|
||||
|
||||
**Re-capturing tokens:** Open suncountry.com in a browser DevTools network tab, find the `PUT /api/nsk/v1/token` request, copy the `Ocp-Apim-Subscription-Key` and `Authorization` header values. Update `sunCountryAPIMKey` and `sunCountryJWT` constants in `AirlineLoadService.swift`.
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
|
||||
Reference in New Issue
Block a user