Skip to content

Commit 803255b

Browse files
authored
Merge pull request Expensify#89219 from Expensify/claude-skipPayBadgeForNonReimbursableReports
Skip PAY badge in LHN for non-reimbursable-only reports
2 parents 268da13 + 5459278 commit 803255b

2 files changed

Lines changed: 188 additions & 4 deletions

File tree

src/libs/actions/IOU/ReportWorkflow.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,17 @@ function getBadgeFromIOUReport(
272272
currentUserAccountID: number,
273273
): ValueOf<typeof CONST.REPORT.ACTION_BADGE> | undefined {
274274
// Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses
275-
if (
276-
canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, undefined, undefined, invoiceReceiverPolicy) ||
277-
canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, true, undefined, invoiceReceiverPolicy)
278-
) {
275+
const canBePaidNow = canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, undefined, undefined, invoiceReceiverPolicy);
276+
if (canBePaidNow) {
279277
return CONST.REPORT.ACTION_BADGE.PAY;
280278
}
279+
// Pay-elsewhere path: covers negative reimbursable spend (mark-as-paid flow for credits).
280+
// Skip the PAY badge when every expense is non-reimbursable — paying is optional and
281+
// should not pin the report in the LHN.
282+
const canBePaidElsewhere = canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, true, undefined, invoiceReceiverPolicy);
283+
if (canBePaidElsewhere) {
284+
return hasOnlyNonReimbursableTransactions(iouReport?.reportID) ? undefined : CONST.REPORT.ACTION_BADGE.PAY;
285+
}
281286
if (canApproveIOU(iouReport, policy, reportMetadata, currentUserAccountID)) {
282287
return CONST.REPORT.ACTION_BADGE.APPROVE;
283288
}

