diff --git a/src/pages/home/UpcomingTravelSection/useTripRoomReports.ts b/src/pages/home/UpcomingTravelSection/useTripRoomReports.ts new file mode 100644 index 000000000000..6ef09691680d --- /dev/null +++ b/src/pages/home/UpcomingTravelSection/useTripRoomReports.ts @@ -0,0 +1,12 @@ +import useOnyx from '@hooks/useOnyx'; +import {isTripRoom} from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +function useTripRoomReports(): Report[] { + const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + + return Object.values(reports ?? {}).filter((report): report is Report => !!report && isTripRoom(report)); +} + +export default useTripRoomReports; diff --git a/src/pages/home/UpcomingTravelSection/useUpcomingTravelReservations.ts b/src/pages/home/UpcomingTravelSection/useUpcomingTravelReservations.ts index abd63d0153ad..1b86f2c76528 100644 --- a/src/pages/home/UpcomingTravelSection/useUpcomingTravelReservations.ts +++ b/src/pages/home/UpcomingTravelSection/useUpcomingTravelReservations.ts @@ -1,30 +1,18 @@ import {accountIDSelector} from '@selectors/Session'; import {useMemo} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; -import {isTripRoom} from '@libs/ReportUtils'; import type {ReservationData} from '@libs/TripReservationUtils'; import {getReservationsFromTripReport} from '@libs/TripReservationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; -import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; +import useTripRoomReports from './useTripRoomReports'; type UpcomingReservation = ReservationData & { reportID: string; }; -const tripRoomSelector = (report: OnyxEntry): Report | undefined => { - if (!report || !isTripRoom(report)) { - return; - } - return report; -}; - -const allTripRoomsSelector = (reports: OnyxCollection) => mapOnyxCollectionItems(reports, tripRoomSelector); - function useUpcomingTravelReservations(): UpcomingReservation[] { - const [tripRoomReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: allTripRoomsSelector}); + const tripRoomReports = useTripRoomReports(); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); return useMemo(() => { @@ -32,12 +20,11 @@ function useUpcomingTravelReservations(): UpcomingReservation[] { const windowEnd = new Date(now); windowEnd.setDate(windowEnd.getDate() + CONST.UPCOMING_TRAVEL_WINDOW_DAYS); - const reports = Object.values(tripRoomReports ?? {}); const upcoming: UpcomingReservation[] = []; - for (const report of reports) { + for (const report of tripRoomReports) { // Only include reservations where the current user is the traveler - if (!report || report.ownerAccountID !== accountID) { + if (report.ownerAccountID !== accountID) { continue; } const reservations = getReservationsFromTripReport(report); diff --git a/tests/unit/hooks/useUpcomingTravelReservations.test.ts b/tests/unit/hooks/useUpcomingTravelReservations.test.ts index 855572fd4ca9..eb8d0801575c 100644 --- a/tests/unit/hooks/useUpcomingTravelReservations.test.ts +++ b/tests/unit/hooks/useUpcomingTravelReservations.test.ts @@ -656,4 +656,107 @@ describe('useUpcomingTravelReservations', () => { expect(result.current).toEqual([]); }); }); + + it('should skip reservations with invalid start date and keep valid ones', async () => { + const invalidFlight = makeAirPnr('PNR_INVALID', 'not-a-date', 'not-a-date'); + const validFlight = makeAirPnr('PNR_VALID', daysFromNow(2), daysFromNow(2, 15)); + const tripRoom = makeTripRoomReport('1000', [invalidFlight, validFlight]); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1000`, tripRoom); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useUpcomingTravelReservations()); + + await waitFor(() => { + expect(result.current).toHaveLength(1); + }); + expect(result.current.at(0)?.reservation.reservationID).toBe('PNR_VALID'); + }); + + it('should return empty array when all reservations have invalid start dates', async () => { + const invalidFlight = makeAirPnr('PNR_INVALID_ALL', '', ''); + const tripRoom = makeTripRoomReport('1001', [invalidFlight]); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1001`, tripRoom); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useUpcomingTravelReservations()); + + await waitFor(() => { + expect(result.current).toEqual([]); + }); + }); + + it('should include reservation at the exact 7-day boundary', async () => { + const boundaryFlight = makeAirPnr('PNR_BOUNDARY', daysFromNow(CONST.UPCOMING_TRAVEL_WINDOW_DAYS, 0), daysFromNow(CONST.UPCOMING_TRAVEL_WINDOW_DAYS, 3)); + const tripRoom = makeTripRoomReport('1002', [boundaryFlight]); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1002`, tripRoom); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useUpcomingTravelReservations()); + + await waitFor(() => { + expect(result.current).toHaveLength(1); + }); + expect(result.current.at(0)?.reservation.reservationID).toBe('PNR_BOUNDARY'); + }); + + it('should return empty array for trip room without tripData', async () => { + const tripRoom = { + reportID: '1003', + ownerAccountID: TEST_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.TRIP_ROOM, + reportName: 'Trip 1003', + policyID: 'policy1', + } as Report; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1003`, tripRoom); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useUpcomingTravelReservations()); + + await waitFor(() => { + expect(result.current).toEqual([]); + }); + }); + + it('should return empty array for trip room with empty pnrs array', async () => { + const tripRoom = makeTripRoomReport('1004', []); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1004`, tripRoom); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useUpcomingTravelReservations()); + + await waitFor(() => { + expect(result.current).toEqual([]); + }); + }); + + it('should ignore non-trip-room reports', async () => { + const flight = makeAirPnr('PNR_NON_TRIP', daysFromNow(2), daysFromNow(2, 15)); + const nonTripReport = { + reportID: '1005', + ownerAccountID: TEST_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + reportName: 'Policy Room', + policyID: 'policy1', + tripData: { + tripID: 'trip-1005', + payload: {pnrs: [flight]}, + }, + } as Report; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1005`, nonTripReport); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useUpcomingTravelReservations()); + + await waitFor(() => { + expect(result.current).toEqual([]); + }); + }); });