Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/components/ReportActionItem/TripDetailsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -102,18 +102,18 @@ function ReservationView({reservation, transactionID, tripRoomReportID, sequence
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}>
{shouldShowArrowIcon ? (
<>
<Text style={[titleTextStyle, styles.lh20, shouldUseNarrowLayout && styles.flex1]}>{formatAirportInfo(reservation.start)}</Text>
<Text style={[titleTextStyle, styles.lh20, shouldUseNarrowLayout && styles.flex1]}>{formatTransitLocationLabel(reservation.start)}</Text>
<Icon
src={expensifyIcons.ArrowRightLong}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
fill={theme.icon}
/>
<Text style={[titleTextStyle, styles.lh20, shouldUseNarrowLayout && styles.flex1]}>{formatAirportInfo(reservation.end)}</Text>
<Text style={[titleTextStyle, styles.lh20, shouldUseNarrowLayout && styles.flex1]}>{formatTransitLocationLabel(reservation.end)}</Text>
</>
) : (
<Text style={[titleTextStyle, styles.lh20, shouldUseNarrowLayout && styles.flex1]}>
{formatAirportInfo(reservation.start)} {translate('common.to').toLowerCase()} {formatAirportInfo(reservation.end)}
{formatTransitLocationLabel(reservation.start)} {translate('common.to').toLowerCase()} {formatTransitLocationLabel(reservation.end)}
</Text>
)}
</View>
Expand Down Expand Up @@ -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) {
Expand Down
23 changes: 13 additions & 10 deletions src/libs/TripReservationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];

Expand All @@ -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),
Comment thread
blimpich marked this conversation as resolved.
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: {
Expand Down Expand Up @@ -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[] {
Expand Down Expand Up @@ -560,7 +563,7 @@ export {
getReservationsFromTripReport,
getTripTotal,
getReservationDetailsFromSequence,
formatAirportInfo,
formatTransitLocationLabel,
getPNRReservationDataFromTripReport,
getAirReservations,
isPnrCancelled,
Expand Down
9 changes: 4 additions & 5 deletions src/pages/Travel/TrainTripDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -50,15 +49,15 @@ function TrainTripDetails({reservation, personalDetails}: TrainTripDetailsProps)
description={translate('travel.trainDetails.departs')}
descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]}
titleComponent={<Text style={[styles.textLarge, styles.textHeadlineH2]}>{startDate.hour}</Text>}
helperText={`${reservation.start.longName} (${reservation.start.shortName})`}
helperText={formatTransitLocationLabel(reservation.start)}
helperTextStyle={[styles.pb3, styles.mtn2]}
interactive={false}
/>
<MenuItemWithTopDescription
description={translate('travel.trainDetails.arrives')}
descriptionTextStyle={[styles.textLabelSupporting, styles.mb1]}
titleComponent={<Text style={[styles.textLarge, styles.textHeadlineH2]}>{endDate.hour}</Text>}
helperText={`${reservation.end.longName} (${reservation.end.shortName})`}
helperText={formatTransitLocationLabel(reservation.end)}
helperTextStyle={[styles.pb3, styles.mtn2]}
interactive={false}
/>
Expand Down
57 changes: 56 additions & 1 deletion tests/unit/TripReservationUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Loading