diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index bf1a9f994c37..21e452de4730 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -224,7 +224,12 @@ function getOneTransactionThreadReportID(reportActions: OnyxEntry } // Get all IOU report actions for the report. - const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT, CONST.IOU.REPORT_ACTION_TYPE.PAY]; + const iouRequestTypes: Array> = [ + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + CONST.IOU.REPORT_ACTION_TYPE.SPLIT, + CONST.IOU.REPORT_ACTION_TYPE.PAY, + CONST.IOU.REPORT_ACTION_TYPE.TRACK, + ]; const iouRequestActions = reportActionsArray.filter( (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && @@ -346,7 +351,11 @@ function getMostRecentIOURequestActionID(reportActions: ReportAction[] | null): if (!Array.isArray(reportActions)) { return null; } - const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT]; + const iouRequestTypes: Array> = [ + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + CONST.IOU.REPORT_ACTION_TYPE.SPLIT, + CONST.IOU.REPORT_ACTION_TYPE.TRACK, + ]; const iouRequestActions = reportActions?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && iouRequestTypes.includes(action.originalMessage.type)) ?? []; if (iouRequestActions.length === 0) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 971d7103d3b4..c2bb77bd97ce 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -167,6 +167,7 @@ type OptimisticExpenseReport = Pick< | 'stateNum' | 'statusNum' | 'total' + | 'nonReimbursableTotal' | 'notificationPreference' | 'parentReportID' | 'lastVisibleActionCreated' @@ -3273,8 +3274,9 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe * @param payeeAccountID - AccountID of the employee (payee) * @param total - Amount in cents * @param currency + * @param reimbursable – Whether the expense is reimbursable */ -function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string): OptimisticExpenseReport { +function buildOptimisticExpenseReport(chatReportID: string, policyID: string, payeeAccountID: number, total: number, currency: string, reimbursable = true): OptimisticExpenseReport { // The amount for Expense reports are stored as negative value in the database const storedTotal = total * -1; const policyName = getPolicyName(allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]); @@ -3298,6 +3300,7 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa stateNum, statusNum, total: storedTotal, + nonReimbursableTotal: reimbursable ? 0 : storedTotal, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, parentReportID: chatReportID, lastVisibleActionCreated: DateUtils.getDBTime(), @@ -4242,6 +4245,7 @@ function buildOptimisticMoneyRequestEntities( isSendMoneyFlow = false, receipt: Receipt = {}, isOwnPolicyExpenseChat = false, + isPersonalTrackingExpense = false, ): [OptimisticCreatedReportAction, OptimisticCreatedReportAction, OptimisticIOUReportAction, OptimisticChatReport, OptimisticCreatedReportAction] { const createdActionForChat = buildOptimisticCreatedReportAction(payeeEmail); @@ -4256,7 +4260,7 @@ function buildOptimisticMoneyRequestEntities( participants, transactionID, paymentType, - iouReport.reportID, + isPersonalTrackingExpense ? '0' : iouReport.reportID, isSettlingUp, isSendMoneyFlow, receipt, @@ -4809,12 +4813,13 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o * - DMs * - Split options should show for: * - DMs - * - chat/ policy rooms with more than 1 participants + * - chat/policy rooms with more than 1 participant * - groups chats with 3 and more participants * - corporate workspace chats * - Track expense option should show for: * - Self DMs - * - admin rooms + * - own policy expense chats + * - open and processing expense reports tied to own policy expense chat * * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. @@ -4853,13 +4858,13 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry}> | undefined = undefined, + reimbursable = true, ): Transaction { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -124,6 +125,7 @@ function buildOptimisticTransaction( category, tag, billable, + reimbursable, }; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index caba0d28564c..540b9143ba6e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -802,10 +802,14 @@ function buildOnyxDataForMoneyRequest( /** Builds the Onyx data for track expense */ function buildOnyxDataForTrackExpense( chatReport: OnyxEntry, + iouReport: OnyxEntry, transaction: OnyxTypes.Transaction, + iouCreatedAction: OptimisticCreatedReportAction, iouAction: OptimisticIOUReportAction, + reportPreviewAction: OnyxEntry, transactionThreadReport: OptimisticChatReport, transactionThreadCreatedReportAction: OptimisticCreatedReportAction, + shouldCreateNewMoneyRequestReport: boolean, policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, @@ -823,22 +827,64 @@ function buildOnyxDataForTrackExpense( lastMessageText: iouAction.message?.[0].text, lastMessageHtml: iouAction.message?.[0].html, lastReadTime: DateUtils.getDBTime(), + iouReportID: iouReport?.reportID, }, }); } - optimisticData.push( - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - value: transaction, - }, - { + if (iouReport) { + optimisticData.push( + { + onyxMethod: shouldCreateNewMoneyRequestReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + ...iouReport, + lastMessageText: iouAction.message?.[0].text, + lastMessageHtml: iouAction.message?.[0].html, + pendingFields: { + ...(shouldCreateNewMoneyRequestReport ? {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + }, + }, + }, + shouldCreateNewMoneyRequestReport + ? { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + value: { + [iouCreatedAction.reportActionID]: iouCreatedAction as OnyxTypes.ReportAction, + [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + }, + } + : { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + value: { + [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + ...(reportPreviewAction && {[reportPreviewAction.reportActionID]: reportPreviewAction}), + }, + }, + ); + } else { + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, value: { [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, }, + }); + } + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: transaction, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -863,6 +909,56 @@ function buildOnyxDataForTrackExpense( const successData: OnyxUpdate[] = []; + if (iouReport) { + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: { + ...(shouldCreateNewMoneyRequestReport + ? { + [iouCreatedAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + } + : {}), + [iouAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + ...(reportPreviewAction && {[reportPreviewAction.reportActionID]: {pendingAction: null}}), + }, + }, + ); + } else { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + ...(reportPreviewAction && {[reportPreviewAction.reportActionID]: {pendingAction: null}}), + }, + }); + } + successData.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -882,27 +978,70 @@ function buildOnyxDataForTrackExpense( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { - [iouAction.reportActionID]: { + [transactionThreadCreatedReportAction.reportActionID]: { pendingAction: null, errors: null, }, }, }, - { + ); + + const failureData: OnyxUpdate[] = []; + + if (iouReport) { + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + pendingFields: null, + errorFields: { + ...(shouldCreateNewMoneyRequestReport ? {createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage')} : {}), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + value: { + ...(shouldCreateNewMoneyRequestReport + ? { + [iouCreatedAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), + }, + [iouAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + }, + } + : { + [iouAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), + }, + }), + }, + }, + ); + } else { + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: { - pendingAction: null, - errors: null, + [iouAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), }, }, - }, - ); + }); + } - const failureData: OnyxUpdate[] = [ + failureData.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, @@ -930,17 +1069,6 @@ function buildOnyxDataForTrackExpense( pendingFields: clearedPendingFields, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: { - [iouAction.reportActionID]: { - // Disabling this line since transaction.filename can be an empty string - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, @@ -950,7 +1078,7 @@ function buildOnyxDataForTrackExpense( }, }, }, - ]; + ); // We don't need to compute violations unless we're on a paid policy if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { @@ -1199,24 +1327,54 @@ function getTrackExpenseInformation( policyTagList: OnyxEntry | undefined, policyCategories: OnyxEntry | undefined, payeeEmail = currentUserEmail, + payeeAccountID = userAccountID, + moneyRequestReportID = '', ): TrackExpenseInformation | EmptyObject { + const isPolicyExpenseChat = participant.isPolicyExpenseChat; + // STEP 1: Get existing chat report let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; - // The chatReport always exist and we can get it from Onyx if chatReport is null. + // The chatReport always exists, and we can get it from Onyx if chatReport is null. if (!chatReport) { chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null; } - // If we still don't have a report, it likely doens't exist and we will early return here as it should not happen + // If we still don't have a report, it likely doesn't exist, and we will early return here as it should not happen // Maybe later, we can build an optimistic selfDM chat. if (!chatReport) { return {}; } - // STEP 2: Get the money request report. - // TODO: This is deferred to later as we are not sure if we create iouReport at all in future. - // We can build an optimistic iouReport here if needed. + // STEP 2: If not in the self-DM flow, we need to use the money request report. + // For this, first use the chatReport.iouReportID property. Build a new optimistic money request report if needed. + const shouldUseMoneyReport = !!isPolicyExpenseChat; + + let iouReport: OnyxEntry = null; + let shouldCreateNewMoneyRequestReport = false; + + if (shouldUseMoneyReport) { + if (moneyRequestReportID) { + iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`] ?? null; + } else { + iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`] ?? null; + } + + shouldCreateNewMoneyRequestReport = ReportUtils.shouldCreateNewMoneyRequestReport(iouReport, chatReport); + if (!iouReport || shouldCreateNewMoneyRequestReport) { + iouReport = ReportUtils.buildOptimisticExpenseReport(chatReport.reportID, chatReport.policyID ?? '', payeeAccountID, amount, currency, false); + } else { + iouReport = {...iouReport}; + if (iouReport?.currency === currency && typeof iouReport.total === 'number' && typeof iouReport.nonReimbursableTotal === 'number') { + // Because of the Expense reports are stored as negative values, we subtract the total from the amount + iouReport.total -= amount; + iouReport.nonReimbursableTotal -= amount; + } + } + } + + // If shouldUseMoneyReport is true, the iouReport was defined. + // But we'll use the `shouldUseMoneyReport && iouReport` check further instead of `shouldUseMoneyReport` to avoid TS errors. // STEP 3: Build optimistic receipt and transaction const receiptObject: Receipt = {}; @@ -1229,9 +1387,9 @@ function getTrackExpenseInformation( const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( - amount, + ReportUtils.isExpenseReport(iouReport) ? -amount : amount, currency, - chatReport.reportID, + shouldUseMoneyReport && iouReport ? iouReport.reportID : '0', comment, created, '', @@ -1244,6 +1402,7 @@ function getTrackExpenseInformation( tag, billable, isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + false, ); // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction @@ -1256,37 +1415,52 @@ function getTrackExpenseInformation( } // STEP 4: Build optimistic reportActions. We need: - // 1. IOU action for the chatReport - // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread - const currentTime = DateUtils.getDBTime(); - const iouAction = ReportUtils.buildOptimisticIOUReportAction( + // 1. CREATED action for the iouReport (if tracking in the Expense chat) + // 2. IOU action for the iouReport (if tracking in the Expense chat), otherwise – for chatReport + // 3. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread + // 4. REPORTPREVIEW action for the chatReport (if tracking in the Expense chat) + const [, optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( + shouldUseMoneyReport && iouReport ? iouReport : chatReport, CONST.IOU.REPORT_ACTION_TYPE.TRACK, amount, currency, comment, + payeeEmail, [participant], optimisticTransaction.transactionID, undefined, - '0', false, false, receiptObject, false, - currentTime, + !shouldUseMoneyReport, ); - const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport); - const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); - // The IOU action and the transactionThread are co-dependent as parent-child, so we need to link them together - iouAction.childReportID = optimisticTransactionThread.reportID; + let reportPreviewAction: OnyxEntry = null; + if (shouldUseMoneyReport && iouReport) { + reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID); + if (reportPreviewAction) { + reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, false, comment, optimisticTransaction); + } else { + reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment, optimisticTransaction); + + // Generated ReportPreview action is a parent report action of the iou report. + // We are setting the iou report's parentReportActionID to display subtitle correctly in IOU page when offline. + iouReport.parentReportActionID = reportPreviewAction.reportActionID; + } + } // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense( chatReport, + iouReport, optimisticTransaction, + optimisticCreatedActionForIOUReport, iouAction, + reportPreviewAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread, + shouldCreateNewMoneyRequestReport, policy, policyTagList, policyCategories, @@ -1294,12 +1468,12 @@ function getTrackExpenseInformation( return { chatReport, - iouReport: undefined, + iouReport: iouReport ?? undefined, transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdIOUReportActionID: undefined, - reportPreviewAction: undefined, + createdIOUReportActionID: shouldCreateNewMoneyRequestReport ? optimisticCreatedActionForIOUReport.reportActionID : '0', + reportPreviewAction: reportPreviewAction ?? undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, onyxData: { @@ -1310,7 +1484,7 @@ function getTrackExpenseInformation( }; } -/** Requests money based on a distance (eg. mileage from a map) */ +/** Requests money based on a distance (e.g. mileage from a map) */ function createDistanceRequest( report: OnyxTypes.Report, participant: Participant, @@ -2195,6 +2369,10 @@ function trackExpense( policyCategories?: OnyxEntry, gpsPoints = undefined, ) { + const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; + const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { iouReport, @@ -2208,7 +2386,7 @@ function trackExpense( createdReportActionIDForThread, onyxData, } = getTrackExpenseInformation( - report, + currentChatReport, participant, comment, amount, @@ -2223,8 +2401,10 @@ function trackExpense( policyTagList, policyCategories, payeeEmail, + payeeAccountID, + moneyRequestReportID, ); - const activeReportID = report.reportID; + const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID; const parameters: TrackExpenseParams = { amount, @@ -3674,7 +3854,12 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor if (typeof updatedIOUReport.total === 'number' && currency === iouReport?.currency) { // Because of the Expense reports are stored as negative values, we add the total from the amount - updatedIOUReport.total += TransactionUtils.getAmount(transaction, true); + const amountDiff = TransactionUtils.getAmount(transaction, true); + updatedIOUReport.total += amountDiff; + + if (!transaction?.reimbursable && typeof updatedIOUReport.nonReimbursableTotal === 'number') { + updatedIOUReport.nonReimbursableTotal += amountDiff; + } } } else { updatedIOUReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, reportAction.actorAccountID ?? -1, TransactionUtils.getAmount(transaction, false), currency, true); @@ -3921,6 +4106,10 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor function deleteTrackExpense(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { // STEP 1: Get all collections we're updating const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; + if (!ReportUtils.isSelfDM(chatReport)) { + return deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView); + } + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; const transactionThreadID = reportAction.childReportID; @@ -4377,7 +4566,7 @@ function getPayMoneyRequestParams( paymentMethodType: PaymentMethodType, full: boolean, ): PayMoneyRequestData { - let total = iouReport.total ?? 0; + let total = (iouReport.total ?? 0) - (iouReport.nonReimbursableTotal ?? 0); if (ReportUtils.hasHeldExpenses(iouReport.reportID) && !full && !!iouReport.unheldTotal) { total = iouReport.unheldTotal; } @@ -4970,7 +5159,7 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R const recipient = {accountID: iouReport.ownerAccountID}; const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType, full); - // For now we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with + // For now, we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with // Expensify Wallets. const apiCommand = paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY ? WRITE_COMMANDS.PAY_MONEY_REQUEST_WITH_WALLET : WRITE_COMMANDS.PAY_MONEY_REQUEST; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index ef436e57dc10..6040c1e67212 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -145,10 +145,10 @@ function ReportActionsView({ // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); - // Filter out request and send money request actions because we don't want to show any preview actions for one transaction reports + // Filter out the money request actions because we don't want to show any preview actions for one-transaction reports const filteredReportActions = [...allReportActions, ...filteredTransactionThreadReportActions].filter((action) => { const actionType = (action as OnyxTypes.OriginalMessageIOU).originalMessage?.type ?? ''; - return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && !ReportActionsUtils.isSentMoneyReportAction(action); + return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK && !ReportActionsUtils.isSentMoneyReportAction(action); }); return ReportActionsUtils.getSortedReportActions(filteredReportActions, true); }, [allReportActions, transactionThreadReportActions]); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index f2571cd60e0b..b3c4f8bdb001 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -357,7 +357,7 @@ describe('ReportUtils', () => { afterAll(() => Onyx.clear()); describe('return empty iou options if', () => { - it('participants aray contains excluded expensify iou emails', () => { + it('participants array contains excluded expensify iou emails', () => { const allEmpty = CONST.EXPENSIFY_ACCOUNT_IDS.every((accountID) => { const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(null, null, [currentUserAccountID, accountID]); return moneyRequestOptions.length === 0; @@ -513,6 +513,32 @@ describe('ReportUtils', () => { }); describe('return only money request option if', () => { + it('it is an IOU report in submitted state', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.IOU, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); + expect(moneyRequestOptions.length).toBe(1); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + }); + + it('it is an IOU report in submitted state even with send money permissions', () => { + const report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.IOU, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); + expect(moneyRequestOptions.length).toBe(1); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + }); + }); + + describe('return only money request and track expense options if', () => { it("it is an expense report tied to user's own policy expense chat", () => { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}102`, { reportID: '102', @@ -525,8 +551,9 @@ describe('ReportUtils', () => { type: CONST.REPORT.TYPE.EXPENSE, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID]); - expect(moneyRequestOptions.length).toBe(1); + expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)).toBe(true); }); }); @@ -553,7 +580,9 @@ describe('ReportUtils', () => { isPolicyExpenseChatEnabled: false, } as const; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); - expect(moneyRequestOptions.length).toBe(1); + expect(moneyRequestOptions.length).toBe(2); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)).toBe(true); }); }); @@ -610,34 +639,39 @@ describe('ReportUtils', () => { policyID: paidPolicy.id, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); - expect(moneyRequestOptions.length).toBe(1); + expect(moneyRequestOptions.length).toBe(2); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)).toBe(true); }); }); }); - describe('return multiple money request option if', () => { - it("it is user's own policy expense chat", () => { + describe('return multiple money request options if', () => { + it('it is a 1:1 DM', () => { const report = { ...LHNTestUtils.getFakeReport(), - chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - isOwnPolicyExpenseChat: true, + type: CONST.REPORT.TYPE.CHAT, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); - expect(moneyRequestOptions.length).toBe(2); - expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); + expect(moneyRequestOptions.length).toBe(3); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); }); + }); - it('it is a 1:1 DM', () => { + describe('return multiple money request options and the track expense option if', () => { + it("it is user's own policy expense chat", () => { const report = { ...LHNTestUtils.getFakeReport(), - type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: true, }; - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, participantsAccountIDs[0]]); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, null, [currentUserAccountID, ...participantsAccountIDs]); expect(moneyRequestOptions.length).toBe(3); - expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); - expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); + expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)).toBe(true); }); }); });