diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 251f50f2e9b2..79a24908ca39 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -32,6 +32,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import useTransactionViolations from '@hooks/useTransactionViolations'; +import {duplicateExpenseTransaction as duplicateTransactionAction} from '@libs/actions/IOU/Duplicate'; import {openOldDotLink} from '@libs/actions/Link'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -116,7 +117,6 @@ import { submitReport, unapproveExpenseReport, } from '@userActions/IOU'; -import {duplicateExpenseTransaction as duplicateTransactionAction} from '@userActions/IOU/DuplicateAction'; import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 07c01272f3c1..50808f30e565 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -21,6 +21,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useTransactionViolations from '@hooks/useTransactionViolations'; import {deleteTrackExpense, initSplitExpense, markRejectViolationAsResolved} from '@libs/actions/IOU'; +import {duplicateExpenseTransaction as duplicateTransactionAction} from '@libs/actions/IOU/Duplicate'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; import {setNameValuePair} from '@libs/actions/User'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -54,7 +55,6 @@ import { } from '@libs/TransactionUtils'; import variables from '@styles/variables'; import {dismissRejectUseExplanation} from '@userActions/IOU'; -import {duplicateExpenseTransaction as duplicateTransactionAction} from '@userActions/IOU/DuplicateAction'; import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts index a45163910231..c98dbafbe8b2 100644 --- a/src/hooks/useDeleteTransactions.ts +++ b/src/hooks/useDeleteTransactions.ts @@ -1,7 +1,7 @@ import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {deleteMoneyRequest, getIOURequestPolicyID, initSplitExpenseItemData, updateSplitTransactions} from '@libs/actions/IOU'; -import {getIOUActionForTransactions} from '@libs/actions/IOU/DuplicateAction'; +import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getChildTransactions, getOriginalTransactionWithSplitInfo} from '@libs/TransactionUtils'; diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 0291873e3e24..6a640914a309 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -1,7 +1,8 @@ import {useCallback, useMemo, useState} from 'react'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import {useSearchContext} from '@components/Search/SearchContext'; -import {initSplitExpense, unholdRequest} from '@libs/actions/IOU'; +import {initSplitExpense} from '@libs/actions/IOU'; +import {unholdRequest} from '@libs/actions/IOU/Hold'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; import {exportReportToCSV} from '@libs/actions/Report'; import {getExportTemplates} from '@libs/actions/Search'; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f15613ff14ef..562e06b1fc6d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -95,9 +95,9 @@ import { setMoneyRequestReportID, startDistanceRequest, startMoneyRequest, - unholdRequest, } from './actions/IOU'; import type {IOURequestType} from './actions/IOU'; +import {unholdRequest} from './actions/IOU/Hold'; import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {createDraftWorkspace} from './actions/Policy/Policy'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; diff --git a/src/libs/actions/IOU/DuplicateAction.ts b/src/libs/actions/IOU/Duplicate.ts similarity index 100% rename from src/libs/actions/IOU/DuplicateAction.ts rename to src/libs/actions/IOU/Duplicate.ts diff --git a/src/libs/actions/IOU/Hold.ts b/src/libs/actions/IOU/Hold.ts new file mode 100644 index 000000000000..f2f776de6d9a --- /dev/null +++ b/src/libs/actions/IOU/Hold.ts @@ -0,0 +1,524 @@ +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import type {HoldMoneyRequestParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import DateUtils from '@libs/DateUtils'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +// eslint-disable-next-line @typescript-eslint/no-deprecated +import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils'; +import {getIOUActionForReportID} from '@libs/ReportActionsUtils'; +import type {Ancestor} from '@libs/ReportUtils'; +import { + buildOptimisticCreatedReportAction, + buildOptimisticHoldReportAction, + buildOptimisticHoldReportActionComment, + buildOptimisticUnHoldReportAction, + buildTransactionThread, + generateReportID, + getDisplayedReportID, + getOptimisticDataForAncestors, + getReportOrDraftReport, + isExpenseReport, +} from '@libs/ReportUtils'; +import {getAmount} from '@libs/TransactionUtils'; +import {notifyNewAction} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, ReportAction} from '@src/types/onyx'; +import {getAllReports, getAllTransactions, getAllTransactionViolations, getCurrentUserEmail, getUserAccountID} from '.'; + +/** + * Put expense on HOLD + */ +function putOnHold(transactionID: string, comment: string, initialReportID: string | undefined, ancestors: Ancestor[] = []) { + const allTransactions = getAllTransactions(); + const allTransactionViolations = getAllTransactionViolations(); + const allReports = getAllReports(); + const userAccountID = getUserAccountID(); + const currentUserEmail = getCurrentUserEmail(); + + const currentTime = DateUtils.getDBTime(); + const reportID = initialReportID ?? generateReportID(); + const createdReportAction = buildOptimisticHoldReportAction(currentTime); + const createdReportActionComment = buildOptimisticHoldReportActionComment(comment, DateUtils.addMillisecondsFromDateTime(currentTime, 1)); + const newViolation = {name: CONST.VIOLATIONS.HOLD, type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}; + const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; + const updatedViolations = [...transactionViolations, newViolation]; + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`]; + const iouAction = getIOUActionForReportID(transaction?.reportID, transactionID); + let transactionThreadReport: Report; + + // If there is no existing transaction thread report, we should create one + // This way we ensure every held request has a dedicated thread for comments + if (initialReportID) { + transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${initialReportID}`] ?? ({} as Report); + } else { + const moneyRequestReport = getReportOrDraftReport(transaction?.reportID); + transactionThreadReport = buildTransactionThread(iouAction, moneyRequestReport, undefined, reportID); + } + + const optimisticCreatedAction = buildOptimisticCreatedReportAction(currentUserEmail); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: createdReportAction as ReportAction, + [createdReportActionComment.reportActionID]: createdReportActionComment as ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + comment: { + hold: createdReportAction.reportActionID, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: updatedViolations, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastVisibleActionCreated: createdReportActionComment.created, + }, + }, + ]; + + if (iouReport && iouReport.currency === transaction?.currency) { + const isExpenseReportLocal = isExpenseReport(iouReport); + const coefficient = isExpenseReportLocal ? -1 : 1; + const transactionAmount = getAmount(transaction, isExpenseReportLocal) * coefficient; + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + unheldTotal: (iouReport.unheldTotal ?? 0) - transactionAmount, + unheldNonReimbursableTotal: !transaction?.reimbursable ? (iouReport.unheldNonReimbursableTotal ?? 0) - transactionAmount : iouReport.unheldNonReimbursableTotal, + }, + }); + } + + optimisticData.push(...getOptimisticDataForAncestors(ancestors, createdReportActionComment.created, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)); + + const successData: Array< + OnyxUpdate + > = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: null, + }, + }, + ]; + + const failureData: Array< + OnyxUpdate< + | typeof ONYXKEYS.COLLECTION.TRANSACTION + | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS + | typeof ONYXKEYS.COLLECTION.REPORT + | typeof ONYXKEYS.COLLECTION.REPORT_METADATA + | typeof ONYXKEYS.COLLECTION.NEXT_STEP + > + > = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: null, + comment: { + hold: null, + }, + errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericHoldExpenseFailureMessage'), + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: null, + [createdReportActionComment.reportActionID]: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastVisibleActionCreated: transactionThreadReport.lastVisibleActionCreated, + }, + }, + ]; + + // If the transaction thread report wasn't created before, we create it optimistically + if (!initialReportID) { + transactionThreadReport.pendingFields = { + createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: transactionThreadReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction}, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isOptimisticReport: true, + }, + }, + ); + + if (iouAction?.reportActionID) { + // We link the IOU action to the new transaction thread by setting childReportID optimistically + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.parentReportID}`, + value: {[iouAction?.reportActionID]: {childReportID: reportID, childType: CONST.REPORT.TYPE.CHAT}}, + }); + // We reset the childReportID if the request fails + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.parentReportID}`, + value: {[iouAction?.reportActionID]: {childReportID: null, childType: null}}, + }); + } + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: { + isOptimisticReport: false, + }, + }, + ); + + failureData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, + value: null, + }, + ); + } + + if (iouReport) { + // buildOptimisticNextStep is used in parallel + // eslint-disable-next-line @typescript-eslint/no-deprecated + const optimisticNextStepDeprecated = buildNextStepNew({ + report: iouReport, + predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, + shouldFixViolations: true, + currentUserAccountIDParam: userAccountID, + currentUserEmailParam: currentUserEmail, + }); + const optimisticNextStep = buildOptimisticNextStep({ + report: iouReport, + predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, + shouldFixViolations: true, + currentUserAccountIDParam: userAccountID, + currentUserEmailParam: currentUserEmail, + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: optimisticNextStepDeprecated, + }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + nextStep: optimisticNextStep, + pendingFields: { + nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + pendingFields: { + nextStep: null, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: null, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + nextStep: iouReport.nextStep ?? null, + pendingFields: { + nextStep: null, + }, + }, + }); + } + + const params: HoldMoneyRequestParams = { + transactionID, + comment, + reportActionID: createdReportAction.reportActionID, + commentReportActionID: createdReportActionComment.reportActionID, + }; + + if (!initialReportID) { + params.transactionThreadReportID = reportID; + params.createdReportActionIDForThread = optimisticCreatedAction.reportActionID; + } + + API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST, params, {optimisticData, successData, failureData}); + + const currentReportID = getDisplayedReportID(reportID); + Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID)); +} + +function putTransactionsOnHold(transactionsID: string[], comment: string, reportID: string, ancestors: Ancestor[] = []) { + for (const transactionID of transactionsID) { + const {childReportID} = getIOUActionForReportID(reportID, transactionID) ?? {}; + putOnHold(transactionID, comment, childReportID, ancestors); + } +} + +/** + * Remove expense from HOLD + */ +function unholdRequest(transactionID: string, reportID: string, policy: OnyxEntry) { + const allTransactions = getAllTransactions(); + const allTransactionViolations = getAllTransactionViolations(); + const allReports = getAllReports(); + const userAccountID = getUserAccountID(); + const currentUserEmail = getCurrentUserEmail(); + + const createdReportAction = buildOptimisticUnHoldReportAction(); + const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const updatedTransactionViolations = transactionViolations?.filter((violation) => violation.name !== CONST.VIOLATIONS.HOLD) ?? []; + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`]; + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + + const optimisticData: Array< + OnyxUpdate< + | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS + | typeof ONYXKEYS.COLLECTION.TRANSACTION + | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + | typeof ONYXKEYS.COLLECTION.REPORT + | typeof ONYXKEYS.COLLECTION.NEXT_STEP + > + > = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: createdReportAction as ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + comment: { + hold: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: updatedTransactionViolations, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastVisibleActionCreated: createdReportAction.created, + }, + }, + ]; + + if (iouReport && iouReport.currency === transaction?.currency) { + const isExpenseReportLocal = isExpenseReport(iouReport); + const coefficient = isExpenseReportLocal ? -1 : 1; + const transactionAmount = getAmount(transaction, isExpenseReportLocal) * coefficient; + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + unheldTotal: (iouReport.unheldTotal ?? 0) + transactionAmount, + unheldNonReimbursableTotal: !transaction?.reimbursable ? (iouReport.unheldNonReimbursableTotal ?? 0) + transactionAmount : iouReport.unheldNonReimbursableTotal, + }, + }); + } + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: null, + comment: { + hold: null, + }, + }, + }, + ]; + + const failureData: Array< + OnyxUpdate< + | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS + | typeof ONYXKEYS.COLLECTION.TRANSACTION + | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + | typeof ONYXKEYS.COLLECTION.REPORT + | typeof ONYXKEYS.COLLECTION.NEXT_STEP + > + > = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: null, + errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericUnholdExpenseFailureMessage'), + comment: { + hold: transaction?.comment?.hold, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: transactionViolations ?? null, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastVisibleActionCreated: report?.lastVisibleActionCreated, + }, + }, + ]; + + if (iouReport) { + // buildOptimisticNextStep is used in parallel + // eslint-disable-next-line @typescript-eslint/no-deprecated + const optimisticNextStepDeprecated = buildNextStepNew({ + report: iouReport, + policy, + predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, + shouldFixViolations: updatedTransactionViolations.length > 0, + currentUserAccountIDParam: userAccountID, + currentUserEmailParam: currentUserEmail, + }); + const optimisticNextStep = buildOptimisticNextStep({ + report: iouReport, + policy, + predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, + shouldFixViolations: updatedTransactionViolations.length > 0, + currentUserAccountIDParam: userAccountID, + currentUserEmailParam: currentUserEmail, + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: optimisticNextStepDeprecated, + }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + nextStep: optimisticNextStep, + pendingFields: { + nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + pendingFields: { + nextStep: null, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: null, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + nextStep: iouReport.nextStep ?? null, + pendingFields: { + nextStep: null, + }, + }, + }); + } + + API.write( + WRITE_COMMANDS.UNHOLD_MONEY_REQUEST, + { + transactionID, + reportActionID: createdReportAction.reportActionID, + }, + {optimisticData, successData, failureData}, + ); + + const currentReportID = getDisplayedReportID(reportID); + notifyNewAction(currentReportID, userAccountID); +} + +export {putOnHold, putTransactionsOnHold, unholdRequest}; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index c07865a07db4..01e383c39ecc 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -23,7 +23,6 @@ import type { CreateWorkspaceParams, DeleteMoneyRequestParams, DetachReceiptParams, - HoldMoneyRequestParams, MarkTransactionViolationAsResolvedParams, PayInvoiceParams, PayMoneyRequestParams, @@ -111,7 +110,7 @@ import { isMoneyRequestAction, isReportPreviewAction, } from '@libs/ReportActionsUtils'; -import type {Ancestor, OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; +import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import { buildOptimisticActionableTrackExpenseWhisper, buildOptimisticAddCommentReportAction, @@ -123,8 +122,6 @@ import { buildOptimisticCreatedReportForUnapprovedAction, buildOptimisticDetachReceipt, buildOptimisticExpenseReport, - buildOptimisticHoldReportAction, - buildOptimisticHoldReportActionComment, buildOptimisticIOUReport, buildOptimisticIOUReportAction, buildOptimisticMarkedAsResolvedReportAction, @@ -139,8 +136,6 @@ import { buildOptimisticSelfDMReport, buildOptimisticSubmittedReportAction, buildOptimisticUnapprovedReportAction, - buildOptimisticUnHoldReportAction, - buildTransactionThread, canBeAutoReimbursed, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, findSelfDMReportID, @@ -151,7 +146,6 @@ import { getDisplayedReportID, getMoneyRequestSpendBreakdown, getNextApproverAccountID, - getOptimisticDataForAncestors, getOutstandingChildRequest, getParsedComment, getPersonalDetailsForAccountID, @@ -11903,486 +11897,6 @@ function adjustRemainingSplitShares(transaction: NonNullable - > = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - pendingAction: null, - }, - }, - ]; - - const failureData: Array< - OnyxUpdate< - | typeof ONYXKEYS.COLLECTION.TRANSACTION - | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - | typeof ONYXKEYS.COLLECTION.REPORT - | typeof ONYXKEYS.COLLECTION.REPORT_METADATA - | typeof ONYXKEYS.COLLECTION.NEXT_STEP - > - > = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - pendingAction: null, - comment: { - hold: null, - }, - errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericHoldExpenseFailureMessage'), - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [createdReportAction.reportActionID]: null, - [createdReportActionComment.reportActionID]: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - lastVisibleActionCreated: transactionThreadReport.lastVisibleActionCreated, - }, - }, - ]; - - // If the transaction thread report wasn't created before, we create it optimistically - if (!initialReportID) { - transactionThreadReport.pendingFields = { - createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }; - optimisticData.push( - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: transactionThreadReport, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction}, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isOptimisticReport: true, - }, - }, - ); - - if (iouAction?.reportActionID) { - // We link the IOU action to the new transaction thread by setting childReportID optimistically - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.parentReportID}`, - value: {[iouAction?.reportActionID]: {childReportID: reportID, childType: CONST.REPORT.TYPE.CHAT}}, - }); - // We reset the childReportID if the request fails - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.parentReportID}`, - value: {[iouAction?.reportActionID]: {childReportID: null, childType: null}}, - }); - } - - successData.push( - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: { - isOptimisticReport: false, - }, - }, - ); - - failureData.push( - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, - value: null, - }, - ); - } - - if (iouReport) { - // buildOptimisticNextStep is used in parallel - // eslint-disable-next-line @typescript-eslint/no-deprecated - const optimisticNextStepDeprecated = buildNextStepNew({ - report: iouReport, - predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, - shouldFixViolations: true, - currentUserAccountIDParam: userAccountID, - currentUserEmailParam: currentUserEmail, - }); - const optimisticNextStep = buildOptimisticNextStep({ - report: iouReport, - predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, - shouldFixViolations: true, - currentUserAccountIDParam: userAccountID, - currentUserEmailParam: currentUserEmail, - }); - - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, - value: optimisticNextStepDeprecated, - }); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - nextStep: optimisticNextStep, - pendingFields: { - nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, - }); - - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - pendingFields: { - nextStep: null, - }, - }, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, - value: null, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - nextStep: iouReport.nextStep ?? null, - pendingFields: { - nextStep: null, - }, - }, - }); - } - - const params: HoldMoneyRequestParams = { - transactionID, - comment, - reportActionID: createdReportAction.reportActionID, - commentReportActionID: createdReportActionComment.reportActionID, - }; - - if (!initialReportID) { - params.transactionThreadReportID = reportID; - params.createdReportActionIDForThread = optimisticCreatedAction.reportActionID; - } - - API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST, params, {optimisticData, successData, failureData}); - - const currentReportID = getDisplayedReportID(reportID); - Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID)); -} - -function putTransactionsOnHold(transactionsID: string[], comment: string, reportID: string, ancestors: Ancestor[] = []) { - for (const transactionID of transactionsID) { - const {childReportID} = getIOUActionForReportID(reportID, transactionID) ?? {}; - putOnHold(transactionID, comment, childReportID, ancestors); - } -} - -/** - * Remove expense from HOLD - */ -function unholdRequest(transactionID: string, reportID: string, policy: OnyxEntry) { - const createdReportAction = buildOptimisticUnHoldReportAction(); - const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - const updatedTransactionViolations = transactionViolations?.filter((violation) => violation.name !== CONST.VIOLATIONS.HOLD) ?? []; - const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`]; - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - - const optimisticData: Array< - OnyxUpdate< - | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - | typeof ONYXKEYS.COLLECTION.TRANSACTION - | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS - | typeof ONYXKEYS.COLLECTION.REPORT - | typeof ONYXKEYS.COLLECTION.NEXT_STEP - > - > = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [createdReportAction.reportActionID]: createdReportAction as ReportAction, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - comment: { - hold: null, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: updatedTransactionViolations, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - lastVisibleActionCreated: createdReportAction.created, - }, - }, - ]; - - if (iouReport && iouReport.currency === transaction?.currency) { - const isExpenseReportLocal = isExpenseReport(iouReport); - const coefficient = isExpenseReportLocal ? -1 : 1; - const transactionAmount = getAmount(transaction, isExpenseReportLocal) * coefficient; - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - unheldTotal: (iouReport.unheldTotal ?? 0) + transactionAmount, - unheldNonReimbursableTotal: !transaction?.reimbursable ? (iouReport.unheldNonReimbursableTotal ?? 0) + transactionAmount : iouReport.unheldNonReimbursableTotal, - }, - }); - } - - const successData: Array> = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - pendingAction: null, - comment: { - hold: null, - }, - }, - }, - ]; - - const failureData: Array< - OnyxUpdate< - | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS - | typeof ONYXKEYS.COLLECTION.TRANSACTION - | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS - | typeof ONYXKEYS.COLLECTION.REPORT - | typeof ONYXKEYS.COLLECTION.NEXT_STEP - > - > = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - [createdReportAction.reportActionID]: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - pendingAction: null, - errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericUnholdExpenseFailureMessage'), - comment: { - hold: transaction?.comment?.hold, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: transactionViolations ?? null, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - lastVisibleActionCreated: report?.lastVisibleActionCreated, - }, - }, - ]; - - if (iouReport) { - // buildOptimisticNextStep is used in parallel - // eslint-disable-next-line @typescript-eslint/no-deprecated - const optimisticNextStepDeprecated = buildNextStepNew({ - report: iouReport, - policy, - predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, - shouldFixViolations: updatedTransactionViolations.length > 0, - currentUserAccountIDParam: userAccountID, - currentUserEmailParam: currentUserEmail, - }); - const optimisticNextStep = buildOptimisticNextStep({ - report: iouReport, - policy, - predictedNextStatus: iouReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, - shouldFixViolations: updatedTransactionViolations.length > 0, - currentUserAccountIDParam: userAccountID, - currentUserEmailParam: currentUserEmail, - }); - - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, - value: optimisticNextStepDeprecated, - }); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - nextStep: optimisticNextStep, - pendingFields: { - nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - }, - }); - - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - pendingFields: { - nextStep: null, - }, - }, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, - value: null, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - nextStep: iouReport.nextStep ?? null, - pendingFields: { - nextStep: null, - }, - }, - }); - } - - API.write( - WRITE_COMMANDS.UNHOLD_MONEY_REQUEST, - { - transactionID, - reportActionID: createdReportAction.reportActionID, - }, - {optimisticData, successData, failureData}, - ); - - const currentReportID = getDisplayedReportID(reportID); - notifyNewAction(currentReportID, userAccountID); -} - // eslint-disable-next-line rulesdir/no-negated-variables function navigateToStartStepIfScanFileCannotBeRead( receiptFilename: string | undefined, @@ -14558,8 +14072,6 @@ export { convertBulkTrackedExpensesToIOU, payInvoice, payMoneyRequest, - putOnHold, - putTransactionsOnHold, replaceReceipt, requestMoney, resetSplitShares, @@ -14604,7 +14116,6 @@ export { submitReport, trackExpense, unapproveExpenseReport, - unholdRequest, updateMoneyRequestAttendees, updateMoneyRequestAmountAndCurrency, updateMoneyRequestReimbursable, diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 3c426d80871e..468cafd5c174 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -5,13 +5,13 @@ import useAncestors from '@hooks/useAncestors'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {clearErrorFields, clearErrors} from '@libs/actions/FormActions'; +import {putTransactionsOnHold} from '@libs/actions/IOU/Hold'; import {holdMoneyRequestOnSearch} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import type {SearchReportActionsParamList} from '@navigation/types'; import HoldReasonFormView from '@pages/iou/HoldReasonFormView'; -import {putTransactionsOnHold} from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx index 3843ff00593b..a923c5c75b12 100644 --- a/src/pages/TransactionDuplicate/Confirmation.tsx +++ b/src/pages/TransactionDuplicate/Confirmation.tsx @@ -18,7 +18,7 @@ import useOnyx from '@hooks/useOnyx'; import useReviewDuplicatesNavigation from '@hooks/useReviewDuplicatesNavigation'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsByID from '@hooks/useTransactionsByID'; -import {mergeDuplicates, resolveDuplicates} from '@libs/actions/IOU/DuplicateAction'; +import {mergeDuplicates, resolveDuplicates} from '@libs/actions/IOU/Duplicate'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index f26c976a554e..cca028e70168 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -3,6 +3,7 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import useAncestors from '@hooks/useAncestors'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import {putOnHold} from '@libs/actions/IOU/Hold'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -11,7 +12,6 @@ import {getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {canEditMoneyRequest, isReportInGroupPolicy} from '@libs/ReportUtils'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import {clearErrorFields, clearErrors, setErrors} from '@userActions/FormActions'; -import {putOnHold} from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index d132a95d8d8e..b1c36fe24f97 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -34,7 +34,7 @@ import { updateSplitExpenseAmountField, updateSplitTransactionsFromSplitExpensesFlow, } from '@libs/actions/IOU'; -import {getIOUActionForTransactions} from '@libs/actions/IOU/DuplicateAction'; +import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate'; import {convertToBackendAmount, convertToDisplayString} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 1d829ca82b25..3bb6e4b79ec3 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -31,7 +31,6 @@ import { initSplitExpense, markRejectViolationAsResolved, payMoneyRequest, - putOnHold, rejectMoneyRequest, replaceReceipt, requestMoney, @@ -44,7 +43,6 @@ import { submitPerDiemExpense, submitReport, trackExpense, - unholdRequest, updateMoneyRequestAmountAndCurrency, updateMoneyRequestAttendees, updateMoneyRequestCategory, @@ -52,6 +50,7 @@ import { updateSplitExpenseAmountField, updateSplitTransactionsFromSplitExpensesFlow, } from '@libs/actions/IOU'; +import {putOnHold} from '@libs/actions/IOU/Hold'; import {getSendInvoiceInformation} from '@libs/actions/IOU/SendInvoice'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {createWorkspace, deleteWorkspace, generatePolicyID, setWorkspaceApprovalMode} from '@libs/actions/Policy/Policy'; @@ -60,22 +59,12 @@ import {clearAllRelatedReportActionErrors} from '@libs/actions/ReportActions'; import {subscribeToUserEvents} from '@libs/actions/User'; import type {ApiCommand} from '@libs/API/types'; import {WRITE_COMMANDS} from '@libs/API/types'; -import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {rand64} from '@libs/NumberUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; // eslint-disable-next-line no-restricted-syntax import type * as PolicyUtils from '@libs/PolicyUtils'; -import { - getOriginalMessage, - getReportActionHtml, - getReportActionMessage, - getReportActionText, - getSortedReportActions, - isActionableTrackExpense, - isActionOfType, - isMoneyRequestAction, -} from '@libs/ReportActionsUtils'; +import {getOriginalMessage, getReportActionHtml, getReportActionMessage, getReportActionText, isActionableTrackExpense, isActionOfType, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import type {OptimisticChatReport} from '@libs/ReportUtils'; import {buildOptimisticIOUReport, buildOptimisticIOUReportAction, buildTransactionThread, createDraftTransactionAndNavigateToParticipantSelector, isIOUReport} from '@libs/ReportUtils'; import {buildOptimisticTransaction, getValidWaypoints, isDistanceRequest as isDistanceRequestUtil} from '@libs/TransactionUtils'; @@ -90,7 +79,7 @@ import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, PolicyTagLists, RecentlyUsedTags, Report, ReportNameValuePairs, SearchResults} from '@src/types/onyx'; import type {Accountant, Attendee, SplitExpense} from '@src/types/onyx/IOU'; import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; -import type {Participant, ReportCollectionDataSet} from '@src/types/onyx/Report'; +import type {Participant} from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {ReportActions, ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; import type Transaction from '@src/types/onyx/Transaction'; @@ -6465,267 +6454,6 @@ describe('actions/IOU', () => { }); }); - describe('putOnHold', () => { - test("should update the transaction thread report's lastVisibleActionCreated to the optimistically added hold comment report action created timestamp", () => { - const iouReport = buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); - const transaction = buildOptimisticTransaction({ - transactionParams: { - amount: 100, - currency: 'USD', - reportID: iouReport.reportID, - }, - }); - - const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - const iouAction: ReportAction = buildOptimisticIOUReportAction({ - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - amount: transaction.amount, - currency: transaction.currency, - comment: '', - participants: [], - transactionID: transaction.transactionID, - }); - const transactionThread = buildTransactionThread(iouAction, iouReport); - - const actions: OnyxInputValue = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouAction.reportActionID}`]: iouAction}; - const reportCollectionDataSet: ReportCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`]: transactionThread, - [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, - }; - const actionCollectionDataSet: ReportActionsCollectionDataSet = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions}; - const comment = 'hold reason'; - - return waitForBatchedUpdates() - .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) - .then(() => { - // When an expense is put on hold - putOnHold(transaction.transactionID, comment, transactionThread.reportID); - return waitForBatchedUpdates(); - }) - .then(() => { - return new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`, - callback: (report) => { - Onyx.disconnect(connection); - const lastVisibleActionCreated = report?.lastVisibleActionCreated; - const connection2 = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`, - callback: (reportActions) => { - Onyx.disconnect(connection2); - resolve(); - const lastAction = getSortedReportActions(Object.values(reportActions ?? {}), true).at(0); - const message = getReportActionMessage(lastAction); - // Then the transaction thread report lastVisibleActionCreated should equal the hold comment action created timestamp. - expect(message?.text).toBe(comment); - expect(lastVisibleActionCreated).toBe(lastAction?.created); - }, - }); - }, - }); - }); - }); - }); - - test('should create transaction thread optimistically when initialReportID is undefined', () => { - const iouReport = buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); - const transaction = buildOptimisticTransaction({ - transactionParams: { - amount: 100, - currency: 'USD', - reportID: iouReport.reportID, - }, - }); - const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - const iouAction: ReportAction = buildOptimisticIOUReportAction({ - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - amount: transaction.amount, - currency: transaction.currency, - comment: '', - participants: [], - transactionID: transaction.transactionID, - }); - const actions: OnyxInputValue = {[iouAction.reportActionID]: iouAction}; - const reportCollectionDataSet: ReportCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, - }; - const actionCollectionDataSet: ReportActionsCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions, - }; - const comment = 'hold reason for new thread'; - - return waitForBatchedUpdates() - .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) - .then(() => { - // When an expense is put on hold without existing transaction thread (undefined initialReportID) - putOnHold(transaction.transactionID, comment, undefined); - return waitForBatchedUpdates(); - }) - .then(() => { - return new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, - callback: (reportActions) => { - Onyx.disconnect(connection); - const updatedIOUAction = reportActions?.[iouAction.reportActionID]; - // Verify that IOU action now has childReportID set optimistically - expect(updatedIOUAction?.childReportID).toBeDefined(); - resolve(); - }, - }); - }); - }); - }); - }); - - describe('unHoldRequest', () => { - test("should update the transaction thread report's lastVisibleActionCreated to the optimistically added unhold report action created timestamp", () => { - const policyID = '577'; - const policy: Policy = { - ...createRandomPolicy(Number(policyID)), - }; - const iouReport: Report = { - ...buildOptimisticIOUReport(1, 2, 100, '1', 'USD'), - policyID, - }; - const transaction = buildOptimisticTransaction({ - transactionParams: { - amount: 100, - currency: 'USD', - reportID: iouReport.reportID, - }, - }); - - const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - const iouAction: ReportAction = buildOptimisticIOUReportAction({ - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - amount: transaction.amount, - currency: transaction.currency, - comment: '', - participants: [], - transactionID: transaction.transactionID, - }); - const transactionThread = buildTransactionThread(iouAction, iouReport); - - const actions: OnyxInputValue = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouAction.reportActionID}`]: iouAction}; - const reportCollectionDataSet: ReportCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`]: transactionThread, - [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, - }; - const actionCollectionDataSet: ReportActionsCollectionDataSet = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions}; - const comment = 'hold reason'; - - return waitForBatchedUpdates() - .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) - .then(() => { - putOnHold(transaction.transactionID, comment, transactionThread.reportID); - return waitForBatchedUpdates(); - }) - .then(() => { - // When an expense is unhold - unholdRequest(transaction.transactionID, transactionThread.reportID, policy); - return waitForBatchedUpdates(); - }) - .then(() => { - return new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`, - callback: (report) => { - Onyx.disconnect(connection); - const lastVisibleActionCreated = report?.lastVisibleActionCreated; - const connection2 = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`, - callback: (reportActions) => { - Onyx.disconnect(connection2); - resolve(); - const lastAction = getSortedReportActions(Object.values(reportActions ?? {}), true).at(0); - // Then the transaction thread report lastVisibleActionCreated should equal the unhold action created timestamp. - expect(lastAction?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.UNHOLD); - expect(lastVisibleActionCreated).toBe(lastAction?.created); - }, - }); - }, - }); - }); - }); - }); - - test('should rollback unhold request on API failure', () => { - const policyID = '577'; - const policy: Policy = { - ...createRandomPolicy(Number(policyID)), - }; - const iouReport: Report = { - ...buildOptimisticIOUReport(1, 2, 100, '1', 'USD'), - policyID, - }; - const transaction = buildOptimisticTransaction({ - transactionParams: { - amount: 100, - currency: 'USD', - reportID: iouReport.reportID, - }, - }); - - const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - const iouAction: ReportAction = buildOptimisticIOUReportAction({ - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - amount: transaction.amount, - currency: transaction.currency, - comment: '', - participants: [], - transactionID: transaction.transactionID, - }); - const transactionThread = buildTransactionThread(iouAction, iouReport); - - const actions: OnyxInputValue = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouAction.reportActionID}`]: iouAction}; - const reportCollectionDataSet: ReportCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`]: transactionThread, - [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, - }; - const actionCollectionDataSet: ReportActionsCollectionDataSet = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions}; - const comment = 'hold reason'; - - return waitForBatchedUpdates() - .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) - .then(() => { - putOnHold(transaction.transactionID, comment, transactionThread.reportID); - return waitForBatchedUpdates(); - }) - .then(() => { - mockFetch.fail(); - mockFetch?.resume?.(); - unholdRequest(transaction.transactionID, transactionThread.reportID, policy); - return waitForBatchedUpdates(); - }) - .then(() => { - return new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - callback: (updatedTransaction) => { - Onyx.disconnect(connection); - expect(updatedTransaction?.pendingAction).toBeFalsy(); - expect(updatedTransaction?.comment?.hold).toBeTruthy(); - expect(Object.values(updatedTransaction?.errors ?? {})).toEqual( - Object.values(getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericUnholdExpenseFailureMessage') ?? {}), - ); - - resolve(); - }, - }); - }); - }); - }); - }); - describe('canIOUBePaid', () => { it('For invoices from archived workspaces', async () => { const {policy, convertedInvoiceChat: chatReport}: InvoiceTestData = InvoiceData; diff --git a/tests/actions/IOUTest/DuplicateActionTest.ts b/tests/actions/IOUTest/DuplicateTest.ts similarity index 99% rename from tests/actions/IOUTest/DuplicateActionTest.ts rename to tests/actions/IOUTest/DuplicateTest.ts index 3eb0ab8347dc..8aafaeb5c1e0 100644 --- a/tests/actions/IOUTest/DuplicateActionTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import type {OnyxEntry, OnyxInputValue} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import {duplicateExpenseTransaction, mergeDuplicates, resolveDuplicates} from '@libs/actions/IOU/DuplicateAction'; +import {duplicateExpenseTransaction, mergeDuplicates, resolveDuplicates} from '@libs/actions/IOU/Duplicate'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {WRITE_COMMANDS} from '@libs/API/types'; import {getOriginalMessage} from '@libs/ReportActionsUtils'; @@ -62,7 +62,7 @@ const RORY_EMAIL = 'rory@expensifail.com'; const RORY_ACCOUNT_ID = 3; OnyxUpdateManager(); -describe('actions/DuplicateAction', () => { +describe('actions/Duplicate', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, diff --git a/tests/actions/IOUTest/HoldTest.ts b/tests/actions/IOUTest/HoldTest.ts new file mode 100644 index 000000000000..b7f65f9a25cb --- /dev/null +++ b/tests/actions/IOUTest/HoldTest.ts @@ -0,0 +1,356 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import Onyx from 'react-native-onyx'; +import type {OnyxEntry, OnyxInputValue} from 'react-native-onyx'; +import {putOnHold, unholdRequest} from '@libs/actions/IOU/Hold'; +import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +// eslint-disable-next-line no-restricted-syntax +import type * as PolicyUtils from '@libs/PolicyUtils'; +import {getReportActionMessage, getSortedReportActions} from '@libs/ReportActionsUtils'; +import {buildOptimisticIOUReport, buildOptimisticIOUReportAction, buildTransactionThread} from '@libs/ReportUtils'; +import {buildOptimisticTransaction} from '@libs/TransactionUtils'; +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; +import type {ReportCollectionDataSet} from '@src/types/onyx/Report'; +import type ReportAction from '@src/types/onyx/ReportAction'; +import type {ReportActions, ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; +import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; +import createRandomPolicy from '../../utils/collections/policies'; +import type {MockFetch} from '../../utils/TestHelper'; +import {getGlobalFetchMock} from '../../utils/TestHelper'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const topMostReportID = '23423423'; +jest.mock('@src/libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + dismissModal: jest.fn(), + dismissModalWithReport: jest.fn(), + goBack: jest.fn(), + getTopmostReportId: jest.fn(() => topMostReportID), + setNavigationActionToMicrotaskQueue: jest.fn(), + removeScreenByKey: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + getReportRouteByID: jest.fn(), + getActiveRouteWithoutParams: jest.fn(), + getActiveRoute: jest.fn(), + navigationRef: { + getRootState: jest.fn(), + }, +})); + +jest.mock('@react-navigation/native'); + +jest.mock('@src/libs/actions/Report', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const originalModule = jest.requireActual('@src/libs/actions/Report'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...originalModule, + notifyNewAction: jest.fn(), + }; +}); + +jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); + +jest.mock('@libs/PolicyUtils', () => ({ + ...jest.requireActual('@libs/PolicyUtils'), + isPaidGroupPolicy: jest.fn().mockReturnValue(true), + isPolicyOwner: jest.fn().mockImplementation((policy?: OnyxEntry, currentUserAccountID?: number) => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID), +})); + +const RORY_EMAIL = 'rory@expensifail.com'; +const RORY_ACCOUNT_ID = 3; + +OnyxUpdateManager(); + +describe('actions/IOU/Hold', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL}, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}}, + }, + }); + initOnyxDerivedValues(); + IntlStore.load(CONST.LOCALES.EN); + return waitForBatchedUpdates(); + }); + + let mockFetch: MockFetch; + beforeEach(() => { + jest.clearAllTimers(); + global.fetch = getGlobalFetchMock(); + mockFetch = fetch as MockFetch; + return Onyx.clear().then(waitForBatchedUpdates); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('putOnHold', () => { + test("should update the transaction thread report's lastVisibleActionCreated to the optimistically added hold comment report action created timestamp", () => { + const iouReport = buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); + const transaction = buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + const iouAction: ReportAction = buildOptimisticIOUReportAction({ + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: transaction.amount, + currency: transaction.currency, + comment: '', + participants: [], + transactionID: transaction.transactionID, + }); + const transactionThread = buildTransactionThread(iouAction, iouReport); + + const actions: OnyxInputValue = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouAction.reportActionID}`]: iouAction}; + const reportCollectionDataSet: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`]: transactionThread, + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + }; + const actionCollectionDataSet: ReportActionsCollectionDataSet = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions}; + const comment = 'hold reason'; + + return waitForBatchedUpdates() + .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) + .then(() => { + // When an expense is put on hold + putOnHold(transaction.transactionID, comment, transactionThread.reportID); + return waitForBatchedUpdates(); + }) + .then(() => { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`, + callback: (report) => { + Onyx.disconnect(connection); + const lastVisibleActionCreated = report?.lastVisibleActionCreated; + const connection2 = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`, + callback: (reportActions) => { + Onyx.disconnect(connection2); + resolve(); + const lastAction = getSortedReportActions(Object.values(reportActions ?? {}), true).at(0); + const message = getReportActionMessage(lastAction); + // Then the transaction thread report lastVisibleActionCreated should equal the hold comment action created timestamp. + expect(message?.text).toBe(comment); + expect(lastVisibleActionCreated).toBe(lastAction?.created); + }, + }); + }, + }); + }); + }); + }); + + test('should create transaction thread optimistically when initialReportID is undefined', () => { + const iouReport = buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); + const transaction = buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + const iouAction: ReportAction = buildOptimisticIOUReportAction({ + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: transaction.amount, + currency: transaction.currency, + comment: '', + participants: [], + transactionID: transaction.transactionID, + }); + const actions: OnyxInputValue = {[iouAction.reportActionID]: iouAction}; + const reportCollectionDataSet: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + }; + const actionCollectionDataSet: ReportActionsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions, + }; + const comment = 'hold reason for new thread'; + + return waitForBatchedUpdates() + .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) + .then(() => { + // When an expense is put on hold without existing transaction thread (undefined initialReportID) + putOnHold(transaction.transactionID, comment, undefined); + return waitForBatchedUpdates(); + }) + .then(() => { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + const updatedIOUAction = reportActions?.[iouAction.reportActionID]; + // Verify that IOU action now has childReportID set optimistically + expect(updatedIOUAction?.childReportID).toBeDefined(); + resolve(); + }, + }); + }); + }); + }); + }); + + describe('unholdRequest', () => { + test("should update the transaction thread report's lastVisibleActionCreated to the optimistically added unhold report action created timestamp", () => { + const policyID = '577'; + const policy: Policy = { + ...createRandomPolicy(Number(policyID)), + }; + const iouReport: Report = { + ...buildOptimisticIOUReport(1, 2, 100, '1', 'USD'), + policyID, + }; + const transaction = buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + const iouAction: ReportAction = buildOptimisticIOUReportAction({ + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: transaction.amount, + currency: transaction.currency, + comment: '', + participants: [], + transactionID: transaction.transactionID, + }); + const transactionThread = buildTransactionThread(iouAction, iouReport); + + const actions: OnyxInputValue = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouAction.reportActionID}`]: iouAction}; + const reportCollectionDataSet: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`]: transactionThread, + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + }; + const actionCollectionDataSet: ReportActionsCollectionDataSet = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions}; + const comment = 'hold reason'; + + return waitForBatchedUpdates() + .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) + .then(() => { + putOnHold(transaction.transactionID, comment, transactionThread.reportID); + return waitForBatchedUpdates(); + }) + .then(() => { + // When an expense is unhold + unholdRequest(transaction.transactionID, transactionThread.reportID, policy); + return waitForBatchedUpdates(); + }) + .then(() => { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`, + callback: (report) => { + Onyx.disconnect(connection); + const lastVisibleActionCreated = report?.lastVisibleActionCreated; + const connection2 = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`, + callback: (reportActions) => { + Onyx.disconnect(connection2); + resolve(); + const lastAction = getSortedReportActions(Object.values(reportActions ?? {}), true).at(0); + // Then the transaction thread report lastVisibleActionCreated should equal the unhold action created timestamp. + expect(lastAction?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.UNHOLD); + expect(lastVisibleActionCreated).toBe(lastAction?.created); + }, + }); + }, + }); + }); + }); + }); + + test('should rollback unhold request on API failure', () => { + const policyID = '577'; + const policy: Policy = { + ...createRandomPolicy(Number(policyID)), + }; + const iouReport: Report = { + ...buildOptimisticIOUReport(1, 2, 100, '1', 'USD'), + policyID, + }; + const transaction = buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + const iouAction: ReportAction = buildOptimisticIOUReportAction({ + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount: transaction.amount, + currency: transaction.currency, + comment: '', + participants: [], + transactionID: transaction.transactionID, + }); + const transactionThread = buildTransactionThread(iouAction, iouReport); + + const actions: OnyxInputValue = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouAction.reportActionID}`]: iouAction}; + const reportCollectionDataSet: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`]: transactionThread, + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + }; + const actionCollectionDataSet: ReportActionsCollectionDataSet = {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`]: actions}; + const comment = 'hold reason'; + + return waitForBatchedUpdates() + .then(() => Onyx.multiSet({...reportCollectionDataSet, ...transactionCollectionDataSet, ...actionCollectionDataSet})) + .then(() => { + putOnHold(transaction.transactionID, comment, transactionThread.reportID); + return waitForBatchedUpdates(); + }) + .then(() => { + mockFetch.fail(); + mockFetch?.resume?.(); + unholdRequest(transaction.transactionID, transactionThread.reportID, policy); + return waitForBatchedUpdates(); + }) + .then(() => { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + callback: (updatedTransaction) => { + Onyx.disconnect(connection); + expect(updatedTransaction?.pendingAction).toBeFalsy(); + expect(updatedTransaction?.comment?.hold).toBeTruthy(); + expect(Object.values(updatedTransaction?.errors ?? {})).toEqual( + Object.values(getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericUnholdExpenseFailureMessage') ?? {}), + ); + + resolve(); + }, + }); + }); + }); + }); + }); +}); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 7f478374558e..5e24182ddb7b 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -8,7 +8,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import usePolicyData from '@hooks/usePolicyData'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import {putOnHold} from '@libs/actions/IOU'; +import {putOnHold} from '@libs/actions/IOU/Hold'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow'; import {convertToDisplayString} from '@libs/CurrencyUtils'; diff --git a/tests/unit/hooks/useSelectedTransactionsActions.test.ts b/tests/unit/hooks/useSelectedTransactionsActions.test.ts index d3c8ccfd682f..f5043ae58930 100644 --- a/tests/unit/hooks/useSelectedTransactionsActions.test.ts +++ b/tests/unit/hooks/useSelectedTransactionsActions.test.ts @@ -4,7 +4,8 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {SelectedTransactions} from '@components/Search/types'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; -import {initSplitExpense, unholdRequest} from '@libs/actions/IOU'; +import {initSplitExpense} from '@libs/actions/IOU'; +import {unholdRequest} from '@libs/actions/IOU/Hold'; import {setupMergeTransactionDataAndNavigate} from '@libs/actions/MergeTransaction'; import {exportReportToCSV} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; @@ -30,6 +31,9 @@ jest.mock('@libs/actions/Search', () => ({ jest.mock('@libs/actions/IOU', () => ({ initSplitExpense: jest.fn(), +})); + +jest.mock('@libs/actions/IOU/Hold', () => ({ unholdRequest: jest.fn(), }));