Skip to content

Commit 7fcf93c

Browse files
MelvinBotOllyws
andcommitted
Keep pending-delete transactions offline in report action calculation
Offline, a queued-for-delete expense still belongs to the report until the delete syncs. Keep it in the transaction list used for the held-state check so the report is not treated as all-held and Remove hold is not offered as the primary action. Co-authored-by: Olly <Ollyws@users.noreply.github.com>
1 parent def3423 commit 7fcf93c

3 files changed

Lines changed: 91 additions & 2 deletions

File tree

src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo
150150
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
151151

152152
const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
153-
const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
153+
// While offline, keep pending-delete transactions so a queued-for-delete expense still counts as a real transaction until the delete syncs.
154+
const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => isOffline || t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
154155
const allTransactions = Object.values(reportTransactions);
155156
const singleTransaction = nonPendingDeleteTransactions.length === 1 ? nonPendingDeleteTransactions.at(0) : undefined;
156157
const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(singleTransaction?.comment?.originalTransactionID)}`);

src/hooks/useReportPrimaryAction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ function useReportPrimaryAction(reportID: string | undefined): ValueOf<typeof CO
5151
return CONST.REPORT.PRIMARY_ACTIONS.SUBMIT;
5252
}
5353

54-
const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => !isTransactionPendingDelete(t));
54+
// While offline, keep pending-delete transactions so a queued-for-delete expense still counts as a real transaction until the delete syncs.
55+
const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => isOffline || !isTransactionPendingDelete(t));
5556

5657
return getReportPrimaryAction({
5758
currentUserLogin: currentUserLogin ?? '',
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {renderHook} from '@testing-library/react-native';
2+
3+
import useReportPrimaryAction from '@hooks/useReportPrimaryAction';
4+
5+
import {getReportPrimaryAction} from '@libs/ReportPrimaryActionUtils';
6+
7+
import CONST from '@src/CONST';
8+
import type {Transaction} from '@src/types/onyx';
9+
10+
import createMock from '../../utils/createMock';
11+
12+
const mockGetReportPrimaryAction = jest.mocked(getReportPrimaryAction);
13+
14+
const REPORT_ID = 'report1';
15+
const POLICY_ID = 'policy1';
16+
17+
// Prefixed with `mock` so they can be referenced inside the hoisted jest.mock factory below.
18+
const mockHeldTransaction = createMock<Transaction>({transactionID: 'held', reportID: REPORT_ID, comment: {hold: 'holdID'}});
19+
const mockPendingDeleteTransaction = createMock<Transaction>({transactionID: 'unheld', reportID: REPORT_ID, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE});
20+
21+
let mockIsOffline = false;
22+
jest.mock('@hooks/useNetwork', () => ({
23+
__esModule: true,
24+
default: () => ({isOffline: mockIsOffline}),
25+
}));
26+
27+
jest.mock('@libs/ReportPrimaryActionUtils', () => ({
28+
getReportPrimaryAction: jest.fn(() => ''),
29+
}));
30+
31+
jest.mock('@hooks/useTransactionsAndViolationsForReport', () => ({
32+
__esModule: true,
33+
default: () => ({transactions: {held: mockHeldTransaction, unheld: mockPendingDeleteTransaction}, violations: {}}),
34+
}));
35+
36+
jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({
37+
__esModule: true,
38+
default: () => ({login: 'test@example.com', accountID: 1}),
39+
}));
40+
41+
jest.mock('@hooks/useReportIsArchived', () => ({
42+
__esModule: true,
43+
default: () => false,
44+
}));
45+
46+
jest.mock('@components/PaymentAnimationsContext', () => ({
47+
usePaymentAnimationsContext: () => ({isPaidAnimationRunning: false, isApprovedAnimationRunning: false, isSubmittingAnimationRunning: false}),
48+
}));
49+
50+
jest.mock('@components/MoneyReportTransactionThreadContext', () => ({
51+
useMoneyReportTransactionThread: () => ({reportActions: []}),
52+
}));
53+
54+
// Return a minimal IOU report for the money request report key so isExpenseReport is false (skips the early return); undefined for everything else.
55+
jest.mock('@hooks/useOnyx', () => ({
56+
__esModule: true,
57+
default: (key: string) => {
58+
if (key === `report_${REPORT_ID}`) {
59+
return [{reportID: REPORT_ID, type: 'iou', policyID: POLICY_ID, ownerAccountID: 1}];
60+
}
61+
return [undefined];
62+
},
63+
}));
64+
65+
describe('useReportPrimaryAction - offline pending-delete transactions', () => {
66+
beforeEach(() => {
67+
jest.clearAllMocks();
68+
mockIsOffline = false;
69+
});
70+
71+
it('drops pending-delete transactions from the held-state calculation while online', () => {
72+
renderHook(() => useReportPrimaryAction(REPORT_ID));
73+
74+
const passedTransactions = mockGetReportPrimaryAction.mock.calls.at(0)?.at(0)?.reportTransactions;
75+
expect(passedTransactions).toHaveLength(1);
76+
expect(passedTransactions?.at(0)?.transactionID).toBe('held');
77+
});
78+
79+
it('keeps pending-delete transactions while offline so the report is not treated as all-held', () => {
80+
mockIsOffline = true;
81+
renderHook(() => useReportPrimaryAction(REPORT_ID));
82+
83+
const passedTransactions = mockGetReportPrimaryAction.mock.calls.at(0)?.at(0)?.reportTransactions;
84+
expect(passedTransactions).toHaveLength(2);
85+
expect(passedTransactions?.map((transaction) => transaction.transactionID)).toEqual(expect.arrayContaining(['held', 'unheld']));
86+
});
87+
});

0 commit comments

Comments
 (0)