diff --git a/src/CONST/index.ts b/src/CONST/index.ts index bdb5ef4f3b30..78f8ce51120e 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6994,6 +6994,15 @@ const CONST = { TRAIN: 'train', }, + PNR_STATUS: { + CANCELLED: 'CANCELLED', + VOIDED: 'VOIDED', + }, + + LEG_STATUS: { + CANCELLED: 'CANCELLED_STATUS', + }, + UPCOMING_TRAVEL_WINDOW_DAYS: 7, RESERVATION_ADDRESS_TEST_ID: 'ReservationAddress', diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 9ab36a58437b..ddcd92beae81 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -157,7 +157,12 @@ function getAirReservations(pnr: Pnr, travelers: PnrTraveler[]): Array<{reservat for (const [index, flightDetails] of flightCoupons.sort((a, b) => a.legIdx - b.legIdx).entries()) { const legIdx = flightDetails.legIdx; const flightIdx = flightDetails.flightIdx; - const flightObject = pnrData.legs?.at(legIdx)?.flights.at(flightIdx); + const leg = pnrData.legs?.at(legIdx); + const flightObject = leg?.flights.at(flightIdx); + + if (leg?.legStatus === CONST.LEG_STATUS.CANCELLED || isCancelledPnrStatus(flightObject?.flightStatus ?? '')) { + continue; + } const airlineCode = flightObject?.marketing.airlineCode; const longAirlineName = airlineInfo.find((info) => info.airlineCode === airlineCode)?.airlineName ?? airlineCode; @@ -394,12 +399,41 @@ function getRailReservations(pnr: Pnr, travelers: PnrTraveler[]): Array<{reserva return reservationList; } +function isCancelledPnrStatus(status: string): boolean { + return status === CONST.PNR_STATUS.CANCELLED || status === CONST.PNR_STATUS.VOIDED; +} + +function isPnrCancelled(pnr: Pnr): boolean { + const {data} = pnr; + + if (data.bookingStatus && isCancelledPnrStatus(data.bookingStatus)) { + return true; + } + + if (data.hotelPnr) { + return isCancelledPnrStatus(data.hotelPnr.pnrStatus); + } + if (data.carPnr) { + return isCancelledPnrStatus(data.carPnr.pnrStatus); + } + if (data.airPnr) { + return data.airPnr.legs.length > 0 && data.airPnr.legs.every((leg) => leg.legStatus === CONST.LEG_STATUS.CANCELLED); + } + if (data.railPnr) { + const {outwardJourney, inwardJourney} = data.railPnr; + return isCancelledPnrStatus(outwardJourney.journeyStatus) && isCancelledPnrStatus(inwardJourney.journeyStatus); + } + + return false; +} + function getReservationsFromSpotnanaPayload(reportID: string, tripData?: TripData): ReservationData[] { if (!tripData?.pnrs) { return []; } const reservations: ReservationData[] = tripData.pnrs + .filter((pnr) => !isPnrCancelled(pnr)) .flatMap((pnr) => { const travelers = pnr.data.pnrTravelers ?? []; @@ -517,5 +551,6 @@ export { formatAirportInfo, getPNRReservationDataFromTripReport, getAirReservations, + isPnrCancelled, }; export type {ReservationData}; diff --git a/src/types/onyx/TripData.ts b/src/types/onyx/TripData.ts index 673ee39f0b9e..31f5976548b0 100644 --- a/src/types/onyx/TripData.ts +++ b/src/types/onyx/TripData.ts @@ -231,6 +231,9 @@ type PnrData = { /** Reason for suspending the booking. */ suspendReason: string; + /** Overall booking status of the PNR. */ + bookingStatus?: string; + /** * Fare Amount details for the PNR. */ diff --git a/tests/unit/TripReservationUtilsTest.ts b/tests/unit/TripReservationUtilsTest.ts index 6b7cd6786572..c9c888c4192c 100644 --- a/tests/unit/TripReservationUtilsTest.ts +++ b/tests/unit/TripReservationUtilsTest.ts @@ -1,5 +1,5 @@ /* cspell:disable */ -import {getAirReservations, getPNRReservationDataFromTripReport, getReservationsFromTripReport} from '@libs/TripReservationUtils'; +import {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'; @@ -2266,6 +2266,19 @@ const tripWithAllReservations: TripData = { pnrs: [airPnrDirect, airPnrConnecting, railPnr, carPnr, hotelPnr], }; +function asDefined(value: T | undefined | null): T { + if (value == null) { + throw new Error('Expected value to be defined'); + } + return value; +} + +const directAirPnrData = asDefined(airPnrDirect.data.airPnr); +const connectingAirPnrData = asDefined(airPnrConnecting.data.airPnr); +const hotelPnrData = asDefined(hotelPnr.data.hotelPnr); +const carPnrData = asDefined(carPnr.data.carPnr); +const railPnrData = asDefined(railPnr.data.railPnr); + describe('TripReservationUtils', () => { describe('getAirReservations', () => { it('should return air reservations in the correct order', () => { @@ -2297,6 +2310,141 @@ describe('TripReservationUtils', () => { expect(seatNumber).not.toContain('amount'); expect(seatNumber).not.toContain('legIdx'); }); + + it('should skip flights with cancelled leg status (CANCELLED_STATUS)', () => { + const firstLeg = asDefined(directAirPnrData.legs.at(0)); + const cancelledLegPnr: Pnr = { + ...airPnrDirect, + data: { + ...airPnrDirect.data, + airPnr: { + ...directAirPnrData, + legs: [ + { + ...firstLeg, + legStatus: CONST.LEG_STATUS.CANCELLED, + }, + ], + }, + }, + }; + const result = getAirReservations(cancelledLegPnr, cancelledLegPnr.data.pnrTravelers); + expect(result).toHaveLength(0); + }); + + it('should skip flights with CANCELLED flight status', () => { + const firstLeg = asDefined(directAirPnrData.legs.at(0)); + const firstFlight = asDefined(firstLeg.flights.at(0)); + const cancelledFlightPnr: Pnr = { + ...airPnrDirect, + data: { + ...airPnrDirect.data, + airPnr: { + ...directAirPnrData, + legs: [ + { + ...firstLeg, + flights: [ + { + ...firstFlight, + flightStatus: CONST.PNR_STATUS.CANCELLED, + }, + ], + }, + ], + }, + }, + }; + const result = getAirReservations(cancelledFlightPnr, cancelledFlightPnr.data.pnrTravelers); + expect(result).toHaveLength(0); + }); + + it('should skip flights with VOIDED flight status', () => { + const firstLeg = asDefined(directAirPnrData.legs.at(0)); + const firstFlight = asDefined(firstLeg.flights.at(0)); + const voidedFlightPnr: Pnr = { + ...airPnrDirect, + data: { + ...airPnrDirect.data, + airPnr: { + ...directAirPnrData, + legs: [ + { + ...firstLeg, + flights: [ + { + ...firstFlight, + flightStatus: CONST.PNR_STATUS.VOIDED, + }, + ], + }, + ], + }, + }, + }; + const result = getAirReservations(voidedFlightPnr, voidedFlightPnr.data.pnrTravelers); + expect(result).toHaveLength(0); + }); + + it('should still return non-cancelled flights when only some legs are cancelled', () => { + const firstLeg = asDefined(connectingAirPnrData.legs.at(0)); + const secondLeg = connectingAirPnrData.legs.at(1); + const partiallyCancelledPnr: Pnr = { + ...airPnrConnecting, + data: { + ...airPnrConnecting.data, + airPnr: { + ...connectingAirPnrData, + legs: [ + { + ...firstLeg, + legStatus: CONST.LEG_STATUS.CANCELLED, + }, + ...(secondLeg ? [secondLeg] : []), + ], + }, + }, + }; + const result = getAirReservations(partiallyCancelledPnr, partiallyCancelledPnr.data.pnrTravelers); + expect(result).toHaveLength(1); + expect(result.at(0)?.reservation.start?.shortName).toBe('EWR'); + expect(result.at(0)?.reservation.end?.shortName).toBe('LAX'); + }); + + it('should still return non-cancelled flights when only some flights in a leg have cancelled status', () => { + const firstLeg = asDefined(connectingAirPnrData.legs.at(0)); + const firstFlight = asDefined(firstLeg.flights.at(0)); + const secondFlight = firstLeg.flights.at(1); + const secondLeg = connectingAirPnrData.legs.at(1); + const partialFlightCancelPnr: Pnr = { + ...airPnrConnecting, + data: { + ...airPnrConnecting.data, + airPnr: { + ...connectingAirPnrData, + legs: [ + { + ...firstLeg, + flights: [ + { + ...firstFlight, + flightStatus: CONST.PNR_STATUS.CANCELLED, + }, + ...(secondFlight ? [secondFlight] : []), + ], + }, + ...(secondLeg ? [secondLeg] : []), + ], + }, + }, + }; + const result = getAirReservations(partialFlightCancelPnr, partialFlightCancelPnr.data.pnrTravelers); + expect(result).toHaveLength(2); + expect(result.at(0)?.reservation.start?.shortName).toBe('MSP'); + expect(result.at(0)?.reservation.end?.shortName).toBe('EWR'); + expect(result.at(1)?.reservation.start?.shortName).toBe('EWR'); + expect(result.at(1)?.reservation.end?.shortName).toBe('LAX'); + }); }); describe('getReservationsFromTripReport', () => { it('should return an empty array when there are no transactions and trip payload', () => { @@ -2335,6 +2483,93 @@ describe('TripReservationUtils', () => { expect(resultWithSingleReservation.at(0)?.reservation.reservationID).toEqual('PNR_HOTEL_789'); expect(resultWithSingleReservation.at(0)?.reservation.type).toEqual(CONST.RESERVATION_TYPE.HOTEL); }); + + it('should filter out cancelled PNRs from results', () => { + const cancelledHotelPnr: Pnr = { + ...hotelPnr, + pnrId: 'PNR_HOTEL_CANCELLED', + data: { + ...hotelPnr.data, + hotelPnr: { + ...hotelPnrData, + pnrStatus: CONST.PNR_STATUS.CANCELLED, + }, + }, + }; + + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [airPnrDirect, cancelledHotelPnr, carPnr], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(2); + expect(result.every((r) => r.reservation.reservationID !== 'PNR_HOTEL_CANCELLED')).toBe(true); + expect(result.at(0)?.reservation.reservationID).toEqual('PNR_AIR_789'); + expect(result.at(1)?.reservation.reservationID).toEqual('PNR_CAR_789'); + }); + + it('should filter out PNRs with cancelled bookingStatus', () => { + const cancelledByBookingStatus: Pnr = { + ...carPnr, + pnrId: 'PNR_CAR_CANCELLED', + data: { + ...carPnr.data, + bookingStatus: CONST.PNR_STATUS.VOIDED, + }, + }; + + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [airPnrDirect, cancelledByBookingStatus, hotelPnr], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(2); + expect(result.every((r) => r.reservation.reservationID !== 'PNR_CAR_CANCELLED')).toBe(true); + expect(result.at(0)?.reservation.reservationID).toEqual('PNR_AIR_789'); + expect(result.at(1)?.reservation.reservationID).toEqual('PNR_HOTEL_789'); + }); + + it('should return empty when all PNRs are cancelled', () => { + const cancelledHotelPnr: Pnr = { + ...hotelPnr, + data: { + ...hotelPnr.data, + bookingStatus: CONST.PNR_STATUS.CANCELLED, + }, + }; + const cancelledCarPnr: Pnr = { + ...carPnr, + data: { + ...carPnr.data, + carPnr: { + ...carPnrData, + pnrStatus: CONST.PNR_STATUS.VOIDED, + }, + }, + }; + + const report = createRandomReport(1, undefined); + report.tripData = { + tripID: 'trip123', + payload: { + ...basicTripData, + pnrs: [cancelledHotelPnr, cancelledCarPnr], + }, + }; + + const result = getReservationsFromTripReport(report, []); + expect(result).toHaveLength(0); + }); }); describe('cityName mapping', () => { @@ -2489,4 +2724,171 @@ describe('TripReservationUtils', () => { expect(pnrReservation?.reservations.at(2)?.reservation?.end?.shortName).toEqual('EWR'); }); }); + + describe('isPnrCancelled', () => { + it('should return true for hotel PNR with CANCELLED pnrStatus', () => { + const cancelledHotel: Pnr = { + ...hotelPnr, + data: { + ...hotelPnr.data, + hotelPnr: { + ...hotelPnrData, + pnrStatus: CONST.PNR_STATUS.CANCELLED, + }, + }, + }; + expect(isPnrCancelled(cancelledHotel)).toBe(true); + }); + + it('should return true for hotel PNR with VOIDED pnrStatus', () => { + const voidedHotel: Pnr = { + ...hotelPnr, + data: { + ...hotelPnr.data, + hotelPnr: { + ...hotelPnrData, + pnrStatus: CONST.PNR_STATUS.VOIDED, + }, + }, + }; + expect(isPnrCancelled(voidedHotel)).toBe(true); + }); + + it('should return false for hotel PNR with active status', () => { + const activeHotel: Pnr = { + ...hotelPnr, + data: { + ...hotelPnr.data, + hotelPnr: { + ...hotelPnrData, + pnrStatus: 'CONFIRMED', + }, + }, + }; + expect(isPnrCancelled(activeHotel)).toBe(false); + }); + + it('should return true for car PNR with CANCELLED pnrStatus', () => { + const cancelledCar: Pnr = { + ...carPnr, + data: { + ...carPnr.data, + carPnr: { + ...carPnrData, + pnrStatus: CONST.PNR_STATUS.CANCELLED, + }, + }, + }; + expect(isPnrCancelled(cancelledCar)).toBe(true); + }); + + it('should return true for air PNR where all legs are cancelled', () => { + const cancelledAir: Pnr = { + ...airPnrDirect, + data: { + ...airPnrDirect.data, + airPnr: { + ...directAirPnrData, + legs: directAirPnrData.legs.map((leg) => ({ + ...leg, + legStatus: CONST.LEG_STATUS.CANCELLED, + })), + }, + }, + }; + expect(isPnrCancelled(cancelledAir)).toBe(true); + }); + + it('should return false for air PNR where only some legs are cancelled', () => { + const partiallyCancelledAir: Pnr = { + ...airPnrConnecting, + data: { + ...airPnrConnecting.data, + airPnr: { + ...connectingAirPnrData, + legs: connectingAirPnrData.legs.map((leg, index) => ({ + ...leg, + legStatus: index === 0 ? CONST.LEG_STATUS.CANCELLED : 'CONFIRMED_STATUS', + })), + }, + }, + }; + expect(isPnrCancelled(partiallyCancelledAir)).toBe(false); + }); + + it('should return true for rail PNR with both journeys cancelled', () => { + const cancelledRail: Pnr = { + ...railPnr, + data: { + ...railPnr.data, + railPnr: { + ...railPnrData, + outwardJourney: { + ...railPnrData.outwardJourney, + journeyStatus: CONST.PNR_STATUS.CANCELLED, + }, + inwardJourney: { + ...railPnrData.inwardJourney, + journeyStatus: CONST.PNR_STATUS.CANCELLED, + }, + }, + }, + }; + expect(isPnrCancelled(cancelledRail)).toBe(true); + }); + + it('should return false for rail PNR with only one journey cancelled', () => { + const partialRail: Pnr = { + ...railPnr, + data: { + ...railPnr.data, + railPnr: { + ...railPnrData, + outwardJourney: { + ...railPnrData.outwardJourney, + journeyStatus: CONST.PNR_STATUS.CANCELLED, + }, + inwardJourney: { + ...railPnrData.inwardJourney, + journeyStatus: 'CONFIRMED', + }, + }, + }, + }; + expect(isPnrCancelled(partialRail)).toBe(false); + }); + + it('should return true when top-level bookingStatus is CANCELLED', () => { + const cancelledByBookingStatus: Pnr = { + ...hotelPnr, + data: { + ...hotelPnr.data, + bookingStatus: CONST.PNR_STATUS.CANCELLED, + hotelPnr: { + ...hotelPnrData, + pnrStatus: 'CONFIRMED', + }, + }, + }; + expect(isPnrCancelled(cancelledByBookingStatus)).toBe(true); + }); + + it('should return true when top-level bookingStatus is VOIDED', () => { + const voidedByBookingStatus: Pnr = { + ...carPnr, + data: { + ...carPnr.data, + bookingStatus: CONST.PNR_STATUS.VOIDED, + }, + }; + expect(isPnrCancelled(voidedByBookingStatus)).toBe(true); + }); + + it('should return false for PNR with no cancellation indicators', () => { + expect(isPnrCancelled(hotelPnr)).toBe(false); + expect(isPnrCancelled(carPnr)).toBe(false); + expect(isPnrCancelled(airPnrDirect)).toBe(false); + expect(isPnrCancelled(railPnr)).toBe(false); + }); + }); });