diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 9216928033ba..235a37d7f52d 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -22,7 +22,7 @@ import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {ReservationData} from '@src/libs/TripReservationUtils'; -import {formatAirportInfo, formatCancelledDescription, getPNRReservationDataFromTripReport, getTripReservationCode, getTripReservationIcon} from '@src/libs/TripReservationUtils'; +import {formatCancelledDescription, formatTransitLocationLabel, getPNRReservationDataFromTripReport, getTripReservationCode, getTripReservationIcon} from '@src/libs/TripReservationUtils'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {Reservation} from '@src/types/onyx/Transaction'; @@ -102,18 +102,18 @@ function ReservationView({reservation, transactionID, tripRoomReportID, sequence {shouldShowArrowIcon ? ( <> - {formatAirportInfo(reservation.start)} + {formatTransitLocationLabel(reservation.start)} - {formatAirportInfo(reservation.end)} + {formatTransitLocationLabel(reservation.end)} ) : ( - {formatAirportInfo(reservation.start)} {translate('common.to').toLowerCase()} {formatAirportInfo(reservation.end)} + {formatTransitLocationLabel(reservation.start)} {translate('common.to').toLowerCase()} {formatTransitLocationLabel(reservation.end)} )} @@ -217,7 +217,7 @@ function TripDetailsView({tripRoomReport, shouldShowHorizontalRule, tripTransact if (!destinationReservation) { return ''; } - return `${translate('travel.flightTo')} ${formatAirportInfo(destinationReservation.reservation.end, true)}`; + return `${translate('travel.flightTo')} ${formatTransitLocationLabel(destinationReservation.reservation.end, true)}`; } case CONST.RESERVATION_TYPE.TRAIN: if (reservations.length === 2 && firstReservation.start.shortName === lastReservation.end.shortName) { diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index e4badc79e01e..5c18405ed428 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -336,6 +336,9 @@ function getCarReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationItem return reservationList; } +// Drop Trainline URN-style codes so the trip UI doesn't render them as station labels. +const getRailStationShortName = (code: string | undefined) => (code && !code.startsWith('urn:') ? code : ''); + function getRailReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationItem[] { const reservationList: ReservationItem[] = []; @@ -362,13 +365,13 @@ function getRailReservations(pnr: Pnr, travelers: PnrTraveler[]): ReservationIte start: { date: leg.departAt.iso8601, longName: leg.originInfo.name, - shortName: leg.originInfo.code, + shortName: getRailStationShortName(leg.originInfo.code), cityName: leg.originInfo.cityName, }, end: { date: leg.arriveAt.iso8601, longName: leg.destinationInfo.name, - shortName: leg.destinationInfo.code, + shortName: getRailStationShortName(leg.destinationInfo.code), cityName: leg.destinationInfo.cityName, }, route: { @@ -471,13 +474,13 @@ function getReservationsFromTripReport(tripReport?: Report, transactions?: Trans return []; } -function formatAirportInfo(reservationTimeDetails: ReservationTimeDetails, hideAirportCode = false): string { - const longName = reservationTimeDetails?.longName ? `${reservationTimeDetails?.longName} ` : ''; - let shortName = reservationTimeDetails?.shortName ? `${reservationTimeDetails?.shortName}` : ''; - - shortName = longName && shortName ? `(${shortName})` : shortName; - - return !hideAirportCode ? `${longName}${shortName}` : longName; +function formatTransitLocationLabel(reservationTimeDetails: ReservationTimeDetails, hideShortCode = false): string { + const longName = reservationTimeDetails?.longName ?? ''; + const shortName = reservationTimeDetails?.shortName ?? ''; + if (hideShortCode || !shortName) { + return longName; + } + return longName ? `${longName} (${shortName})` : `(${shortName})`; } function getPNRReservationDataFromTripReport(tripReport?: Report, transactions?: Transaction[]): ReservationPNRData[] { @@ -560,7 +563,7 @@ export { getReservationsFromTripReport, getTripTotal, getReservationDetailsFromSequence, - formatAirportInfo, + formatTransitLocationLabel, getPNRReservationDataFromTripReport, getAirReservations, isPnrCancelled, diff --git a/src/pages/Travel/TrainTripDetails.tsx b/src/pages/Travel/TrainTripDetails.tsx index aae615a0c757..2c1aaeb0b27a 100644 --- a/src/pages/Travel/TrainTripDetails.tsx +++ b/src/pages/Travel/TrainTripDetails.tsx @@ -7,6 +7,7 @@ import UserPills from '@components/UserPills'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import {formatTransitLocationLabel} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; import type {Reservation} from '@src/types/onyx/Transaction'; @@ -22,9 +23,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) const startDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date)); const endDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date)); - const trainRouteDescription = `${reservation.start.longName} (${reservation.start.shortName}) ${translate('common.conjunctionTo')} ${reservation.end.longName} (${ - reservation.end.shortName - })`; + const trainRouteDescription = `${formatTransitLocationLabel(reservation.start)} ${translate('common.conjunctionTo')} ${formatTransitLocationLabel(reservation.end)}`; const trainDuration = DateUtils.getFormattedDurationBetweenDates(translate, new Date(reservation.start.date), new Date(reservation.end.date)); const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name; @@ -50,7 +49,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) description={translate('travel.trainDetails.departs')} descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]} titleComponent={{startDate.hour}} - helperText={`${reservation.start.longName} (${reservation.start.shortName})`} + helperText={formatTransitLocationLabel(reservation.start)} helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> @@ -58,7 +57,7 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps) description={translate('travel.trainDetails.arrives')} descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]} titleComponent={{endDate.hour}} - helperText={`${reservation.end.longName} (${reservation.end.shortName})`} + helperText={formatTransitLocationLabel(reservation.end)} helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> diff --git a/tests/unit/TripReservationUtilsTest.ts b/tests/unit/TripReservationUtilsTest.ts index 4addabcc12a8..5af0a2f5b272 100644 --- a/tests/unit/TripReservationUtilsTest.ts +++ b/tests/unit/TripReservationUtilsTest.ts @@ -1,5 +1,5 @@ /* cspell:disable */ -import {getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport, isPnrCancelled} from '@libs/TripReservationUtils'; +import {formatTransitLocationLabel, getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport, isPnrCancelled} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import type {Pnr, TripData} from '@src/types/onyx/TripData'; import {airReservationPnrData, airReservationTravelers} from '../data/TripAirReservationData'; @@ -2692,6 +2692,61 @@ describe('TripReservationUtils', () => { }); }); + describe('rail shortName sanitization', () => { + it('should drop a URN-formatted code from rail shortName', () => { + const railPnrWithUrnCodes = JSON.parse(JSON.stringify(railPnr)) as typeof railPnr; + const leg = railPnrWithUrnCodes.data.railPnr?.legInfos.at(0); + if (leg) { + leg.originInfo.code = 'urn:trainline:public:nloc:at000408'; + leg.destinationInfo.code = 'urn:trainline:public:nloc:at001685'; + } + + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [railPnrWithUrnCodes], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(1); + + const trainReservation = result.at(0)?.reservation; + expect(trainReservation?.type).toEqual(CONST.RESERVATION_TYPE.TRAIN); + expect(trainReservation?.start?.shortName).toEqual(''); + expect(trainReservation?.end?.shortName).toEqual(''); + }); + + it('should render only longName when shortName is empty (no trailing space, no empty parens)', () => { + expect(formatTransitLocationLabel({date: '', longName: 'Brockenhurst', shortName: '', cityName: ''})).toEqual('Brockenhurst'); + }); + + it('should render longName with parenthesized shortName when both are present', () => { + expect(formatTransitLocationLabel({date: '', longName: 'Solana Beach station', shortName: 'SOL', cityName: ''})).toEqual('Solana Beach station (SOL)'); + }); + + it('should preserve a clean station code in rail shortName', () => { + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [railPnr], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(1); + + const trainReservation = result.at(0)?.reservation; + expect(trainReservation?.type).toEqual(CONST.RESERVATION_TYPE.TRAIN); + expect(trainReservation?.start?.shortName).toEqual('STX'); + expect(trainReservation?.end?.shortName).toEqual('STY'); + }); + }); + describe('getPNRReservationDataFromTripReport', () => { it('should return an empty array when there are no transactions and trip payload', () => { const report = createRandomReport(1, undefined);