From c1a64123c7b23b42f806a24746ec0bcd78c56350 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 27 Jun 2026 16:55:44 +0200 Subject: [PATCH] fix(airtrail): import the airline name, not the ICAO code (#1334) AirTrail returns each airline as {icao, iata, name}, but the import reduced it to the ICAO/IATA code, so an imported flight showed e.g. 'EWG' instead of 'Eurowings'. The picker and the stored reservation now use the airline name (falling back to the code when AirTrail has none). The raw code is kept in metadata.airline_code so the writeback to AirTrail still sends a code, not a name (#1240), and the change-detection snapshot hash stays on the code so existing flights don't spuriously re-sync. --- server/src/services/airtrail/airtrailMapper.ts | 17 +++++++++++++++-- server/src/services/airtrail/airtrailSync.ts | 5 +++-- .../tests/unit/services/airtrailMapper.test.ts | 11 +++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/server/src/services/airtrail/airtrailMapper.ts b/server/src/services/airtrail/airtrailMapper.ts index 40c4fdd6..dd300b06 100644 --- a/server/src/services/airtrail/airtrailMapper.ts +++ b/server/src/services/airtrail/airtrailMapper.ts @@ -15,6 +15,15 @@ export function entityCode(e: AirtrailNamedCode | null | undefined): string | nu return e?.icao || e?.iata || null; } +/** + * Human-readable name for an airline/aircraft (e.g. "Lufthansa"), falling back to the + * code when AirTrail doesn't provide a name. Used for what TREK displays/stores; the + * raw code stays available via entityCode for the writeback payload (#1334). + */ +export function entityName(e: AirtrailNamedCode | null | undefined): string | null { + return e?.name || e?.icao || e?.iata || null; +} + /** * Local calendar date + clock time for an instant at a given IANA zone. * AirTrail stores `departure`/`arrival` as instants (ISO w/ offset) plus a local @@ -57,7 +66,7 @@ export function normalizeFlight(raw: AirtrailFlightRaw): AirtrailFlight { date: raw.date ?? null, departure: raw.departureScheduled ?? null, arrival: raw.arrivalScheduled ?? null, - airline: entityCode(raw.airline), + airline: entityName(raw.airline), flightNumber: raw.flightNumber ?? null, aircraft: entityCode(raw.aircraft), seatClass: (raw.seats?.find(s => s.userId) ?? raw.seats?.[0])?.seatClass ?? null, @@ -142,10 +151,14 @@ export function mapFlightToReservation(raw: AirtrailFlightRaw): MappedReservatio } const seat = raw.seats?.find(s => s.userId) ?? raw.seats?.[0]; + const airlineName = entityName(raw.airline); const airlineCode = entityCode(raw.airline); const aircraftCode = entityCode(raw.aircraft); const metadata: Record = {}; - if (airlineCode) metadata.airline = airlineCode; + // Display the airline name; keep the code in airline_code for the AirTrail writeback, + // which expects a code, not a name (#1334 / #1240). + if (airlineName) metadata.airline = airlineName; + if (airlineCode) metadata.airline_code = airlineCode; if (raw.flightNumber) metadata.flight_number = raw.flightNumber; if (aircraftCode) metadata.aircraft = aircraftCode; if (raw.aircraftReg) metadata.aircraft_reg = raw.aircraftReg; diff --git a/server/src/services/airtrail/airtrailSync.ts b/server/src/services/airtrail/airtrailSync.ts index 9e3d164f..bf76b3a8 100644 --- a/server/src/services/airtrail/airtrailSync.ts +++ b/server/src/services/airtrail/airtrailSync.ts @@ -216,9 +216,10 @@ export function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): arrivalScheduledTime: arr.time, // These are AirTrail-owned details TREK doesn't surface in its edit UI — a TREK // edit can leave them out of `metadata`. Preserve AirTrail's current value when - // TREK has none rather than nulling it out (#1240). entityCode mirrors the + // TREK has none rather than nulling it out (#1240). Use airline_code (not the + // display name in metadata.airline, #1334); both it and entityCode mirror the // import/hash code-selection so a writeback stays a no-op for the hash. - airline: meta.airline ?? entityCode(existing.airline) ?? null, + airline: meta.airline_code ?? entityCode(existing.airline) ?? null, flightNumber: meta.flight_number ?? existing.flightNumber ?? null, aircraft: meta.aircraft ?? entityCode(existing.aircraft) ?? null, aircraftReg: meta.aircraft_reg ?? existing.aircraftReg ?? null, diff --git a/server/tests/unit/services/airtrailMapper.test.ts b/server/tests/unit/services/airtrailMapper.test.ts index d3418ad1..10d9b5c5 100644 --- a/server/tests/unit/services/airtrailMapper.test.ts +++ b/server/tests/unit/services/airtrailMapper.test.ts @@ -47,7 +47,7 @@ describe('airtrailMapper.normalizeFlight', () => { fromCode: 'JFK', toCode: 'LHR', date: '2021-09-01', - airline: 'BAW', + airline: 'British Airways', flightNumber: 'BA178', seatClass: 'economy', }); @@ -98,12 +98,19 @@ describe('airtrailMapper.mapFlightToReservation', () => { it('carries flight metadata', () => { const m = mapFlightToReservation(flight()); - expect(m.metadata).toMatchObject({ airline: 'BAW', flight_number: 'BA178', aircraft: 'B772', aircraft_reg: 'G-VIIL', flight_reason: 'leisure', seat: '12A' }); + // #1334: display the airline name, keep the code in airline_code for the writeback. + expect(m.metadata).toMatchObject({ airline: 'British Airways', airline_code: 'BAW', flight_number: 'BA178', aircraft: 'B772', aircraft_reg: 'G-VIIL', flight_reason: 'leisure', seat: '12A' }); expect(m.type).toBe('flight'); expect(m.status).toBe('confirmed'); expect(m.notes).toBe('window seat'); }); + it('#1334 falls back to the airline code when AirTrail provides no name', () => { + const a = { id: 9, icao: 'EWG', iata: 'EW' }; + expect(normalizeFlight(flight({ airline: a })).airline).toBe('EWG'); + expect(mapFlightToReservation(flight({ airline: a })).metadata).toMatchObject({ airline: 'EWG', airline_code: 'EWG' }); + }); + it('uses only the seat number for the seat, not the cabin class (#1246)', () => { // AirTrail often has a class but no seat number until check-in; the class // must not leak into the seat field.