From 8353ad0a15650496b1b7c3be728dd2aaa2c45cc2 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 30 Oct 2025 12:11:53 +0700 Subject: [PATCH 1/4] fix: As Admin (non-approver), GBR appears in LHN after opening WS chat --- src/components/MoneyReportHeader.tsx | 3 +- .../MoneyRequestReportPreviewContent.tsx | 5 ++- .../Search/ActionCell.tsx | 4 +-- src/libs/actions/IOU.ts | 29 ++++++++------- tests/actions/IOUTest.ts | 36 +++++++------------ 5 files changed, 31 insertions(+), 46 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 15e0ad8d76799..17f33d6b3bb11 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -315,8 +315,7 @@ function MoneyReportHeader({ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${moneyRequestReport?.reportID}`, {canBeMissing: true}); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) => - canIOUBePaidAction(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState), + (onlyShowPayElsewhere = false) => canIOUBePaidAction(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere), [moneyRequestReport, chatReport, policy, transaction], ); diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index f9d905d1b0370..aa10fe20baeae 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -169,8 +169,7 @@ function MoneyRequestReportPreviewContent({ const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations); const getCanIOUBePaid = useCallback( - (shouldShowOnlyPayElsewhere = false, shouldCheckApprovedState = true) => - canIOUBePaidIOUActions(iouReport, chatReport, policy, transactions, shouldShowOnlyPayElsewhere, undefined, undefined, shouldCheckApprovedState), + (shouldShowOnlyPayElsewhere = false) => canIOUBePaidIOUActions(iouReport, chatReport, policy, transactions, shouldShowOnlyPayElsewhere), [iouReport, chatReport, policy, transactions], ); @@ -179,7 +178,7 @@ function MoneyRequestReportPreviewContent({ const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere; const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(iouReport, shouldShowPayButton); - const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]); + const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false), [getCanIOUBePaid]); const connectedIntegration = getConnectedIntegration(policy); const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(iouReport?.reportID); diff --git a/src/components/SelectionListWithSections/Search/ActionCell.tsx b/src/components/SelectionListWithSections/Search/ActionCell.tsx index 557a8e442ff36..158f2d941d6bc 100644 --- a/src/components/SelectionListWithSections/Search/ActionCell.tsx +++ b/src/components/SelectionListWithSections/Search/ActionCell.tsx @@ -72,8 +72,8 @@ function ActionCell({ const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID); const policy = usePolicy(policyID); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`, {canBeMissing: true}); - const canBePaid = canIOUBePaid(iouReport, chatReport, policy, transactions, false, undefined, undefined, true); - const shouldOnlyShowElsewhere = !canBePaid && canIOUBePaid(iouReport, chatReport, policy, transactions, true, undefined, undefined, true); + const canBePaid = canIOUBePaid(iouReport, chatReport, policy, transactions, false); + const shouldOnlyShowElsewhere = !canBePaid && canIOUBePaid(iouReport, chatReport, policy, transactions, true); const text = isChildListItem ? translate(actionTranslationsMap[CONST.SEARCH.ACTION_TYPES.VIEW]) : translate(actionTranslationsMap[action]); const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 6aa23f334884f..1731300c1186e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -191,6 +191,7 @@ import { isPayAtEndExpenseReport as isPayAtEndExpenseReportReportUtils, isPayer as isPayerReportUtils, isPolicyExpenseChat as isPolicyExpenseChatReportUtil, + isProcessingReport, isReportApproved, isReportManager, isSelectedManagerMcTest, @@ -10071,7 +10072,6 @@ function canIOUBePaid( onlyShowPayElsewhere = false, chatReportRNVP?: OnyxTypes.ReportNameValuePairs, invoiceReceiverPolicy?: SearchPolicy, - shouldCheckApprovedState = true, ) { const reportNameValuePairs = chatReportRNVP ?? allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatReport?.reportID}`]; const isChatReportArchived = isArchivedReport(reportNameValuePairs); @@ -10112,23 +10112,22 @@ function canIOUBePaid( policy, ); - const isOpenExpenseReport = isOpenExpenseReportReportUtils(iouReport); - const {reimbursableSpend} = getMoneyRequestSpendBreakdown(iouReport); const isAutoReimbursable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES ? false : canBeAutoReimbursed(iouReport, policy); - const shouldBeApproved = canApproveIOU(iouReport, policy, transactions); const isPayAtEndExpenseReport = isPayAtEndExpenseReportReportUtils(iouReport ?? undefined, transactions); - return ( - isPayer && - !isOpenExpenseReport && - !iouSettled && - !iouReport?.isWaitingOnBankAccount && - reimbursableSpend > 0 && - !isChatReportArchived && - !isAutoReimbursable && - (!shouldBeApproved || !shouldCheckApprovedState) && - !isPayAtEndExpenseReport - ); + const isProcessing = isProcessingReport(iouReport); + const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; + const isSubmittedWithoutApprovalsEnabled = !isApprovalEnabled && isProcessing; + const isApproved = isReportApproved({report: iouReport}) || isSubmittedWithoutApprovalsEnabled; + const isClosed = isClosedReportUtil(iouReport); + const isReportFinished = (isApproved || isClosed) && !iouReport?.isWaitingOnBankAccount; + const isIOU = isIOUReport(iouReport); + + if (isIOU && isPayer && !iouSettled && reimbursableSpend > 0) { + return true; + } + + return isPayer && isReportFinished && !iouSettled && reimbursableSpend > 0 && !isChatReportArchived && !isAutoReimbursable && !isPayAtEndExpenseReport; } function canCancelPayment(iouReport: OnyxEntry, session: OnyxEntry) { diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 43d6f82ed3a80..43534ad8749ad 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -5057,10 +5057,8 @@ describe('actions/IOU', () => { }); resolve(); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, true)).toBe(true); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, false)).toBe(true); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, false)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], true)).toBe(true); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], false)).toBe(false); }, }); }), @@ -5080,10 +5078,8 @@ describe('actions/IOU', () => { expect(expenseReport?.stateNum).toBe(0); expect(expenseReport?.statusNum).toBe(0); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, false)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, false)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], true)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], false)).toBe(false); }, }); }), @@ -5109,10 +5105,8 @@ describe('actions/IOU', () => { expect(expenseReport?.stateNum).toBe(2); expect(expenseReport?.statusNum).toBe(2); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, true)).toBe(true); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, false)).toBe(true); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, false)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], true)).toBe(true); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], false)).toBe(false); }, }); }), @@ -5141,10 +5135,8 @@ describe('actions/IOU', () => { Onyx.disconnect(connection); chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, false)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, false)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], true)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], false)).toBe(false); resolve(); }, }); @@ -5237,10 +5229,8 @@ describe('actions/IOU', () => { stateNum: 0, }); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, true)).toBe(true); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, false)).toBe(true); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, false)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], true)).toBe(true); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], false)).toBe(false); resolve(); }, }); @@ -5284,10 +5274,8 @@ describe('actions/IOU', () => { // Report was submitted with some fail expect(expenseReport?.stateNum).toBe(0); expect(expenseReport?.statusNum).toBe(0); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], true, undefined, undefined, false)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, true)).toBe(false); - expect(canIOUBePaid(expenseReport, chatReport, policy, [], false, undefined, undefined, false)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], true)).toBe(false); + expect(canIOUBePaid(expenseReport, chatReport, policy, [], false)).toBe(false); resolve(); }, }); From 42c2efa8c4e1f140ff6a4bda7992c85bab1fa12c Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 13 Nov 2025 16:19:41 +0700 Subject: [PATCH 2/4] fix test --- tests/actions/IOUTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 4ab395ed278b9..501867897f70c 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -6544,8 +6544,8 @@ describe('actions/IOU', () => { total: 100, // positive amount in the DB means negative amount in the UI }; - expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, [], false, undefined, undefined, false)).toBeFalsy(); - expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, [], true, undefined, undefined, false)).toBeTruthy(); + expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, [], false)).toBeFalsy(); + expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, [], true)).toBeTruthy(); }); }); From c779f8a7bca916a39fea99189785bca0c3cda773 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 13 Nov 2025 16:37:17 +0700 Subject: [PATCH 3/4] fix test --- tests/actions/IOUTest.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 501867897f70c..130c539ee9b58 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -6521,13 +6521,13 @@ describe('actions/IOU', () => { }); describe('canIOUBePaid', () => { - it('should return false if the report has negative total and onlyShowPayElsewhere is false', () => { + it('should return false if the report has negative total and onlyShowPayElsewhere is false', async() => { const policyChat = createRandomReport(1, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT); const fakePolicy: Policy = { ...createRandomPolicy(Number('AA')), id: 'AA', type: CONST.POLICY.TYPE.TEAM, - approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO, role: CONST.POLICY.ROLE.ADMIN, }; @@ -6544,6 +6544,8 @@ describe('actions/IOU', () => { total: 100, // positive amount in the DB means negative amount in the UI }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, [], false)).toBeFalsy(); expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, [], true)).toBeTruthy(); }); From 480aac3c898cbd9b366f1324a4623259bff86280 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Nov 2025 15:02:00 +0700 Subject: [PATCH 4/4] resolve conflict --- tests/actions/IOUTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 8008ef2f9fcd1..787e168da97d0 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -6542,7 +6542,7 @@ describe('actions/IOU', () => { }); describe('canIOUBePaid', () => { - it('should return false if the report has negative total and onlyShowPayElsewhere is false', async() => { + it('should return false if the report has negative total and onlyShowPayElsewhere is false', async () => { const policyChat = createRandomReport(1, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT); const fakePolicy: Policy = { ...createRandomPolicy(Number('AA')),