From f16f7de7248c0e198c8a649f6217c796faf666c3 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 26 Aug 2025 15:47:11 +0700 Subject: [PATCH 01/10] Revert "[CP Staging] Revert "fix: approve button is present after submitting a scan expense with missing amount"" --- src/components/MoneyReportHeader.tsx | 11 +---- src/libs/NextStepUtils.ts | 55 +++++++++++++++++++++- src/libs/ReportPrimaryActionUtils.ts | 7 +-- src/libs/TransactionUtils/index.ts | 5 ++ tests/unit/ReportPrimaryActionUtilsTest.ts | 36 ++++++++++++-- 5 files changed, 96 insertions(+), 18 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index a3f34af8e5ee..c51fc1b4654b 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -28,7 +28,7 @@ import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButt import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList, SearchFullscreenNavigatorParamList, SearchReportParamList} from '@libs/Navigation/types'; -import {buildOptimisticNextStepForPreventSelfApprovalsEnabled} from '@libs/NextStepUtils'; +import {getReportNextStep} from '@libs/NextStepUtils'; import {isSecondaryActionAPaymentOption, selectPaymentType} from '@libs/PaymentUtils'; import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; import {getConnectedIntegration, getValidConnectedIntegration} from '@libs/PolicyUtils'; @@ -41,7 +41,6 @@ import { getArchiveReason, getIntegrationExportIcon, getIntegrationNameFromExportMessage as getIntegrationNameFromExportMessageUtils, - getNextApproverAccountID, getNonHeldAndFullAmount, getTransactionsWithReceipts, hasHeldExpenses as hasHeldExpensesReportUtils, @@ -51,7 +50,6 @@ import { isExported as isExportedUtils, isInvoiceReport as isInvoiceReportUtil, isProcessingReport, - isReportOwner, navigateOnDeleteExpense, navigateToDetailsPage, } from '@libs/ReportUtils'; @@ -350,12 +348,7 @@ function MoneyReportHeader({ const shouldShowStatusBar = hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions || hasDuplicates; - // When prevent self-approval is enabled & the current user is submitter AND they're submitting to themselves, we need to show the optimistic next step - // We should always show this optimistic message for policies with preventSelfApproval - // to avoid any flicker during transitions between online/offline states - const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport); - const isSubmitterSameAsNextApprover = isReportOwner(moneyRequestReport) && nextApproverAccountID === moneyRequestReport?.ownerAccountID; - const optimisticNextStep = isSubmitterSameAsNextApprover && policy?.preventSelfApproval ? buildOptimisticNextStepForPreventSelfApprovalsEnabled() : nextStep; + const optimisticNextStep = getReportNextStep(nextStep, moneyRequestReport, transactions, policy); const shouldShowNextStep = isFromPaidPolicy && !isInvoiceReport && !shouldShowStatusBar; const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index a05acb1cd0b2..ba8e9e428493 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -5,7 +5,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, Report, ReportNextStep, TransactionViolations} from '@src/types/onyx'; +import type {Beta, Policy, Report, ReportNextStep, Transaction, TransactionViolations} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import EmailUtils from './EmailUtils'; @@ -20,8 +20,12 @@ import { hasViolations as hasViolationsReportUtils, isExpenseReport, isInvoiceReport, + isOpenExpenseReport, isPayer, + isProcessingReport, + isReportOwner, } from './ReportUtils'; +import {isPendingCardOrIncompleteTransaction, isPendingCardOrScanningTransaction} from './TransactionUtils'; let currentUserAccountID = -1; let currentUserEmail = ''; @@ -124,6 +128,53 @@ function buildOptimisticNextStepForPreventSelfApprovalsEnabled() { return optimisticNextStep; } +function buildOptimisticFixIssueNextStep() { + const optimisticNextStep: ReportNextStep = { + type: 'neutral', + icon: CONST.NEXT_STEP.ICONS.HOURGLASS, + message: [ + { + text: 'Waiting for ', + }, + { + text: `you`, + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'fix the issue(s)', + }, + ], + }; + + return optimisticNextStep; +} + +function getReportNextStep(currentNextStep: ReportNextStep | undefined, moneyRequestReport: OnyxEntry, transactions: Array>, policy: OnyxEntry) { + const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport); + + if (isOpenExpenseReport(moneyRequestReport) && transactions.length > 0 && transactions.every((transaction) => isPendingCardOrIncompleteTransaction(transaction))) { + return buildOptimisticFixIssueNextStep(); + } + + if (isProcessingReport(moneyRequestReport) && transactions.length > 0 && transactions.every((transaction) => isPendingCardOrScanningTransaction(transaction))) { + return buildOptimisticFixIssueNextStep(); + } + + const isSubmitterSameAsNextApprover = isReportOwner(moneyRequestReport) && nextApproverAccountID === moneyRequestReport?.ownerAccountID; + + // When prevent self-approval is enabled & the current user is submitter AND they're submitting to themselves, we need to show the optimistic next step + // We should always show this optimistic message for policies with preventSelfApproval + // to avoid any flicker during transitions between online/offline states + if (isSubmitterSameAsNextApprover && policy?.preventSelfApproval) { + return buildOptimisticNextStepForPreventSelfApprovalsEnabled(); + } + + return currentNextStep; +} + /** * Generates an optimistic nextStep based on a current report status and other properties. * @@ -498,4 +549,4 @@ function buildNextStep( return optimisticNextStep; } -export {parseMessage, buildNextStep, buildOptimisticNextStepForPreventSelfApprovalsEnabled}; +export {parseMessage, buildNextStep, buildOptimisticNextStepForPreventSelfApprovalsEnabled, getReportNextStep}; diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 37b6c34cb5ee..fb1b4b366f89 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -43,7 +43,8 @@ import { hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDuplicate, isOnHold as isOnHoldTransactionUtils, - isPending, + isPendingCardOrIncompleteTransaction, + isPendingCardOrScanningTransaction, isScanning, shouldShowBrokenConnectionViolationForMultipleTransactions, shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, @@ -83,7 +84,7 @@ function isSubmitAction(report: Report, reportTransactions: Transaction[], polic const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL; const transactionAreComplete = reportTransactions.every((transaction) => transaction.amount !== 0 || transaction.modifiedAmount !== 0); - if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPending(transaction))) { + if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPendingCardOrIncompleteTransaction(transaction))) { return false; } @@ -128,7 +129,7 @@ function isApproveAction(report: Report, reportTransactions: Transaction[], poli return false; } - if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPending(transaction))) { + if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPendingCardOrScanningTransaction(transaction))) { return false; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 544567d996be..e47911fdfcf3 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -255,6 +255,10 @@ function isPendingCardOrScanningTransaction(transaction: OnyxEntry) return (isExpensifyCardTransaction(transaction) && isPending(transaction)) || isPartialTransaction(transaction) || (isScanRequest(transaction) && isScanning(transaction)); } +function isPendingCardOrIncompleteTransaction(transaction: OnyxEntry): boolean { + return (isExpensifyCardTransaction(transaction) && isPending(transaction)) || (isAmountMissing(transaction) && isMerchantMissing(transaction)); +} + /** * Optimistically generate a transaction. * @@ -2002,6 +2006,7 @@ export { isDemoTransaction, shouldShowViolation, isUnreportedAndHasInvalidDistanceRateTransaction, + isPendingCardOrIncompleteTransaction, getTransactionViolationsOfTransaction, isExpenseSplit, }; diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index bfbbdc618c84..a2f0c231db6e 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -80,7 +80,7 @@ describe('getPrimaryAction', () => { ); }); - it('should not return SUBMIT option for admin with only pending transactions', async () => { + it('should not return SUBMIT option for admin with only pending/incomplete transactions', async () => { const report = { reportID: REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, @@ -99,9 +99,22 @@ describe('getPrimaryAction', () => { amount: 10, merchant: 'Merchant', date: '2025-01-01', + bank: CONST.EXPENSIFY_CARD.BANK, } as unknown as Transaction; - expect(getReportPrimaryAction({report, chatReport, reportTransactions: [transaction], violations: {}, policy: policy as Policy, isChatReportArchived: false})).toBe(''); + const transaction1 = { + reportID: `${REPORT_ID}`, + amount: 0, + modifiedAmount: 0, + receipt: { + source: 'test', + state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, + }, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + modifiedMerchant: undefined, + } as unknown as Transaction; + + expect(getReportPrimaryAction({report, chatReport, reportTransactions: [transaction, transaction1], violations: {}, policy: policy as Policy, isChatReportArchived: false})).toBe(''); }); it('should return Approve for report being processed', async () => { @@ -123,6 +136,8 @@ describe('getPrimaryAction', () => { comment: { hold: 'Hold', }, + amount: 10, + merchant: 'merchant', } as unknown as Transaction; expect(getReportPrimaryAction({report, chatReport, reportTransactions: [transaction], violations: {}, policy: policy as Policy, isChatReportArchived: false})).toBe( @@ -157,7 +172,7 @@ describe('getPrimaryAction', () => { expect(getReportPrimaryAction({report, chatReport, reportTransactions: [transaction], violations: {}, policy: policy as Policy, isChatReportArchived: false})).toBe(''); }); - it('should return empty for report being processed but transactions are pending', async () => { + it('should return empty for report being processed but transactions are pending/partial', async () => { const report = { reportID: REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, @@ -177,9 +192,22 @@ describe('getPrimaryAction', () => { amount: 10, merchant: 'Merchant', date: '2025-01-01', + bank: CONST.EXPENSIFY_CARD.BANK, } as unknown as Transaction; - expect(getReportPrimaryAction({report, chatReport, reportTransactions: [transaction], violations: {}, policy: policy as Policy, isChatReportArchived: false})).toBe(''); + const transaction1 = { + reportID: `${REPORT_ID}`, + amount: 0, + modifiedAmount: 0, + receipt: { + source: 'test', + state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, + }, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + modifiedMerchant: undefined, + } as unknown as Transaction; + + expect(getReportPrimaryAction({report, chatReport, reportTransactions: [transaction, transaction1], violations: {}, policy: policy as Policy, isChatReportArchived: false})).toBe(''); }); it('should return PAY for submitted invoice report if paid as personal', async () => { From b344d457d8484e754679c1ede0426935c163cafb Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 26 Aug 2025 16:04:32 +0700 Subject: [PATCH 02/10] fix missing merchant case --- src/libs/TransactionUtils/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index e47911fdfcf3..35deb85c4886 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -238,12 +238,6 @@ function isManualRequest(transaction: Transaction): boolean { } function isPartialTransaction(transaction: OnyxEntry): boolean { - const merchant = getMerchant(transaction); - - if (!merchant || isPartialMerchant(merchant)) { - return true; - } - if (isAmountMissing(transaction) && isScanRequest(transaction)) { return true; } From 00afc44c0a7fc08ab9de67a44be8f5b4c215e99e Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 26 Aug 2025 16:15:15 +0700 Subject: [PATCH 03/10] fix test --- tests/actions/ReportPreviewActionUtilsTest.ts | 2 ++ tests/unit/ReportPrimaryActionUtilsTest.ts | 2 ++ tests/unit/TransactionPreviewUtils.test.ts | 4 ++-- tests/unit/ViolationUtilsTest.ts | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index da7f9af809dd..70f4c130f6fc 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -173,9 +173,11 @@ describe('getReportPreviewAction', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); const transaction = { + amount: 0, reportID: `${REPORT_ID}`, receipt: { state: CONST.IOU.RECEIPT_STATE.SCANNING, + source: 'test', }, } as unknown as Transaction; diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index a2f0c231db6e..30527171448d 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -160,12 +160,14 @@ describe('getPrimaryAction', () => { approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, }; const transaction = { + amount: 0, reportID: `${REPORT_ID}`, comment: { hold: 'Hold', }, receipt: { state: CONST.IOU.RECEIPT_STATE.SCANNING, + source: 'test' }, } as unknown as Transaction; diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index e64678081776..802554140e98 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -138,7 +138,7 @@ describe('TransactionPreviewUtils', () => { }); it('displays description when receipt is being scanned', () => { - const functionArgs = {...basicProps, transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}}, originalTransaction: undefined}; + const functionArgs = {...basicProps, transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, amount: 0}, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); expect(result.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'common.receipt'}])); }); @@ -153,7 +153,7 @@ describe('TransactionPreviewUtils', () => { const functionArgs = { ...basicProps, transactionDetails: {amount: 300, currency: 'EUR'}, - transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}}, + transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, amount: 0}, originalTransaction: undefined, }; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index feb20d952ef0..247369fe6aa5 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -288,7 +288,7 @@ describe('getViolationsOnyxData', () => { amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, category: undefined, - receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, }; const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policy, policyTags, policyCategories, false, false); expect(result.value).not.toContainEqual(missingCategoryViolation); @@ -386,7 +386,7 @@ describe('getViolationsOnyxData', () => { amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, tag: undefined, - receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, }; const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policy, policyTags, policyCategories, false, false); expect(result.value).not.toContainEqual(missingTagViolation); From 93c84fc048e68c144c4ce6a2e99b259719d22193 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 26 Aug 2025 16:20:21 +0700 Subject: [PATCH 04/10] run prettier --- tests/unit/ReportPrimaryActionUtilsTest.ts | 2 +- tests/unit/TransactionPreviewUtils.test.ts | 6 +++++- tests/unit/TransactionUtilsTest.ts | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index 30527171448d..13b593acba9f 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -167,7 +167,7 @@ describe('getPrimaryAction', () => { }, receipt: { state: CONST.IOU.RECEIPT_STATE.SCANNING, - source: 'test' + source: 'test', }, } as unknown as Transaction; diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 802554140e98..fc6f1e83042c 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -138,7 +138,11 @@ describe('TransactionPreviewUtils', () => { }); it('displays description when receipt is being scanned', () => { - const functionArgs = {...basicProps, transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, amount: 0}, originalTransaction: undefined}; + const functionArgs = { + ...basicProps, + transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, amount: 0}, + originalTransaction: undefined, + }; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); expect(result.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'common.receipt'}])); }); diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 09689eedbec5..5c9107c4101b 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -340,8 +340,10 @@ describe('TransactionUtils', () => { describe('shouldShowRTERViolationMessage', () => { it('should return true if transaction is receipt being scanned', () => { const transaction = generateTransaction({ + amount: 0, receipt: { state: CONST.IOU.RECEIPT_STATE.SCAN_READY, + source: 'test', }, }); expect(TransactionUtils.shouldShowRTERViolationMessage([transaction])).toBe(true); From 85b09d476d7740abd80cddf400daf979209a1d91 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 21 Oct 2025 11:17:17 +0700 Subject: [PATCH 05/10] remove unnecessary change --- src/libs/NextStepUtils.ts | 4 ---- src/libs/ReportPrimaryActionUtils.ts | 4 ++-- src/libs/TransactionUtils/index.ts | 6 ++++++ tests/unit/ReportPrimaryActionUtilsTest.ts | 17 ++--------------- tests/unit/TransactionPreviewUtils.test.ts | 8 ++------ tests/unit/TransactionUtilsTest.ts | 2 -- tests/unit/ViolationUtilsTest.ts | 4 ++-- 7 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index c5bb73bce155..22adb200b6c5 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -172,10 +172,6 @@ function getReportNextStep(currentNextStep: ReportNextStep | undefined, moneyReq return buildOptimisticFixIssueNextStep(); } - if (isProcessingReport(moneyRequestReport) && transactions.length > 0 && transactions.every((transaction) => isPendingCardOrScanningTransaction(transaction))) { - return buildOptimisticFixIssueNextStep(); - } - const isSubmitterSameAsNextApprover = isReportOwner(moneyRequestReport) && nextApproverAccountID === moneyRequestReport?.ownerAccountID; // When prevent self-approval is enabled & the current user is submitter AND they're submitting to themselves, we need to show the optimistic next step diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 1392bb3b2871..ba061657309f 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -44,8 +44,8 @@ import { hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDuplicate, isOnHold as isOnHoldTransactionUtils, + isPending, isPendingCardOrIncompleteTransaction, - isPendingCardOrScanningTransaction, isScanning, shouldShowBrokenConnectionViolationForMultipleTransactions, shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, @@ -133,7 +133,7 @@ function isApproveAction(report: Report, reportTransactions: Transaction[], poli return false; } - if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPendingCardOrScanningTransaction(transaction))) { + if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPending(transaction))) { return false; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index f91f52e8a068..2d33264521ae 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -263,6 +263,12 @@ function isManualRequest(transaction: Transaction): boolean { } function isPartialTransaction(transaction: OnyxEntry): boolean { + const merchant = getMerchant(transaction); + + if (!merchant || isPartialMerchant(merchant)) { + return true; + } + if (isAmountMissing(transaction) && isScanRequest(transaction)) { return true; } diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index d48abd18c508..758b7908de2d 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -242,7 +242,7 @@ describe('getPrimaryAction', () => { ).toBe(''); }); - it('should return empty for report being processed but transactions are pending/partial', async () => { + it('should return empty for report being processed but transactions are pending', async () => { const report = { reportID: REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, @@ -262,19 +262,6 @@ describe('getPrimaryAction', () => { amount: 10, merchant: 'Merchant', date: '2025-01-01', - bank: CONST.EXPENSIFY_CARD.BANK, - } as unknown as Transaction; - - const transaction1 = { - reportID: `${REPORT_ID}`, - amount: 0, - modifiedAmount: 0, - receipt: { - source: 'test', - state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, - }, - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - modifiedMerchant: undefined, } as unknown as Transaction; expect( @@ -282,7 +269,7 @@ describe('getPrimaryAction', () => { currentUserEmail: CURRENT_USER_EMAIL, report, chatReport, - reportTransactions: [transaction, transaction1], + reportTransactions: [transaction], violations: {}, policy: policy as Policy, isChatReportArchived: false, diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 3d3ab04acc98..4331f657be60 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -138,11 +138,7 @@ describe('TransactionPreviewUtils', () => { }); it('displays description when receipt is being scanned', () => { - const functionArgs = { - ...basicProps, - transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, amount: 0}, - originalTransaction: undefined, - }; + const functionArgs = {...basicProps, transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}}, originalTransaction: undefined}; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); expect(result.previewHeaderText).toEqual(expect.arrayContaining([{translationPath: 'common.receipt'}])); }); @@ -157,7 +153,7 @@ describe('TransactionPreviewUtils', () => { const functionArgs = { ...basicProps, transactionDetails: {amount: 300, currency: 'EUR'}, - transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, amount: 0}, + transaction: {...basicProps.transaction, receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}}, originalTransaction: undefined, }; const result = getTransactionPreviewTextAndTranslationPaths(functionArgs); diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index c8c49dfe85bc..d482d7043c52 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -402,10 +402,8 @@ describe('TransactionUtils', () => { describe('shouldShowRTERViolationMessage', () => { it('should return true if transaction is receipt being scanned', () => { const transaction = generateTransaction({ - amount: 0, receipt: { state: CONST.IOU.RECEIPT_STATE.SCAN_READY, - source: 'test', }, }); expect(TransactionUtils.shouldShowRTERViolationMessage([transaction])).toBe(true); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index d1827d61a067..afffa0890a96 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -327,7 +327,7 @@ describe('getViolationsOnyxData', () => { amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, category: undefined, - receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}, }; const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policy, policyTags, policyCategories, false, false); expect(result.value).not.toContainEqual(missingCategoryViolation); @@ -425,7 +425,7 @@ describe('getViolationsOnyxData', () => { amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, tag: undefined, - receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'test'}, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING}, }; const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policy, policyTags, policyCategories, false, false); expect(result.value).not.toContainEqual(missingTagViolation); From 9ac3cb5f6c5cee9f46c1983c0cfb877a30a553df Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 21 Oct 2025 11:18:09 +0700 Subject: [PATCH 06/10] remove unused change --- tests/actions/ReportPreviewActionUtilsTest.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index d15cd517b48c..5c9329cdd423 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -214,11 +214,9 @@ describe('getReportPreviewAction', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); const transaction = { - amount: 0, reportID: `${REPORT_ID}`, receipt: { state: CONST.IOU.RECEIPT_STATE.SCANNING, - source: 'test', }, } as unknown as Transaction; From 7a2903a95c3c5af28626a40034bf41f526479b00 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 21 Oct 2025 11:36:34 +0700 Subject: [PATCH 07/10] remove unused import --- src/libs/NextStepUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 22adb200b6c5..862c2b8eef74 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -22,10 +22,9 @@ import { isInvoiceReport, isOpenExpenseReport, isPayer, - isProcessingReport, isReportOwner, } from './ReportUtils'; -import {isPendingCardOrIncompleteTransaction, isPendingCardOrScanningTransaction} from './TransactionUtils'; +import {isPendingCardOrIncompleteTransaction} from './TransactionUtils'; type BuildNextStepNewParams = { report: OnyxEntry; From 186e784188e5ad3d4279a9e43dafcea20d4e94fb Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Sat, 22 Nov 2025 21:18:09 +0700 Subject: [PATCH 08/10] fix lint and ts error --- src/libs/NextStepUtils.ts | 7 ++++++- src/libs/TransactionUtils/index.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 4881efbce576..6063bed4f3cc 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -392,7 +392,12 @@ function buildOptimisticNextStepForStrictPolicyRuleViolations() { return optimisticNextStep; } -function getReportNextStep(currentNextStep: ReportNextStep | undefined, moneyRequestReport: OnyxEntry, transactions: Array>, policy: OnyxEntry) { +function getReportNextStep( + currentNextStep: ReportNextStepDeprecated | undefined, + moneyRequestReport: OnyxEntry, + transactions: Array>, + policy: OnyxEntry, +) { const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport); if (isOpenExpenseReport(moneyRequestReport) && transactions.length > 0 && transactions.every((transaction) => isPendingCardOrIncompleteTransaction(transaction))) { diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 12f8591d71fd..4b7e2115caa3 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -68,7 +68,7 @@ import type { import type {Attendee, Participant, SplitExpense} from '@src/types/onyx/IOU'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; +import type {SearchReport} from '@src/types/onyx/SearchResults'; import type { Comment, Receipt, @@ -1099,7 +1099,7 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry, r * Get all transaction violations of the transaction with given transactionID. */ function getTransactionViolations( - transaction: OnyxEntry, + transaction: OnyxEntry, transactionViolations: OnyxCollection, currentUserEmail?: string, ): TransactionViolations | undefined { @@ -1132,7 +1132,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if there is broken connection violation. */ -function hasBrokenConnectionViolation(transaction: Transaction | SearchTransaction, transactionViolations: OnyxCollection | undefined): boolean { +function hasBrokenConnectionViolation(transaction: Transaction, transactionViolations: OnyxCollection | undefined): boolean { const violations = getTransactionViolations(transaction, transactionViolations); return !!violations?.find((violation) => isBrokenConnectionViolation(violation)); } @@ -1247,7 +1247,7 @@ function shouldShowViolation( /** * Check if there is pending rter violation in all transactionViolations with given transactionIDs. */ -function allHavePendingRTERViolation(transactions: OnyxEntry, transactionViolations: OnyxCollection | undefined): boolean { +function allHavePendingRTERViolation(transactions: OnyxEntry, transactionViolations: OnyxCollection | undefined): boolean { if (!transactions) { return false; } @@ -1269,7 +1269,7 @@ function checkIfShouldShowMarkAsCashButton(hasRTERPendingViolation: boolean, sho /** * Check if there is any transaction without RTER violation within the given transactionIDs. */ -function hasAnyTransactionWithoutRTERViolation(transactions: Transaction[] | SearchTransaction[], transactionViolations: OnyxCollection | undefined): boolean { +function hasAnyTransactionWithoutRTERViolation(transactions: Transaction[], transactionViolations: OnyxCollection | undefined): boolean { return ( transactions.length > 0 && transactions.some((transaction) => { @@ -1424,7 +1424,7 @@ function hasViolation(transaction: Transaction | undefined, transactionViolation ); } -function hasDuplicateTransactions(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean { +function hasDuplicateTransactions(iouReportID?: string, allReportTransactions?: Transaction[]): boolean { const transactionsByIouReportID = getReportTransactions(iouReportID); const reportTransactions = allReportTransactions ?? transactionsByIouReportID; From b5ff761fede6e5b155fa296eccfd6e068cb66a29 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 28 Jan 2026 15:35:19 +0700 Subject: [PATCH 09/10] fix: use new logics --- src/components/MoneyReportHeader.tsx | 2 +- src/libs/NextStepUtils.ts | 17 +++++++++++++---- src/libs/ReportPrimaryActionUtils.ts | 8 ++++++-- src/libs/TransactionUtils/index.ts | 5 ----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 72d92debd2bf..bc49a0a16b93 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -487,7 +487,7 @@ function MoneyReportHeader({ hasDuplicates || shouldShowMarkAsResolved; - let optimisticNextStep = getReportNextStep(nextStep, moneyRequestReport, transactions, policy); + let optimisticNextStep = getReportNextStep(nextStep, moneyRequestReport, transactions, policy, allTransactionViolations, email ?? '', accountID); // Check for DEW submit failed or pending - show appropriate next step if (isDEWBetaEnabled && hasDynamicExternalWorkflow(policy) && moneyRequestReport?.statusNum === CONST.REPORT.STATUS_NUM.OPEN) { diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index e404e06e57e4..d47d22ce41df 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,10 +1,10 @@ import {addMonths, format, isPast, setDate} from 'date-fns'; import {Str} from 'expensify-common'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import type {Policy, Report, ReportNextStepDeprecated, Transaction} from '@src/types/onyx'; +import type {Policy, Report, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; import type {ReportNextStep} from '@src/types/onyx/Report'; import type {Message} from '@src/types/onyx/ReportNextStepDeprecated'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -24,7 +24,7 @@ import { isPayer, isReportOwner, } from './ReportUtils'; -import {isPendingCardOrIncompleteTransaction} from './TransactionUtils'; +import {hasSubmissionBlockingViolations} from './TransactionUtils'; type BuildNextStepNewParams = { report: OnyxEntry; @@ -364,10 +364,19 @@ function getReportNextStep( moneyRequestReport: OnyxEntry, transactions: Array>, policy: OnyxEntry, + transactionViolations: OnyxCollection, + currentUserEmail: string, + currentUserAccountID: number, ) { const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport); - if (isOpenExpenseReport(moneyRequestReport) && transactions.length > 0 && transactions.every((transaction) => isPendingCardOrIncompleteTransaction(transaction))) { + if ( + isOpenExpenseReport(moneyRequestReport) && + transactions.length > 0 && + transactions.every( + (transaction) => !!transaction && hasSubmissionBlockingViolations(transaction, transactionViolations, currentUserEmail, currentUserAccountID, moneyRequestReport, policy), + ) + ) { return buildOptimisticFixIssueNextStep(); } diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 11184b0729bc..5fe59670b2b9 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -45,7 +45,6 @@ import { isDuplicate, isOnHold as isOnHoldTransactionUtils, isPending, - isPendingCardOrIncompleteTransaction, isScanning, shouldShowBrokenConnectionViolationForMultipleTransactions, shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, @@ -104,7 +103,12 @@ function isSubmitAction( } const transactionAreComplete = reportTransactions.every((transaction) => transaction.amount !== 0 || transaction.modifiedAmount !== 0); - if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPendingCardOrIncompleteTransaction(transaction))) { + if ( + reportTransactions.length > 0 && + reportTransactions.every((transaction) => + hasSubmissionBlockingViolations(transaction, violations, currentUserEmail ?? '', currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, report, policy), + ) + ) { return false; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 44a54083a01a..afe9ad3479af 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -441,10 +441,6 @@ function isPendingCardOrScanningTransaction(transaction: OnyxEntry) ); } -function isPendingCardOrIncompleteTransaction(transaction: OnyxEntry): boolean { - return (isExpensifyCardTransaction(transaction) && isPending(transaction)) || (isAmountMissing(transaction) && isMerchantMissing(transaction)); -} - /** * Optimistically generate a transaction. * @@ -2846,7 +2842,6 @@ export { isDemoTransaction, shouldShowViolation, isUnreportedAndHasInvalidDistanceRateTransaction, - isPendingCardOrIncompleteTransaction, hasTransactionBeenRejected, isExpenseSplit, getAttendeesListDisplayString, From 70f025cd3a2bece87fba53bd67c41d56b33faa7f Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 28 Jan 2026 15:40:10 +0700 Subject: [PATCH 10/10] fix: remove tests --- src/components/MoneyReportHeader.tsx | 2 +- src/libs/ReportPrimaryActionUtils.ts | 7 +------ tests/unit/ReportPrimaryActionUtilsTest.ts | 22 ++-------------------- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index bc49a0a16b93..1e824156f289 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -47,7 +47,7 @@ import Log from '@libs/Log'; import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList, SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import { buildOptimisticNextStepForDEWOfflineSubmission, buildOptimisticNextStepForDynamicExternalWorkflowError, diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 5fe59670b2b9..9c094f88eead 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -103,12 +103,7 @@ function isSubmitAction( } const transactionAreComplete = reportTransactions.every((transaction) => transaction.amount !== 0 || transaction.modifiedAmount !== 0); - if ( - reportTransactions.length > 0 && - reportTransactions.every((transaction) => - hasSubmissionBlockingViolations(transaction, violations, currentUserEmail ?? '', currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, report, policy), - ) - ) { + if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPending(transaction))) { return false; } diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index ff73844fa2bd..ec1bc80dffbb 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -144,8 +144,7 @@ describe('getPrimaryAction', () => { }), ).toBe(CONST.REPORT.PRIMARY_ACTIONS.SUBMIT); }); - - it('should not return SUBMIT option for admin with only pending/incomplete transactions', async () => { + it('should not return SUBMIT option for admin with only pending transactions', async () => { const report = { reportID: REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, @@ -164,19 +163,6 @@ describe('getPrimaryAction', () => { amount: 10, merchant: 'Merchant', date: '2025-01-01', - bank: CONST.EXPENSIFY_CARD.BANK, - } as unknown as Transaction; - - const transaction1 = { - reportID: `${REPORT_ID}`, - amount: 0, - modifiedAmount: 0, - receipt: { - source: 'test', - state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, - }, - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - modifiedMerchant: undefined, } as unknown as Transaction; expect( @@ -185,7 +171,7 @@ describe('getPrimaryAction', () => { currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, chatReport, - reportTransactions: [transaction, transaction1], + reportTransactions: [transaction], violations: {}, bankAccountList: {}, policy: policy as Policy, @@ -213,8 +199,6 @@ describe('getPrimaryAction', () => { comment: { hold: 'Hold', }, - amount: 10, - merchant: 'merchant', } as unknown as Transaction; expect( @@ -247,14 +231,12 @@ describe('getPrimaryAction', () => { approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, }; const transaction = { - amount: 0, reportID: `${REPORT_ID}`, comment: { hold: 'Hold', }, receipt: { state: CONST.IOU.RECEIPT_STATE.SCANNING, - source: 'test', }, } as unknown as Transaction;