diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 26ab90d84dc61..85fa9909c223c 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -7167,8 +7167,11 @@ function getIOUReportActionWithBadge( return false; } const iouReport = getReportOrDraftReport(action.childReportID); - // Only show to the actual payer, exclude admins with bank account access - if (canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy)) { + // Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses + if ( + canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || + canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, true, undefined, invoiceReceiverPolicy) + ) { actionBadge = CONST.REPORT.ACTION_BADGE.PAY; return true; } diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index f1dd7dc36b294..fb524f429b8fc 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -13785,6 +13785,140 @@ describe('actions/IOU', () => { expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.SUBMIT); }); + it('should return PAY badge for negative expense (credit) via onlyShowPayElsewhere path', async () => { + const chatReportID = '500'; + const iouReportID = '501'; + const policyID = '502'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + // For a negative/credit expense, total is positive on expense reports + // getMoneyRequestSpendBreakdown flips sign: reimbursableSpend = -total = -5000 (negative) + // The first canIOUBePaid call (onlyShowPayElsewhere=false) returns false because reimbursableSpend < 0 + // The second canIOUBePaid call (onlyShowPayElsewhere=true) returns true via canShowMarkedAsPaidForNegativeAmount + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: RORY_ACCOUNT_ID, + total: 5000, + nonReimbursableTotal: 0, + isWaitingOnBankAccount: false, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, fakePolicy, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.PAY); + }); + + it('should return undefined actionBadge for REIMBURSEMENT_NO policy with non-SUBMITTED status', async () => { + const chatReportID = '600'; + const iouReportID = '601'; + const policyID = '602'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + // REIMBURSEMENT_NO + APPROVED (not SUBMITTED): + // First canIOUBePaid call (onlyShowPayElsewhere=false) returns false (early-exit for REIMBURSEMENT_NO) + // Second canIOUBePaid call (onlyShowPayElsewhere=true) also returns false (statusNum !== SUBMITTED) + // So no PAY badge is shown + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: RORY_ACCOUNT_ID, + total: -5000, + nonReimbursableTotal: 0, + isWaitingOnBankAccount: false, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, fakePolicy, {}, undefined); + expect(result.reportAction).toBeUndefined(); + expect(result.actionBadge).toBeUndefined(); + }); + it('should return undefined actionBadge when report is settled', async () => { const chatReportID = '400'; const iouReportID = '401';