diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 087fd39444da..b2ce04a6e20c 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -920,6 +920,18 @@ function allHavePendingRTERViolation(transactionIds: string[], transactionViolat return transactionsWithRTERViolations.length > 0 && transactionsWithRTERViolations.every((value) => value === true); } +/** + * Check if there is any transaction without RTER violation within the given transactionIDs. + */ +function hasAnyTransactionWithoutRTERViolation(transactionIds: string[], transactionViolations: OnyxCollection | undefined): boolean { + return ( + transactionIds.length > 0 && + transactionIds.some((transactionId) => { + return !hasBrokenConnectionViolation(transactionId, transactionViolations); + }) + ); +} + /** * Check if the transaction is pending or has a pending rter violation. */ @@ -1539,6 +1551,7 @@ export { getOriginalAmount, getFormattedAttendees, getMerchant, + hasAnyTransactionWithoutRTERViolation, getMerchantOrDescription, getMCCGroup, getCreated, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0373933f3b10..ad170e9383b7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -158,6 +158,7 @@ import { getMerchant, getTransaction, getUpdatedTransaction, + hasAnyTransactionWithoutRTERViolation, hasDuplicateTransactions, hasReceipt as hasReceiptTransactionUtils, isAmountMissing, @@ -174,7 +175,6 @@ import { isReceiptBeingScanned as isReceiptBeingScannedTransactionUtils, isScanRequest as isScanRequestTransactionUtils, removeSettledAndApprovedTransactions, - shouldShowBrokenConnectionViolationForMultipleTransactions, } from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUActionParams, IOUType} from '@src/CONST'; @@ -8588,8 +8588,7 @@ function canSubmitReport( const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const transactionIDList = transactions.map((transaction) => transaction.transactionID); const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList, allViolations); - const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDList, report, policy, allViolations); - + const hasTransactionWithoutRTERViolation = hasAnyTransactionWithoutRTERViolation(transactionIDList, allViolations); const hasOnlyPendingCardOrScanFailTransactions = transactions.length > 0 && transactions.every((t) => (isExpensifyCardTransaction(t) && isPending(t)) || (isPartialMerchant(getMerchant(t)) && isAmountMissing(t)) || isReceiptBeingScannedTransactionUtils(t)); @@ -8600,7 +8599,7 @@ function canSubmitReport( !isArchived && !hasOnlyPendingCardOrScanFailTransactions && !hasAllPendingRTERViolations && - !hasBrokenConnectionViolation && + hasTransactionWithoutRTERViolation && (report?.ownerAccountID === currentUserAccountID || isAdmin || report?.managerID === currentUserAccountID) ); } diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index 6eb96b57c804..c74fdd10b511 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -1,13 +1,24 @@ import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import DateUtils from '@libs/DateUtils'; +import {canSubmitReport} from '@userActions/IOU'; import CONST from '@src/CONST'; import * as IOUUtils from '@src/libs/IOUUtils'; import * as ReportUtils from '@src/libs/ReportUtils'; import * as TransactionUtils from '@src/libs/TransactionUtils'; +import {hasAnyTransactionWithoutRTERViolation} from '@src/libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, Transaction, TransactionViolations} from '@src/types/onyx'; import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; +import createRandomPolicy from '../utils/collections/policies'; +import createRandomReport from '../utils/collections/reports'; +import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import currencyList from './currencyList.json'; +const testDate = DateUtils.getDBTime(); +const currentUserAccountID = 5; + function initCurrencyList() { Onyx.init({ keys: ONYXKEYS, @@ -171,6 +182,150 @@ describe('isValidMoneyRequestType', () => { }); }); +describe('hasRTERWithoutViolation', () => { + test('Return true if there is at least one rter without violation in transactionViolations with given transactionIDs.', async () => { + const transactionIDWithViolation = 1; + const transactionIDWithoutViolation = 2; + const currentReportId = ''; + const transactionWithViolation: Transaction = { + ...createRandomTransaction(transactionIDWithViolation), + category: '', + tag: '', + created: testDate, + reportID: currentReportId, + }; + const transactionWithoutViolation: Transaction = { + ...createRandomTransaction(transactionIDWithoutViolation), + category: '', + tag: '', + created: testDate, + reportID: currentReportId, + }; + const transactionViolations = `transactionViolations_${transactionIDWithViolation}`; + const violations: OnyxCollection = { + [transactionViolations]: [ + { + type: 'warning', + name: 'rter', + data: { + tooltip: "Personal Cards: Fix your card from Account Settings. Corporate Cards: ask your Expensify admin to fix your company's card connection.", + rterType: 'brokenCardConnection', + }, + showInReview: true, + }, + ], + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithViolation}`, transactionWithViolation); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithoutViolation}`, transactionWithoutViolation); + expect(hasAnyTransactionWithoutRTERViolation([String(transactionIDWithoutViolation), String(transactionIDWithViolation)], violations)).toBe(true); + }); + + test('Return false if there is no rter without violation in all transactionViolations with given transactionIDs.', async () => { + const transactionIDWithViolation = 1; + const currentReportId = ''; + const transactionWithViolation: Transaction = { + ...createRandomTransaction(transactionIDWithViolation), + category: '', + tag: '', + created: testDate, + reportID: currentReportId, + }; + const transactionViolations = `transactionViolations_${transactionIDWithViolation}`; + const violations: OnyxCollection = { + [transactionViolations]: [ + { + type: 'warning', + name: 'rter', + data: { + tooltip: "Personal Cards: Fix your card from Account Settings. Corporate Cards: ask your Expensify admin to fix your company's card connection.", + rterType: 'brokenCardConnection', + }, + showInReview: true, + }, + ], + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithViolation}`, transactionWithViolation); + expect(hasAnyTransactionWithoutRTERViolation([String(transactionIDWithViolation)], violations)).toBe(false); + }); +}); + +describe('canSubmitReport', () => { + test('Return true if report can be submitted', async () => { + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID}); + const fakePolicy: Policy = { + ...createRandomPolicy(6), + ownerAccountID: currentUserAccountID, + areRulesEnabled: true, + preventSelfApproval: false, + }; + const expenseReport: Report = { + ...createRandomReport(6), + type: CONST.REPORT.TYPE.EXPENSE, + managerID: currentUserAccountID, + ownerAccountID: currentUserAccountID, + policyID: fakePolicy.id, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const transactionIDWithViolation = 1; + const transactionIDWithoutViolation = 2; + const transactionWithViolation: Transaction = { + ...createRandomTransaction(transactionIDWithViolation), + category: '', + tag: '', + created: testDate, + reportID: expenseReport?.reportID, + }; + const transactionWithoutViolation: Transaction = { + ...createRandomTransaction(transactionIDWithoutViolation), + category: '', + tag: '', + created: testDate, + reportID: expenseReport?.reportID, + }; + const transactionViolations = `transactionViolations_${transactionIDWithViolation}`; + const violations: OnyxCollection = { + [transactionViolations]: [ + { + type: 'warning', + name: 'rter', + data: { + tooltip: "Personal Cards: Fix your card from Account Settings. Corporate Cards: ask your Expensify admin to fix your company's card connection.", + rterType: 'brokenCardConnection', + }, + showInReview: true, + }, + ], + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithViolation}`, transactionWithViolation); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithoutViolation}`, transactionWithoutViolation); + expect(canSubmitReport(expenseReport, fakePolicy, [transactionWithViolation, transactionWithoutViolation], violations)).toBe(true); + }); + + test('Return false if report can not be submitted', async () => { + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID}); + const fakePolicy: Policy = { + ...createRandomPolicy(6), + ownerAccountID: currentUserAccountID, + areRulesEnabled: true, + preventSelfApproval: false, + }; + const expenseReport: Report = { + ...createRandomReport(6), + type: CONST.REPORT.TYPE.EXPENSE, + managerID: currentUserAccountID, + ownerAccountID: currentUserAccountID, + policyID: fakePolicy.id, + }; + + expect(canSubmitReport(expenseReport, fakePolicy, [], undefined)).toBe(false); + }); +}); + describe('Check valid amount for IOU/Expense request', () => { test('IOU amount should be positive', () => { const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, '1', 'USD');