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 |
|
| 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. |
|
| 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. |
|
| ~~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 |
|
||||||
|
|
||||||
@@ -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)
|
## 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).
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ actor AirlineLoadService {
|
|||||||
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 "AM": return await fetchAeromexicoLoad(flightNumber: flightNumber, date: date, origin: origin, destination: destination)
|
||||||
|
case "SY": return await fetchSunCountryLoad(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)")
|
||||||
@@ -1058,6 +1059,161 @@ actor AirlineLoadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sun Country
|
||||||
|
|
||||||
|
/// Sun Country runs on Navitaire (same PSS as JSX). Their public booking
|
||||||
|
/// availability search returns full per-flight inventory data including
|
||||||
|
/// `sold` (booked passenger count) and `capacity` per leg — better than
|
||||||
|
/// AA, which only gives seat-availability counts. No standby program
|
||||||
|
/// exposed via this endpoint (SY is single-class), so we return cabin
|
||||||
|
/// load only.
|
||||||
|
///
|
||||||
|
/// Imperva WAF in front of `syprod-api.suncountry.com` blocks bare
|
||||||
|
/// curl. Gated on User-Agent / Referer / Origin headers (same pattern
|
||||||
|
/// as American). Browser-shaped headers pass cleanly.
|
||||||
|
///
|
||||||
|
/// Endpoint: `POST /api/nsk/v4/availability/search/simple`
|
||||||
|
/// Auth: Azure APIM key + a long-lived dotREZ JWT (both extracted from
|
||||||
|
/// network traffic of suncountry.com; neither is a user session token).
|
||||||
|
private static let sunCountryAPIMKey = "bc7f707786c44a56859c396102f6cd21"
|
||||||
|
|
||||||
|
/// dotREZ JWT used as the `Authorization` header. Issued by "dotREZ
|
||||||
|
/// API" with `sub: DOTREZ` — a static API client identity, not a user
|
||||||
|
/// token. If this stops working, capture a fresh one from
|
||||||
|
/// suncountry.com's PUT /api/nsk/v1/token request.
|
||||||
|
private static let sunCountryJWT =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJET1RSRVoiLCJqdGkiOiI0ZWQyNjQ0Ny0zOTU4LWQ1YjQtZTkxNi0xZDM4YWFiNTQ0ZTMiLCJpc3MiOiJkb3RSRVogQVBJIn0.W_zpG_6nZbD37S7hsWgahYG9Dc1gwgG_8s0KA3V72Qg"
|
||||||
|
|
||||||
|
private func fetchSunCountryLoad(
|
||||||
|
flightNumber: String,
|
||||||
|
date: Date,
|
||||||
|
origin: String,
|
||||||
|
destination: String
|
||||||
|
) async -> FlightLoad? {
|
||||||
|
let num = stripAirlinePrefix(flightNumber)
|
||||||
|
let dateStr = dayString(from: date, originIATA: origin)
|
||||||
|
|
||||||
|
guard let url = URL(string: "https://syprod-api.suncountry.com/api/nsk/v4/availability/search/simple") else {
|
||||||
|
print("[SY] Invalid URL")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let body: [String: Any] = [
|
||||||
|
"Origin": origin.uppercased(),
|
||||||
|
"Destination": destination.uppercased(),
|
||||||
|
"BeginDate": dateStr,
|
||||||
|
"EndDate": dateStr,
|
||||||
|
"Passengers": ["Types": [["Type": "ADT", "Count": 1]]],
|
||||||
|
"Currency": "USD"
|
||||||
|
]
|
||||||
|
|
||||||
|
print("[SY] POST \(url.absoluteString) for SY\(num) \(origin)→\(destination) on \(dateStr)")
|
||||||
|
|
||||||
|
do {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
Self.applySunCountryBrowserHeaders(to: &request)
|
||||||
|
request.setValue(Self.sunCountryAPIMKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
|
||||||
|
request.setValue(Self.sunCountryJWT, forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
let status = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||||
|
print("[SY] HTTP status: \(status), \(data.count) bytes")
|
||||||
|
|
||||||
|
if status != 200 {
|
||||||
|
if let bodyStr = String(data: data, encoding: .utf8) {
|
||||||
|
print("[SY] body (first 500): \(bodyStr.prefix(500))")
|
||||||
|
if status == 403, bodyStr.contains("Incapsula") || bodyStr.contains("Imperva") {
|
||||||
|
print("[SY] ⚠️ Imperva WAF rejection — check the User-Agent / Referer / Origin headers in applySunCountryBrowserHeaders")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
let dataObj = json["data"] as? [String: Any],
|
||||||
|
let results = dataObj["results"] as? [[String: Any]] else {
|
||||||
|
print("[SY] Response shape unexpected — top-level keys: \((try? JSONSerialization.jsonObject(with: data) as? [String: Any])?.keys.sorted() ?? [])")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk results → trips → journeysAvailableByMarket → match by
|
||||||
|
// flight number, then read sold/capacity from the leg.
|
||||||
|
let key = "\(origin.uppercased())|\(destination.uppercased())"
|
||||||
|
var match: [String: Any]?
|
||||||
|
outer: for result in results {
|
||||||
|
for trip in (result["trips"] as? [[String: Any]]) ?? [] {
|
||||||
|
let market = trip["journeysAvailableByMarket"] as? [String: Any]
|
||||||
|
for journey in (market?[key] as? [[String: Any]]) ?? [] {
|
||||||
|
let segments = journey["segments"] as? [[String: Any]] ?? []
|
||||||
|
let first = segments.first
|
||||||
|
let flightId = ((first?["identifier"] as? [String: Any])?["identifier"] as? String) ?? ""
|
||||||
|
if flightId == num {
|
||||||
|
match = journey
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let journey = match,
|
||||||
|
let segments = journey["segments"] as? [[String: Any]],
|
||||||
|
let legs = (segments.first?["legs"] as? [[String: Any]]),
|
||||||
|
let legInfo = legs.first?["legInfo"] as? [String: Any] else {
|
||||||
|
print("[SY] No matching flight \(num) in response for \(origin)-\(destination)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let capacity = (legInfo["capacity"] as? Int) ?? (legInfo["adjustedCapacity"] as? Int) ?? 0
|
||||||
|
let sold = legInfo["sold"] as? Int ?? 0
|
||||||
|
let equipment = legInfo["equipmentType"] as? String ?? ""
|
||||||
|
|
||||||
|
print("[SY] Found SY\(num): capacity=\(capacity) sold=\(sold) equipment=\(equipment) load=\(capacity > 0 ? Double(sold)/Double(capacity) : 0)")
|
||||||
|
|
||||||
|
if capacity <= 0 {
|
||||||
|
print("[SY] Capacity was 0; treating as no data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let cabin = CabinLoad(
|
||||||
|
name: "Economy",
|
||||||
|
capacity: capacity,
|
||||||
|
booked: sold,
|
||||||
|
revenueStandby: 0,
|
||||||
|
nonRevStandby: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return FlightLoad(
|
||||||
|
airlineCode: "SY",
|
||||||
|
flightNumber: "SY\(num)",
|
||||||
|
cabins: [cabin],
|
||||||
|
standbyList: [],
|
||||||
|
upgradeList: [],
|
||||||
|
seatAvailability: []
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
print("[SY] error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browser-shaped headers so Imperva lets the request through. The
|
||||||
|
/// API host is gated on User-Agent + Referer + Origin; bare curl
|
||||||
|
/// (or default URLSession) gets 403 with an Incapsula page.
|
||||||
|
private static func applySunCountryBrowserHeaders(to request: inout URLRequest) {
|
||||||
|
request.setValue(
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
||||||
|
+ "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
|
||||||
|
forHTTPHeaderField: "User-Agent"
|
||||||
|
)
|
||||||
|
request.setValue("https://www.suncountry.com/", forHTTPHeaderField: "Referer")
|
||||||
|
request.setValue("https://www.suncountry.com", forHTTPHeaderField: "Origin")
|
||||||
|
request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - JSX (JetSuiteX)
|
// MARK: - JSX (JetSuiteX)
|
||||||
|
|
||||||
private func fetchJSXLoad(
|
private func fetchJSXLoad(
|
||||||
|
|||||||
@@ -45,10 +45,16 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
|||||||
/// route-explorer's schedule feed). Each entry is a well-known daily
|
/// route-explorer's schedule feed). Each entry is a well-known daily
|
||||||
/// operation that's been stable over time; if any of these stop
|
/// operation that's been stable over time; if any of these stop
|
||||||
/// operating, update the entry.
|
/// operating, update the entry.
|
||||||
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String)] = [
|
///
|
||||||
"EK": ("201", "JFK", "DXB"), // Emirates JFK → Dubai, daily flagship
|
/// `dayOffset` controls which day's flight to probe:
|
||||||
"KE": ("82", "JFK", "ICN"), // Korean Air JFK → Incheon, daily
|
/// - `0` (today) for carriers whose snapshot window is T-1d to T+0 (AM)
|
||||||
"AM": ("58", "MEX", "MTY"), // Aeromexico MEX → Monterrey, multiple daily
|
/// - `1` (tomorrow) for carriers whose API only returns future flights
|
||||||
|
/// (SY's Navitaire availability/search drops already-departed legs)
|
||||||
|
private static let knownDailyFlights: [String: (flightNumber: String, origin: String, destination: String, dayOffset: Int)] = [
|
||||||
|
"EK": ("201", "JFK", "DXB", 1), // Emirates JFK → Dubai, daily flagship
|
||||||
|
"KE": ("82", "JFK", "ICN", 1), // Korean Air JFK → Incheon, daily
|
||||||
|
"AM": ("58", "MEX", "MTY", 0), // Aeromexico — snapshot only T-1d/T+0
|
||||||
|
"SY": ("104", "LAS", "MSP", 1), // Sun Country — Navitaire shows future only
|
||||||
]
|
]
|
||||||
|
|
||||||
// MARK: - Per-airline tests
|
// MARK: - Per-airline tests
|
||||||
@@ -104,6 +110,16 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func test_SY_sunCountry() async throws {
|
||||||
|
// Sun Country runs on Navitaire; the load endpoint takes flight
|
||||||
|
// number + route + date. Route-explorer doesn't cover SY well, so
|
||||||
|
// most runs hit the known-daily fallback (SY104 LAS-MSP).
|
||||||
|
try await runAirlineLoadTest(
|
||||||
|
carrier: "SY",
|
||||||
|
hubs: ["MSP", "LAS", "MCO", "DEN"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
@@ -159,12 +175,15 @@ final class AirlineLoadIntegrationTests: XCTestCase {
|
|||||||
// Fallback: known-good daily flight. Triggers when route-explorer
|
// Fallback: known-good daily flight. Triggers when route-explorer
|
||||||
// found nothing OR when the discovered flight returned nil (e.g. a
|
// found nothing OR when the discovered flight returned nil (e.g. a
|
||||||
// regional carrier op that isn't in the upstream load system).
|
// regional carrier op that isn't in the upstream load system).
|
||||||
|
// dayOffset in the table controls today-vs-tomorrow based on each
|
||||||
|
// carrier's snapshot window quirks.
|
||||||
if let known = Self.knownDailyFlights[carrier] {
|
if let known = Self.knownDailyFlights[carrier] {
|
||||||
NSLog("[\(carrier)Test] Using known daily \(carrier)\(known.flightNumber) \(known.origin)→\(known.destination)")
|
let probeDate = Date().addingTimeInterval(TimeInterval(known.dayOffset * 86400))
|
||||||
|
NSLog("[\(carrier)Test] Using known daily \(carrier)\(known.flightNumber) \(known.origin)→\(known.destination) +\(known.dayOffset)d")
|
||||||
let load = await loadService.fetchLoad(
|
let load = await loadService.fetchLoad(
|
||||||
airlineCode: carrier,
|
airlineCode: carrier,
|
||||||
flightNumber: known.flightNumber,
|
flightNumber: known.flightNumber,
|
||||||
date: Date(),
|
date: probeDate,
|
||||||
origin: known.origin,
|
origin: known.origin,
|
||||||
destination: known.destination,
|
destination: known.destination,
|
||||||
departureTime: nil
|
departureTime: nil
|
||||||
|
|||||||
Reference in New Issue
Block a user