tests/actions/IOUTest/ReportWorkflowTest.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3415,6 +3415,7 @@ describe('actions/IOU/ReportWorkflow', () => {
34153415
amount: 100,
34163416
status: CONST.TRANSACTION.STATUS.POSTED,
34173417
bank: '',
3418+
reimbursable: true,
34183419
};
34193420

34203421
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
@@ -3754,6 +3755,184 @@ describe('actions/IOU/ReportWorkflow', () => {
37543755
expect(result).toBe(CONST.REPORT.ACTION_BADGE.PAY);
37553756
});
37563757

3758+
it('should skip PAY badge for non-reimbursable-only report via pay-elsewhere path', async () => {
3759+
const iouReportID = '1400';
3760+
const chatReportID = '1401';
3761+
const policyID = '1402';
3762+
3763+
const fakePolicy: Policy = {
3764+
...createRandomPolicy(Number(policyID)),
3765+
id: policyID,
3766+
type: CONST.POLICY.TYPE.TEAM,
3767+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
3768+
role: CONST.POLICY.ROLE.ADMIN,
3769+
reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL,
3770+
};
3771+
3772+
const fakeChatReport: Report = {
3773+
...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT),
3774+
reportID: chatReportID,
3775+
policyID,
3776+
};
3777+
3778+
// Non-reimbursable-only report: total and nonReimbursableTotal are equal.
3779+
// canBePaidElsewhere returns true (admin payer + isOnlyNonReimbursablePayElsewhere).
3780+
// But since all transactions are non-reimbursable, PAY badge should be skipped.
3781+
// Use a different managerID so canApproveIOU also returns false (current user is not the manager).
3782+
const fakeIouReport: Report = {
3783+
...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT),
3784+
reportID: iouReportID,
3785+
type: CONST.REPORT.TYPE.EXPENSE,
3786+
policyID,
3787+
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
3788+
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
3789+
managerID: 999999,
3790+
total: -5000,
3791+
nonReimbursableTotal: -5000,
3792+
isWaitingOnBankAccount: false,
3793+
};
3794+
3795+
const fakeTransaction: Transaction = {
3796+
...createRandomTransaction(0),
3797+
reportID: iouReportID,
3798+
amount: 5000,
3799+
status: CONST.TRANSACTION.STATUS.POSTED,
3800+
bank: '',
3801+
reimbursable: false,
3802+
};
3803+
3804+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
3805+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport);
3806+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport);
3807+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction);
3808+
await waitForBatchedUpdates();
3809+
3810+
const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID);
3811+
expect(result).toBeUndefined();
3812+
});
3813+
3814+
it('should return PAY badge for mixed reimbursable and non-reimbursable report', async () => {
3815+
const iouReportID = '1500';
3816+
const chatReportID = '1501';
3817+
const policyID = '1502';
3818+
3819+
const fakePolicy: Policy = {
3820+
...createRandomPolicy(Number(policyID)),
3821+
id: policyID,
3822+
type: CONST.POLICY.TYPE.TEAM,
3823+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
3824+
role: CONST.POLICY.ROLE.ADMIN,
3825+
reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL,
3826+
};
3827+
3828+
const fakeChatReport: Report = {
3829+
...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT),
3830+
reportID: chatReportID,
3831+
policyID,
3832+
};
3833+
3834+
// Mixed report: has both reimbursable and non-reimbursable spend
3835+
// PAY badge should still be shown because not all transactions are non-reimbursable
3836+
const fakeIouReport: Report = {
3837+
...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT),
3838+
reportID: iouReportID,
3839+
type: CONST.REPORT.TYPE.EXPENSE,
3840+
policyID,
3841+
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
3842+
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
3843+
managerID: RORY_ACCOUNT_ID,
3844+
total: -8000,
3845+
nonReimbursableTotal: -3000,
3846+
isWaitingOnBankAccount: false,
3847+
};
3848+
3849+
const fakeReimbursableTransaction: Transaction = {
3850+
...createRandomTransaction(0),
3851+
reportID: iouReportID,
3852+
amount: 5000,
3853+
status: CONST.TRANSACTION.STATUS.POSTED,
3854+
bank: '',
3855+
reimbursable: true,
3856+
};
3857+
3858+
const fakeNonReimbursableTransaction: Transaction = {
3859+
...createRandomTransaction(1),
3860+
reportID: iouReportID,
3861+
amount: 3000,
3862+
status: CONST.TRANSACTION.STATUS.POSTED,
3863+
bank: '',
3864+
reimbursable: false,
3865+
};
3866+
3867+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
3868+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport);
3869+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport);
3870+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeReimbursableTransaction.transactionID}`, fakeReimbursableTransaction);
3871+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeNonReimbursableTransaction.transactionID}`, fakeNonReimbursableTransaction);
3872+
await waitForBatchedUpdates();
3873+
3874+
const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID);
3875+
expect(result).toBe(CONST.REPORT.ACTION_BADGE.PAY);
3876+
});
3877+
3878+
it('should return PAY badge for negative reimbursable expense via pay-elsewhere path', async () => {
3879+
const iouReportID = '1600';
3880+
const chatReportID = '1601';
3881+
const policyID = '1602';
3882+
3883+
const fakePolicy: Policy = {
3884+
...createRandomPolicy(Number(policyID)),
3885+
id: policyID,
3886+
type: CONST.POLICY.TYPE.TEAM,
3887+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
3888+
role: CONST.POLICY.ROLE.ADMIN,
3889+
reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL,
3890+
};
3891+
3892+
const fakeChatReport: Report = {
3893+
...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT),
3894+
reportID: chatReportID,
3895+
policyID,
3896+
};
3897+
3898+
// Negative reimbursable expense (credit): total is positive on expense reports,
3899+
// which flips to negative reimbursableSpend in getMoneyRequestSpendBreakdown.
3900+
// canBePaidNow returns false (negative reimbursableSpend), but canBePaidElsewhere
3901+
// returns true via canShowMarkedAsPaidForNegativeAmount.
3902+
// Since the transaction IS reimbursable, hasOnlyNonReimbursableTransactions returns
3903+
// false, so PAY badge should be shown.
3904+
const fakeIouReport: Report = {
3905+
...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT),
3906+
reportID: iouReportID,
3907+
type: CONST.REPORT.TYPE.EXPENSE,
3908+
policyID,
3909+
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
3910+
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
3911+
managerID: 999999,
3912+
total: 5000,
3913+
nonReimbursableTotal: 0,
3914+
isWaitingOnBankAccount: false,
3915+
};
3916+
3917+
const fakeTransaction: Transaction = {
3918+
...createRandomTransaction(0),
3919+
reportID: iouReportID,
3920+
amount: 5000,
3921+
status: CONST.TRANSACTION.STATUS.POSTED,
3922+
bank: '',
3923+
reimbursable: true,
3924+
};
3925+
3926+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
3927+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport);
3928+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport);
3929+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction);
3930+
await waitForBatchedUpdates();
3931+
3932+
const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID);
3933+
expect(result).toBe(CONST.REPORT.ACTION_BADGE.PAY);
3934+
});
3935+
37573936
it('should return undefined badge for settled report', async () => {
37583937
const iouReportID = '1300';
37593938
const chatReportID = '1301';

0 commit comments

Comments
 (0)