Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@
transactionViolations: OnyxCollection<OnyxTypes.TransactionViolation[]>;
quickAction: OnyxEntry<OnyxTypes.QuickAction>;
policyRecentlyUsedCurrencies: string[];
betas: OnyxEntry<OnyxTypes.Beta[]>;

Check warning on line 562 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>;
};

Expand Down Expand Up @@ -678,7 +678,7 @@
userBillingGracePeriodEnds: OnyxCollection<OnyxTypes.BillingGraceEndPeriod>;
amountOwed: OnyxEntry<number>;
onSubmitted?: () => void;
ownerBillingGracePeriodEnd: OnyxEntry<number>;

Check warning on line 681 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
delegateEmail: string | undefined;
};

Expand All @@ -692,7 +692,7 @@
return;
}

allTransactions = value;

Check warning on line 695 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand All @@ -701,7 +701,7 @@
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
allTransactionDrafts = value ?? {};

Check warning on line 704 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand All @@ -715,7 +715,7 @@
return;
}

allTransactionViolations = value;

Check warning on line 718 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand All @@ -728,7 +728,7 @@
allPolicyTags = {};
return;
}
allPolicyTags = value;

Check warning on line 731 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand All @@ -737,7 +737,7 @@
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
allReports = value;

Check warning on line 740 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand All @@ -747,7 +747,7 @@
waitForCollectionCallback: true,
callback: (value) => {
allReportNameValuePairs = value;
},

Check warning on line 750 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
});

let deprecatedUserAccountID = -1;
Expand All @@ -756,7 +756,7 @@
key: ONYXKEYS.SESSION,
callback: (value) => {
deprecatedCurrentUserEmail = value?.email ?? '';
deprecatedUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;

Check warning on line 759 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand All @@ -764,7 +764,7 @@
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
deprecatedCurrentUserPersonalDetails = value?.[deprecatedUserAccountID] ?? undefined;

Check warning on line 767 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
},
});

Expand Down Expand Up @@ -7167,8 +7167,11 @@
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;
}
Expand Down
134 changes: 134 additions & 0 deletions tests/actions/IOUTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading