diff --git a/src/libs/API/parameters/BulkHoldRequestParams.ts b/src/libs/API/parameters/BulkHoldRequestParams.ts new file mode 100644 index 0000000000000..87a1f79bc342b --- /dev/null +++ b/src/libs/API/parameters/BulkHoldRequestParams.ts @@ -0,0 +1,6 @@ +type BulkHoldRequestParams = { + holdData: string; // This is a json object with shape Record; + comment: string; +}; + +export default BulkHoldRequestParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9122418f3430c..1a12145a6de88 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -172,6 +172,7 @@ export type {default as SubmitReportParams} from './SubmitReportParams'; export type {default as DetachReceiptParams} from './DetachReceiptParams'; export type {default as PayMoneyRequestParams} from './PayMoneyRequestParams'; export type {default as HoldMoneyRequestParams} from './HoldMoneyRequestParams'; +export type {default as BulkHoldRequestParams} from './BulkHoldRequestParams'; export type {default as UnHoldMoneyRequestParams} from './UnHoldMoneyRequestParams'; export type {default as CancelPaymentParams} from './CancelPaymentParams'; export type {default as AcceptACHContractForBankAccount} from './AcceptACHContractForBankAccount'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 5b9cf8df95c7f..e9dcdbef7c2f9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -202,6 +202,7 @@ const WRITE_COMMANDS = { UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription', UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY: 'UpdateMoneyRequestAmountAndCurrency', HOLD_MONEY_REQUEST: 'HoldRequest', + BULK_HOLD_MONEY_REQUEST: 'BulkHoldRequest', UPDATE_BILLING_CARD_CURRENCY: 'UpdateBillingCardCurrency', UNHOLD_MONEY_REQUEST: 'UnHoldRequest', REQUEST_MONEY: 'RequestMoney', @@ -674,6 +675,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST]: Parameters.HoldMoneyRequestParams; + [WRITE_COMMANDS.BULK_HOLD_MONEY_REQUEST]: Parameters.BulkHoldRequestParams; [WRITE_COMMANDS.UNHOLD_MONEY_REQUEST]: Parameters.UnHoldMoneyRequestParams; [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY]: Parameters.UpdateMoneyRequestParams; [WRITE_COMMANDS.REQUEST_MONEY]: Parameters.RequestMoneyParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d9d720c768a45..362df1fe6ff99 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -85,6 +85,7 @@ import { import { getAllReportActions, getIOUActionForReportID, + getIOUActionForTransactionID, getIOUReportIDFromReportActionPreview, getLastVisibleAction, getLastVisibleMessage, @@ -360,6 +361,13 @@ type PayMoneyRequestData = { failureData: OnyxUpdate[]; }; +type HoldDataEntry = { + transactionThreadReportID?: string; + transactionThreadCreatedReportActionID?: string; + holdReportActionID: string; + commentReportActionID: string; +}; + type SendMoneyParamsData = { params: SendMoneyParams; optimisticData: OnyxUpdate[]; @@ -10832,14 +10840,250 @@ function putOnHold(transactionID: string, comment: string, initialReportID: stri Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID)); } -function putTransactionsOnHold(transactionsID: string[], comment: string, reportID: string) { - transactionsID.forEach((transactionID) => { - const {childReportID} = getIOUActionForReportID(reportID, transactionID) ?? {}; - if (!childReportID) { +/** + * Put expenses on HOLD in bulk + */ +function bulkHold(transactionIDs: string[], comment: string, reportID: string, searchHash?: number) { + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const iouReportActions = Object.values(getAllReportActions(reportID)); + const iouReportOptimisticData: Partial = {}; + const holdData: Record = {}; + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + transactionIDs.forEach((transactionID) => { + const iouAction = getIOUActionForTransactionID(iouReportActions, transactionID); + + if (!iouAction) { return; } - putOnHold(transactionID, comment, childReportID); + + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + + if (iouReport && iouReport.currency === transaction?.currency) { + const isExpenseReportLocal = isExpenseReport(iouReport); + const coefficient = isExpenseReportLocal ? -1 : 1; + const transactionAmount = getAmount(transaction, isExpenseReportLocal) * coefficient; + iouReportOptimisticData.unheldTotal = (iouReportOptimisticData.unheldTotal ?? 0) - transactionAmount; + iouReportOptimisticData.unheldNonReimbursableTotal = !transaction?.reimbursable + ? (iouReportOptimisticData.unheldNonReimbursableTotal ?? 0) - transactionAmount + : iouReportOptimisticData.unheldNonReimbursableTotal; + } + + const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; + const newViolation = {name: CONST.VIOLATIONS.HOLD, type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}; + const updatedViolations = [...transactionViolations, newViolation]; + + const currentTime = DateUtils.getDBTime(); + const createdReportAction = buildOptimisticHoldReportAction(currentTime); + const createdReportActionComment = buildOptimisticHoldReportActionComment(comment, DateUtils.addMillisecondsFromDateTime(currentTime, 1)); + + holdData[transactionID] = { + holdReportActionID: createdReportAction.reportActionID, + commentReportActionID: createdReportActionComment.reportActionID, + }; + + let transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouAction?.childReportID}`]; + if (!transactionThreadReport) { + const optimisticTransactionThread = buildTransactionThread(iouAction, iouReport); + const optimisticCreatedActionForTransactionThread = buildOptimisticCreatedReportAction(currentUserEmail); + + // If the transactionThread is optimistic, we need the transactionThreadReportID and transactionThreadCreatedReportActionID in the holdData. + holdData[transactionID].transactionThreadReportID = optimisticTransactionThread.reportID; + holdData[transactionID].transactionThreadCreatedReportActionID = optimisticCreatedActionForTransactionThread.reportActionID; + + transactionThreadReport = optimisticTransactionThread; + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: {...optimisticTransactionThread, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: {[optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[iouAction.reportActionID]: {childReportID: optimisticTransactionThread.reportID}}, + }, + ); + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: {[optimisticCreatedActionForTransactionThread.reportActionID]: {pendingAction: null}}, + }, + ); + + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, + value: {[optimisticCreatedActionForTransactionThread.reportActionID]: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[iouAction.reportActionID]: {childReportID: null}}, + }, + ); + } + + const parentReportActionOptimistic = getOptimisticDataForParentReportAction( + transactionThreadReport.reportID, + createdReportActionComment.created, + CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + ); + + optimisticData.push( + ...parentReportActionOptimistic.filter((parentActionData): parentActionData is OnyxUpdate => parentActionData !== null), + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [createdReportAction.reportActionID]: createdReportAction as ReportAction, + [createdReportActionComment.reportActionID]: createdReportActionComment as ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + lastVisibleActionCreated: createdReportActionComment.created, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [createdReportAction.reportActionID]: null, + [createdReportActionComment.reportActionID]: null, + }, + }, + { + 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, + }, + ); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingAction: null, + }, + }); + + failureData.push( + { + 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}${transactionThreadReport.reportID}`, + value: { + lastVisibleActionCreated: transactionThreadReport?.lastVisibleActionCreated, + }, + }, + + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: transactionViolations, + }, + ); + + // If we are holding from the search page, we optimistically update the snapshot data that search uses so that it is kept in sync + if (searchHash) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + canHold: false, + canUnhold: true, + }, + }, + } as Record>>, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`, + value: { + data: { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { + canHold: true, + canUnhold: false, + }, + }, + } as Record>>, + }); + } }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: iouReportOptimisticData, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + unheldTotal: iouReport?.unheldTotal, + unheldNonReimbursableTotal: iouReport?.unheldNonReimbursableTotal, + }, + }); + + API.write( + WRITE_COMMANDS.BULK_HOLD_MONEY_REQUEST, + { + holdData: JSON.stringify(holdData), + comment, + }, + {optimisticData, successData, failureData}, + ); + + const currentReportID = getDisplayedReportID(reportID); + Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID)); } /** @@ -11899,7 +12143,7 @@ export { payInvoice, payMoneyRequest, putOnHold, - putTransactionsOnHold, + bulkHold, replaceReceipt, requestMoney, resetSplitShares, diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 8182f089e3f98..dd080c10af236 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -9,7 +9,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import type {SearchReportParamList} from '@navigation/types'; import HoldReasonFormView from '@pages/iou/HoldReasonFormView'; -import {putTransactionsOnHold} from '@userActions/IOU'; +import {bulkHold} from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; @@ -22,7 +22,7 @@ function SearchHoldReasonPage({route}: PlatformStackScreenProps) => { if (route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS) { - putTransactionsOnHold(context.selectedTransactionIDs, comment, reportID); + bulkHold(context.selectedTransactionIDs, comment, reportID, context.currentSearchHash); context.clearSelectedTransactions(true); } else { holdMoneyRequestOnSearch(context.currentSearchHash, Object.keys(context.selectedTransactions), comment);