From 7bd6e412448797c020663131f1f3b88f1840e8ed Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Wed, 12 Mar 2025 20:41:03 -0700 Subject: [PATCH 1/8] Simplified Actions - Implement MoveIOUReportToPolicyAndInviteSubmitter --- ...UReportToPolicyAndInviteSubmitterParams.ts | 9 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/ReportUtils.ts | 50 ++++++ src/libs/actions/Report.ts | 168 ++++++++++++++++++ 5 files changed, 230 insertions(+) create mode 100644 src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts diff --git a/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts b/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts new file mode 100644 index 000000000000..4868b9d60ab8 --- /dev/null +++ b/src/libs/API/parameters/MoveIOUReportToPolicyAndInviteSubmitterParams.ts @@ -0,0 +1,9 @@ +type MoveIOUReportToPolicyAndInviteSubmitterParams = { + iouReportID: string; + policyID: string; + policyExpenseChatReportID: string; + policyExpenseCreatedReportActionID: string; + changePolicyReportActionID: string; +}; + +export default MoveIOUReportToPolicyAndInviteSubmitterParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 049de664f15f..82e4b02ed359 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -380,3 +380,4 @@ export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboar export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams'; export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams'; export type {default as SaveCorpayOnboardingBeneficialOwnerParams} from './SaveCorpayOnboardingBeneficialOwnerParams'; +export type {default as MoveIOUReportToPolicyAndInviteSubmitterParams} from './MoveIOUReportToPolicyAndInviteSubmitterParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 987a1d0ac4f5..054c4d5e8614 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -144,6 +144,7 @@ const WRITE_COMMANDS = { CREATE_WORKSPACE: 'CreateWorkspace', CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', + MOVE_IOU_REPORT_TO_POLICY_AND_INVITE_SUBMITTER: 'MoveIOUReportToPolicyAndInviteSubmitter', SET_POLICY_TAGS_ENABLED: 'SetPolicyTagsEnabled', CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', IMPORT_CATEGORIES_SPREADSHEET: 'ImportCategoriesSpreadsheet', @@ -593,6 +594,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CREATE_WORKSPACE]: Parameters.CreateWorkspaceParams; [WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams; [WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams; + [WRITE_COMMANDS.MOVE_IOU_REPORT_TO_POLICY_AND_INVITE_SUBMITTER]: Parameters.MoveIOUReportToPolicyAndInviteSubmitterParams; [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams; [WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams; [WRITE_COMMANDS.IMPORT_PER_DIEM_RATES]: Parameters.ImportPerDiemRatesParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index df13c7728ef2..19e6a05a2345 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5714,6 +5714,55 @@ function buildOptimisticMovedReportAction(fromPolicyID: string | undefined, toPo }; } +/** + * Builds an optimistic CHANGEPOLICY report action with a randomly generated reportActionID. + * This action is used when we change a report's workspace. + */ +function buildOptimisticChangePolicyReportAction( + fromPolicyID: string | undefined, + toPolicyID: string, + newParentReportID: string, + oldParentReportID: string, + policyName: string, + oldPolicyName: string, + automaticAction = true, +) { + const originalMessage = { + fromPolicyID, + toPolicyID, + automaticAction, + }; + + const movedActionMessage = [ + { + html: `moved the report to the ${policyName} (previously ${oldPolicyName})`, + text: `moved the report to the ${policyName} (previously ${oldPolicyName})`, + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + }, + ]; + + return { + actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY, + actorAccountID: currentUserAccountID, + automatic: false, + avatar: getCurrentUserAvatar(), + isAttachmentOnly: false, + originalMessage, + message: movedActionMessage, + person: [ + { + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + type: 'TEXT', + }, + ], + reportActionID: rand64(), + shouldShow: true, + created: DateUtils.getDBTime(), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + /** * Builds an optimistic SUBMITTED report action with a randomly generated reportActionID. * @@ -9289,6 +9338,7 @@ export { buildOptimisticModifiedExpenseReportAction, buildOptimisticMoneyRequestEntities, buildOptimisticMovedReportAction, + buildOptimisticChangePolicyReportAction, buildOptimisticMovedTrackedExpenseModifiedReportAction, buildOptimisticRenamedRoomReportAction, buildOptimisticRoomDescriptionUpdatedReportAction, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 50af6ec3987f..f6c1c8feb14d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -27,6 +27,7 @@ import type { LeaveRoomParams, MarkAsExportedParams, MarkAsUnreadParams, + MoveIOUReportToPolicyAndInviteSubmitterParams, OpenReportParams, OpenRoomMembersPageParams, ReadNewestActionParams, @@ -91,6 +92,7 @@ import type {OptimisticAddCommentReportAction, OptimisticChatReport, OptimisticN import { buildOptimisticAddCommentReportAction, buildOptimisticChangeFieldAction, + buildOptimisticChangePolicyReportAction, buildOptimisticChatReport, buildOptimisticCreatedReportAction, buildOptimisticExportIntegrationAction, @@ -124,12 +126,14 @@ import { getReportLastVisibleActionCreated, getReportMetadata, getReportNotificationPreference, + getReportTransactions, getReportViolations, getRouteFromLink, isChatThread as isChatThreadReportUtils, isConciergeChatReport, isGroupChat as isGroupChatReportUtils, isHiddenForCurrentUser, + isIOUReportUsingReport, isMoneyRequestReport, isSelfDM, isValidReportIDFromPath, @@ -162,6 +166,7 @@ import type { ReportAction, ReportActionReactions, ReportUserIsTyping, + Transaction, } from '@src/types/onyx'; import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -4852,6 +4857,168 @@ function clearDeleteTransactionNavigateBackUrl() { Onyx.merge(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, null); } +/** + * Moves an IOU report to a policy by converting it to an expense report + * @param reportID - The ID of the IOU report to move + * @param policyID - The ID of the policy to move the report to + */ +function moveIOUReportToPolicyAndInviteSubmitter( + iouReportID: string, + policyID: string, + policyExpenseChatReportID: string, + policyExpenseCreatedReportActionID: string, + changePolicyReportActionID: string, +) { + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; + const policy = getPolicy(policyID); + + // This flow only works for IOU reports + if (!policy || !iouReport || !isIOUReportUsingReport(iouReport)) { + return; + } + + // We do not want to create negative amount expenses + if (ReportActionsUtils.hasRequestFromCurrentAccount(iouReport.reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { + return; + } + + // Generate new variables for the policy + const policyName = policy.name ?? ''; + const oldPolicyName = iouReport.reportName ?? ''; + const employeeAccountID = iouReport.ownerAccountID; + const chatReportID = getPolicyExpenseChat(employeeAccountID, policyID)?.reportID; + + if (!iouReport.parentReportID || !chatReportID) { + return; + } + + const optimisticData: OnyxUpdate[] = []; + + const successData: OnyxUpdate[] = []; + + const failureData: OnyxUpdate[] = []; + + // Convert the IOU report to Expense report by changing: + // - update parentReportID to point to the expense report + // - report type + // - change the sign of the report total + // - update its policyID and policyName + // - update the chatReportID to point to the workspace chat if the policy has policy expense chat enabled + const expenseReport = { + ...iouReport, + parentReportID: iouReport.parentReportID, + chatReportID: policy.isPolicyExpenseChatEnabled ? chatReportID : undefined, + policyID, + policyName, + type: CONST.REPORT.TYPE.EXPENSE, + total: -(iouReport?.total ?? 0), + }; + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + value: expenseReport, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + value: iouReport, + }); + + // The expense report transactions need to have the amount reversed to negative values + const reportTransactions = getReportTransactions(iouReportID); + + // For performance reasons, we are going to compose a merge collection data for transactions + const transactionsOptimisticData: Record = {}; + const transactionFailureData: Record = {}; + reportTransactions.forEach((transaction) => { + transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = { + ...transaction, + amount: -transaction.amount, + modifiedAmount: transaction.modifiedAmount ? -transaction.modifiedAmount : 0, + }; + + transactionFailureData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = transaction; + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE_COLLECTION, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}`, + value: transactionsOptimisticData, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE_COLLECTION, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}`, + value: transactionFailureData, + }); + + // We need to move the report preview action from the DM to the workspace chat. + const parentReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]; + const parentReportActionID = iouReport.parentReportActionID; + const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReport?.[parentReportActionID] : undefined; + const oldChatReportID = iouReport.chatReportID; + const newChatReportID = chatReportID; + + if (reportPreview?.reportActionID) { + // Remove from old chat (DM) + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[reportPreview.reportActionID]: null}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[reportPreview.reportActionID]: reportPreview}, + }); + + // Add to new chat (workspace chat) + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newChatReportID}`, + value: {[reportPreview.reportActionID]: reportPreview}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newChatReportID}`, + value: {[reportPreview.reportActionID]: null}, + }); + } + + // Create the CHANGEPOLICY report action and add it to the DM chat which indicates to the user where the report has been moved + const changedPolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, chatReportID, iouReportID, policyName, oldPolicyName); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[changedPolicyReportAction.reportActionID]: changedPolicyReportAction}, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: { + [changedPolicyReportAction.reportActionID]: { + ...changedPolicyReportAction, + pendingAction: null, + }, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[changedPolicyReportAction.reportActionID]: null}, + }); + + const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = { + iouReportID, + policyID, + policyExpenseChatReportID, + policyExpenseCreatedReportActionID, + changePolicyReportActionID, + }; + + API.write(WRITE_COMMANDS.MOVE_IOU_REPORT_TO_POLICY_AND_INVITE_SUBMITTER, parameters, {optimisticData, successData, failureData}); +} + export type {Video}; export { @@ -4948,4 +5115,5 @@ export { updateRoomVisibility, updateWriteCapability, prepareOnboardingOnyxData, + moveIOUReportToPolicyAndInviteSubmitter, }; From d8148a4714bc230ca8a0c18bd93711ca98ae48d0 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 13 Mar 2025 13:23:37 -0700 Subject: [PATCH 2/8] added admin policy check and invite submitter logic --- src/libs/ReportUtils.ts | 50 +++++ src/libs/actions/Policy/Member.ts | 1 + src/libs/actions/Report.ts | 305 +++++++++++++++++++++--------- 3 files changed, 262 insertions(+), 94 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2c82b5163d6..f8950217d734 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5725,6 +5725,55 @@ function buildOptimisticMovedReportAction(fromPolicyID: string | undefined, toPo }; } +/** + * Builds an optimistic CHANGEPOLICY report action with a randomly generated reportActionID. + * This action is used when we move a report to another workspace. + */ +function buildOptimisticMovePolicyAndInviteSubmitterReportAction(fromPolicyID: string | undefined, toPolicyID: string, automaticAction = true) { + const fromPolicy = getPolicy(fromPolicyID); + const toPolicy = getPolicy(toPolicyID); + + const originalMessage = { + fromPolicy, + toPolicy, + automaticAction, + }; + + const movePolicyReportActionMessage = [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + text: `changed the workspace to ${toPolicy?.name}`, + }, + ...(fromPolicyID + ? [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + text: ` (previously ${fromPolicy?.name})`, + }, + ] + : []), + ]; + + return { + actionName: CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY, + actorAccountID: currentUserAccountID, + avatar: getCurrentUserAvatar(), + created: DateUtils.getDBTime(), + originalMessage, + message: movePolicyReportActionMessage, + person: [ + { + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + type: 'TEXT', + }, + ], + reportActionID: rand64(), + shouldShow: true, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + /** * Builds an optimistic CHANGEPOLICY report action with a randomly generated reportActionID. * This action is used when we change the workspace of a report. @@ -9374,6 +9423,7 @@ export { buildOptimisticModifiedExpenseReportAction, buildOptimisticMoneyRequestEntities, buildOptimisticMovedReportAction, + buildOptimisticMovePolicyAndInviteSubmitterReportAction, buildOptimisticChangePolicyReportAction, buildOptimisticMovedTrackedExpenseModifiedReportAction, buildOptimisticRenamedRoomReportAction, diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index d43dcd236c59..2e0199358d45 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -1223,6 +1223,7 @@ export { importPolicyMembers, downloadMembersCSV, clearInviteDraft, + buildRoomMembersOnyxData, }; export type {NewCustomUnit}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1e62832bdcd9..d7aff0baf1bb 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3,7 +3,7 @@ import {format as timezoneFormat, toZonedTime} from 'date-fns-tz'; import {Str} from 'expensify-common'; import isEmpty from 'lodash/isEmpty'; import {DeviceEventEmitter, InteractionManager, Linking} from 'react-native'; -import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; @@ -83,7 +83,7 @@ import {rand64} from '@libs/NumberUtils'; import Parser from '@libs/Parser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; -import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils'; +import {extractPolicyIDFromPath, getDefaultApprover, getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import Pusher from '@libs/Pusher'; import type {UserIsLeavingRoomEvent, UserIsTypingEvent} from '@libs/Pusher/types'; @@ -97,6 +97,7 @@ import { buildOptimisticCreatedReportAction, buildOptimisticExportIntegrationAction, buildOptimisticGroupChatReport, + buildOptimisticMovePolicyAndInviteSubmitterReportAction, buildOptimisticRenamedRoomReportAction, buildOptimisticReportPreview, buildOptimisticRoomDescriptionUpdatedReportAction, @@ -110,6 +111,7 @@ import { findSelfDMReportID, formatReportLastMessageText, generateReportID, + getAllPolicyReports, getChatByParticipants, getChildReportNotificationPreference, getDefaultNotificationPreferenceForReport, @@ -127,7 +129,6 @@ import { getReportLastVisibleActionCreated, getReportMetadata, getReportNotificationPreference, - getReportTransactions, getReportViolations, getRouteFromLink, isChatThread as isChatThreadReportUtils, @@ -162,6 +163,7 @@ import type { PersonalDetails, PersonalDetailsList, Policy, + PolicyEmployee, PolicyReportField, QuickAction, RecentlyUsedReportFields, @@ -169,7 +171,6 @@ import type { ReportAction, ReportActionReactions, ReportUserIsTyping, - Transaction, } from '@src/types/onyx'; import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -179,6 +180,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {clearByKey} from './CachedPDFPaths'; import {close} from './Modal'; import navigateFromNotification from './navigateFromNotification'; +import {buildRoomMembersOnyxData} from './Policy/Member'; +import {createPolicyExpenseChats} from './Policy/Policy'; import { createUpdateCommentMatcher, resolveCommentDeletionConflicts, @@ -380,6 +383,39 @@ Onyx.connect({ callback: (value) => (nvpDismissedProductTraining = value), }); +const allPolicies: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + callback: (val, key) => { + if (!key) { + return; + } + if (val === null || val === undefined) { + // If we are deleting a policy, we have to check every report linked to that policy + // and unset the draft indicator (pencil icon) alongside removing any draft comments. Clearing these values will keep the newly archived chats from being displayed in the LHN. + // More info: https://github.com/Expensify/App/issues/14260 + const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); + const policyReports = getAllPolicyReports(policyID); + const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + policyReports.forEach((policyReport) => { + if (!policyReport) { + return; + } + const {reportID} = policyReport; + cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; + }); + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); + Onyx.multiSet(cleanUpSetQueries); + delete allPolicies[key]; + return; + } + + allPolicies[key] = val; + }, +}); + let environmentURL: string; Environment.getEnvironmentURL().then((url: string) => (environmentURL = url)); @@ -4878,145 +4914,226 @@ function moveIOUReportToPolicyAndInviteSubmitter( policyExpenseCreatedReportActionID: string, changePolicyReportActionID: string, ) { - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; + if (!iouReportID || !policyID) { + return; + } + + const reportToMove = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; const policy = getPolicy(policyID); + const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); // This flow only works for IOU reports - if (!policy || !iouReport || !isIOUReportUsingReport(iouReport)) { + if (!policy || !reportToMove || reportToMove?.policyID === policyID || !isPolicyAdmin || !isExpenseReport(reportToMove) || !isIOUReportUsingReport(reportToMove)) { return; } // We do not want to create negative amount expenses - if (ReportActionsUtils.hasRequestFromCurrentAccount(iouReport.reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { - return; - } - - // Generate new variables for the policy - const policyName = policy.name ?? ''; - const oldPolicyName = iouReport.reportName ?? ''; - const employeeAccountID = iouReport.ownerAccountID; - const chatReportID = getPolicyExpenseChat(employeeAccountID, policyID)?.reportID; - - if (!iouReport.parentReportID || !chatReportID) { + if (ReportActionsUtils.hasRequestFromCurrentAccount(iouReportID, reportToMove.managerID ?? CONST.DEFAULT_NUMBER_ID)) { return; } const optimisticData: OnyxUpdate[] = []; - const successData: OnyxUpdate[] = []; - const failureData: OnyxUpdate[] = []; - // Convert the IOU report to Expense report by changing: - // - update parentReportID to point to the expense report - // - report type - // - change the sign of the report total - // - update its policyID and policyName - // - update the chatReportID to point to the workspace chat if the policy has policy expense chat enabled - const expenseReport = { - ...iouReport, - parentReportID: iouReport.parentReportID, - chatReportID: policy.isPolicyExpenseChatEnabled ? chatReportID : undefined, - policyID, - policyName, - type: CONST.REPORT.TYPE.EXPENSE, - total: -(iouReport?.total ?? 0), - }; - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - value: expenseReport, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - value: iouReport, - }); + // 1. Optimistically set the policyID on the report (and all its threads) + // Preprocess reports to create a map of parentReportID to child reports list of reportIDs + const reportIDToThreadsReportIDsMap = buildReportIDToThreadsReportIDsMap(); + + // Recursively update the policyID of the report and all its child reports + updatePolicyIdForReportAndThreads(iouReportID, policyID, reportIDToThreadsReportIDsMap, optimisticData, failureData); - // The expense report transactions need to have the amount reversed to negative values - const reportTransactions = getReportTransactions(iouReportID); - - // For performance reasons, we are going to compose a merge collection data for transactions - const transactionsOptimisticData: Record = {}; - const transactionFailureData: Record = {}; - reportTransactions.forEach((transaction) => { - transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = { - ...transaction, - amount: -transaction.amount, - modifiedAmount: transaction.modifiedAmount ? -transaction.modifiedAmount : 0, + // 2. If the old workspace had a workspace chat, mark the report preview action as deleted + if (reportToMove?.parentReportID && reportToMove?.parentReportActionID) { + const oldReportPreviewAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`]?.[reportToMove?.parentReportActionID]; + const deletedTime = DateUtils.getDBTime(); + const firstMessage = Array.isArray(oldReportPreviewAction?.message) ? oldReportPreviewAction.message.at(0) : null; + const updatedReportPreviewAction = { + ...oldReportPreviewAction, + originalMessage: { + deleted: deletedTime, + }, + ...(firstMessage && { + message: [ + { + ...firstMessage, + deleted: deletedTime, + }, + ...(Array.isArray(oldReportPreviewAction?.message) ? oldReportPreviewAction.message.slice(1) : []), + ], + }), + ...(!Array.isArray(oldReportPreviewAction?.message) && { + message: { + deleted: deletedTime, + }, + }), }; - transactionFailureData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = transaction; - }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`, + value: {[reportToMove?.parentReportActionID]: updatedReportPreviewAction}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove?.parentReportID}`, + value: {[reportToMove?.parentReportActionID]: oldReportPreviewAction}, + }); + } - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE_COLLECTION, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}`, - value: transactionsOptimisticData, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE_COLLECTION, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}`, - value: transactionFailureData, - }); + // 3. Optimistically create a new REPORTPREVIEW reportAction with the newReportPreviewActionID + // and set it as a parent of the moved report + const policyExpenseChat = getPolicyExpenseChat(currentUserAccountID, policyID); + const optimisticReportPreviewAction = buildOptimisticReportPreview(policyExpenseChat, reportToMove); - // We need to move the report preview action from the DM to the workspace chat. - const parentReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]; - const parentReportActionID = iouReport.parentReportActionID; - const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReport?.[parentReportActionID] : undefined; - const oldChatReportID = iouReport.chatReportID; - const newChatReportID = chatReportID; + if (policyExpenseChat) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat.reportID}`, + value: {[optimisticReportPreviewAction.reportActionID]: optimisticReportPreviewAction}, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat.reportID}`, + value: { + [optimisticReportPreviewAction.reportActionID]: { + pendingAction: null, + }, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat.reportID}`, + value: {[optimisticReportPreviewAction.reportActionID]: null}, + }); - if (reportPreview?.reportActionID) { - // Remove from old chat (DM) + // Set the new report preview action as a parent of the moved report, + // and set the parentReportID on the moved report as the workspace chat reportID optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[reportPreview.reportActionID]: null}, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + value: {parentReportActionID: optimisticReportPreviewAction.reportActionID, parentReportID: policyExpenseChat.reportID}, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[reportPreview.reportActionID]: reportPreview}, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + value: {parentReportActionID: reportToMove.parentReportActionID, parentReportID: reportToMove.parentReportID}, }); - // Add to new chat (workspace chat) + // Set lastVisibleActionCreated optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newChatReportID}`, - value: {[reportPreview.reportActionID]: reportPreview}, + key: `${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`, + value: {lastVisibleActionCreated: optimisticReportPreviewAction?.created}, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newChatReportID}`, - value: {[reportPreview.reportActionID]: null}, + key: `${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`, + value: {lastVisibleActionCreated: policyExpenseChat.lastVisibleActionCreated}, }); } - // Create the CHANGEPOLICY report action and add it to the DM chat which indicates to the user where the report has been moved - const changedPolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, chatReportID, iouReportID, policyName, oldPolicyName); - + // 4. Optimistically create a CHANGEPOLICY reportAction on the report using the reportActionID + const optimisticMovedReportAction = buildOptimisticMovePolicyAndInviteSubmitterReportAction(reportToMove.policyID, policyID); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[changedPolicyReportAction.reportActionID]: changedPolicyReportAction}, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove.reportID}`, + value: {[optimisticMovedReportAction.reportActionID]: optimisticMovedReportAction}, }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove.reportID}`, value: { - [changedPolicyReportAction.reportActionID]: { - ...changedPolicyReportAction, + [optimisticMovedReportAction.reportActionID]: { + ...optimisticMovedReportAction, pendingAction: null, }, }, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[changedPolicyReportAction.reportActionID]: null}, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportToMove.reportID}`, + value: {[optimisticMovedReportAction.reportActionID]: null}, }); + // 5. Optimistically add the submitter to the workspace and create a workspace chat for them + const submitterAccountID = reportToMove.ownerAccountID; + const submitterEmail = PersonalDetailsUtils.getLoginByAccountID(submitterAccountID ?? CONST.DEFAULT_NUMBER_ID); + + if (!!submitterAccountID && !!submitterEmail) { + const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; + const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = { + [submitterEmail]: submitterAccountID, + }; + + // Set up new member optimistic data + const role = CONST.POLICY.ROLE.USER; + const submitterLogin = PhoneNumber.addSMSDomainIfPhoneNumber(submitterEmail); + + // Get personal details onyx data (similar to addMembersToWorkspace) + const {newAccountIDs, newLogins} = PersonalDetailsUtils.getNewAccountIDsAndLogins([submitterLogin], [submitterAccountID]); + const newPersonalDetailsOnyxData = PersonalDetailsUtils.getPersonalDetailsOnyxDataForOptimisticUsers(newLogins, newAccountIDs); + + // Build announce room members data for the new member + const announceRoomMembers = buildRoomMembersOnyxData(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID, [submitterAccountID]); + + // Create policy expense chat for the submitter + const membersChats = createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs); + + // Set up optimistic member state + const optimisticMembersState: OnyxCollectionInputValue = { + [submitterLogin]: { + role, + email: submitterLogin, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + submitsTo: getDefaultApprover(allPolicies?.[policyKey]), + }, + }; + + const successMembersState: OnyxCollectionInputValue = { + [submitterLogin]: {pendingAction: null}, + }; + + const failureMembersState: OnyxCollectionInputValue = { + [submitterLogin]: { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd'), + }, + }; + + // Add employee list update to optimistic data + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: optimisticMembersState, + }, + }); + + // Add success and failure data + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: successMembersState, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: failureMembersState, + }, + }); + + // Add personal details and chat data + optimisticData.push(...newPersonalDetailsOnyxData.optimisticData, ...membersChats.onyxOptimisticData, ...announceRoomMembers.optimisticData); + + successData.push(...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxSuccessData, ...announceRoomMembers.successData); + + failureData.push(...membersChats.onyxFailureData, ...announceRoomMembers.failureData); + } + const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = { iouReportID, policyID, From b77e8fa31ffa1786c6eca02d72aac715ff2a173f Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 20 Mar 2025 11:54:01 -0700 Subject: [PATCH 3/8] removed unused cleanUpMergeQueries --- src/libs/actions/Report.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a9cb2f7bea6f..86ca045e8c23 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -397,7 +397,6 @@ Onyx.connect({ // More info: https://github.com/Expensify/App/issues/14260 const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); const policyReports = getAllPolicyReports(policyID); - const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; policyReports.forEach((policyReport) => { if (!policyReport) { @@ -407,7 +406,6 @@ Onyx.connect({ cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; }); - Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); delete allPolicies[key]; return; From c4a3f7c0c5dc5990e275273f173481371a1a9bbd Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 25 Mar 2025 17:01:00 -0700 Subject: [PATCH 4/8] updated moveIOUReportToPolicyAndInviteSubmitter --- src/libs/ReportUtils.ts | 1 - src/libs/actions/Report.ts | 10 ++++------ src/pages/ReportChangeWorkspacePage.tsx | 8 +++++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8a93995fa6e6..d28c7d272e79 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9471,7 +9471,6 @@ export { buildOptimisticModifiedExpenseReportAction, buildOptimisticMoneyRequestEntities, buildOptimisticMovedReportAction, - buildOptimisticMovePolicyAndInviteSubmitterReportAction, buildOptimisticChangePolicyReportAction, buildOptimisticMovedTrackedExpenseModifiedReportAction, buildOptimisticRenamedRoomReportAction, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b97c93f97a3e..38a0ca78f0f9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -5084,10 +5084,7 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) { * @param reportID - The ID of the IOU report to move * @param policyID - The ID of the policy to move the report to */ -function moveIOUReportToPolicyAndInviteSubmitter( - reportID: string, - policyID: string, -) { +function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: string) { const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const policy = getPolicy(policyID); const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); @@ -5105,8 +5102,8 @@ function moveIOUReportToPolicyAndInviteSubmitter( // Generate new variables for the policy const policyName = policy.name ?? ''; const iouReportID = iouReport.reportID; - const employeeAccountID = iouReport.ownerAccountID; - const expenseChatReportID = getPolicyExpenseChat(employeeAccountID, policyID)?.reportID; + const ownerAccountID = iouReport.ownerAccountID; + const expenseChatReportID = getPolicyExpenseChat(ownerAccountID, policyID)?.reportID; if (!expenseChatReportID) { return; @@ -5678,6 +5675,7 @@ export { saveReportDraft, prepareOnboardingOnyxData, moveIOUReportToPolicy, + moveIOUReportToPolicyAndInviteSubmitter, dismissChangePolicyModal, changeReportPolicy, }; diff --git a/src/pages/ReportChangeWorkspacePage.tsx b/src/pages/ReportChangeWorkspacePage.tsx index 804588aed916..91440d30cd98 100644 --- a/src/pages/ReportChangeWorkspacePage.tsx +++ b/src/pages/ReportChangeWorkspacePage.tsx @@ -11,11 +11,11 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; import useWorkspaceList from '@hooks/useWorkspaceList'; -import {changeReportPolicy, moveIOUReportToPolicy} from '@libs/actions/Report'; +import {changeReportPolicy, moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeWorkspaceNavigatorParamList} from '@libs/Navigation/types'; -import {isPolicyMember, isWorkspaceEligibleForReportChange} from '@libs/PolicyUtils'; +import {getPolicy, isPolicyAdmin, isPolicyMember, isWorkspaceEligibleForReportChange} from '@libs/PolicyUtils'; import {isIOUReport, isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -45,7 +45,9 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) { return; } Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - if (isIOUReport(reportID) && isPolicyMember(currentUserLogin, policyID)) { + if (isIOUReport(reportID) && isPolicyAdmin(getPolicy(policyID))) { + moveIOUReportToPolicyAndInviteSubmitter(reportID, policyID); + } else if (isIOUReport(reportID) && isPolicyMember(currentUserLogin, policyID)) { moveIOUReportToPolicy(reportID, policyID); } else { changeReportPolicy(reportID, policyID); From 89291d3d18325523a544ad6955de686af43a501f Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Wed, 26 Mar 2025 17:57:41 -0700 Subject: [PATCH 5/8] fixed moveIOUReportToPolicyAndInviteSubmitter implementation --- src/libs/actions/Report.ts | 28 +++++++++++++++---------- src/pages/ReportChangeWorkspacePage.tsx | 5 +++-- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2b9dcbb0e6f5..52c1b6fea677 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -5110,10 +5110,16 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const policyName = policy.name ?? ''; const iouReportID = iouReport.reportID; const ownerAccountID = iouReport.ownerAccountID; - const expenseChatReportID = getPolicyExpenseChat(ownerAccountID, policyID)?.reportID; - if (!expenseChatReportID) { - return; + // Create an optimistic policy expense chat for the submitter who's not a policy member + let optimisticExpenseChatReportID: string | undefined; + let optimisticExpenseChatCreatedReportActionID: string | undefined; + + if (ownerAccountID) { + const employeeEmail = allPersonalDetails?.[ownerAccountID]?.login ?? ''; + const employeeWorkspaceChat = createPolicyExpenseChats(policyID, {[employeeEmail]: ownerAccountID}, true); + optimisticExpenseChatReportID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportID; + optimisticExpenseChatCreatedReportActionID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportActionID; } const optimisticData: OnyxUpdate[] = []; @@ -5128,7 +5134,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str // - update the chatReportID to point to the workspace chat if the policy has policy expense chat enabled const expenseReport = { ...iouReport, - chatReportID: policy.isPolicyExpenseChatEnabled ? expenseChatReportID : undefined, + chatReportID: policy.isPolicyExpenseChatEnabled ? optimisticExpenseChatReportID : undefined, policyID, policyName, parentReportID: iouReport.parentReportID, @@ -5194,12 +5200,12 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str // Add the reportPreview action to workspace chat optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, value: {[reportPreview.reportActionID]: {...reportPreview, created: DateUtils.getDBTime()}}, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, value: {[reportPreview.reportActionID]: null}, }); } @@ -5208,12 +5214,12 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const changePolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, true); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, value: {[changePolicyReportAction.reportActionID]: changePolicyReportAction}, }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, value: { [changePolicyReportAction.reportActionID]: { ...changePolicyReportAction, @@ -5223,7 +5229,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, value: {[changePolicyReportAction.reportActionID]: null}, }); @@ -5326,8 +5332,8 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = { iouReportID, policyID, - policyExpenseChatReportID: expenseChatReportID, - policyExpenseCreatedReportActionID: expenseReport.parentReportActionID ?? String(CONST.DEFAULT_NUMBER_ID), + policyExpenseChatReportID: optimisticExpenseChatReportID ?? String(CONST.DEFAULT_NUMBER_ID), + policyExpenseCreatedReportActionID: optimisticExpenseChatCreatedReportActionID ?? String(CONST.DEFAULT_NUMBER_ID), changePolicyReportActionID: changePolicyReportAction.reportActionID, }; diff --git a/src/pages/ReportChangeWorkspacePage.tsx b/src/pages/ReportChangeWorkspacePage.tsx index 91440d30cd98..e6bd240f803b 100644 --- a/src/pages/ReportChangeWorkspacePage.tsx +++ b/src/pages/ReportChangeWorkspacePage.tsx @@ -15,6 +15,7 @@ import {changeReportPolicy, moveIOUReportToPolicy, moveIOUReportToPolicyAndInvit import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeWorkspaceNavigatorParamList} from '@libs/Navigation/types'; +import {getLoginByAccountID} from '@libs/PersonalDetailsUtils'; import {getPolicy, isPolicyAdmin, isPolicyMember, isWorkspaceEligibleForReportChange} from '@libs/PolicyUtils'; import {isIOUReport, isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -45,7 +46,7 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) { return; } Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - if (isIOUReport(reportID) && isPolicyAdmin(getPolicy(policyID))) { + if (isIOUReport(reportID) && isPolicyAdmin(getPolicy(policyID)) && report.ownerAccountID && !isPolicyMember(getLoginByAccountID(report.ownerAccountID), policyID)) { moveIOUReportToPolicyAndInviteSubmitter(reportID, policyID); } else if (isIOUReport(reportID) && isPolicyMember(currentUserLogin, policyID)) { moveIOUReportToPolicy(reportID, policyID); @@ -53,7 +54,7 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) { changeReportPolicy(reportID, policyID); } }, - [currentUserLogin, reportID], + [currentUserLogin, report, reportID], ); const {sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput} = useWorkspaceList({ From cd7ed6139c8791d64767824d2d5b83624baf6630 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Thu, 27 Mar 2025 13:50:47 -0700 Subject: [PATCH 6/8] adjusted moveIOUReportToPolicyAndInviteSubmitter implementation --- src/libs/actions/Report.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 52c1b6fea677..d350826d125c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -5112,14 +5112,14 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const ownerAccountID = iouReport.ownerAccountID; // Create an optimistic policy expense chat for the submitter who's not a policy member - let optimisticExpenseChatReportID: string | undefined; - let optimisticExpenseChatCreatedReportActionID: string | undefined; + let optimisticPolicyExpenseChatReportID: string | undefined; + let optimisticPolicyExpenseChatCreatedReportActionID: string | undefined; if (ownerAccountID) { const employeeEmail = allPersonalDetails?.[ownerAccountID]?.login ?? ''; const employeeWorkspaceChat = createPolicyExpenseChats(policyID, {[employeeEmail]: ownerAccountID}, true); - optimisticExpenseChatReportID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportID; - optimisticExpenseChatCreatedReportActionID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportActionID; + optimisticPolicyExpenseChatReportID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportID; + optimisticPolicyExpenseChatCreatedReportActionID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportActionID; } const optimisticData: OnyxUpdate[] = []; @@ -5134,7 +5134,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str // - update the chatReportID to point to the workspace chat if the policy has policy expense chat enabled const expenseReport = { ...iouReport, - chatReportID: policy.isPolicyExpenseChatEnabled ? optimisticExpenseChatReportID : undefined, + chatReportID: policy.isPolicyExpenseChatEnabled ? optimisticPolicyExpenseChatReportID : undefined, policyID, policyName, parentReportID: iouReport.parentReportID, @@ -5200,12 +5200,12 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str // Add the reportPreview action to workspace chat optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatReportID}`, value: {[reportPreview.reportActionID]: {...reportPreview, created: DateUtils.getDBTime()}}, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatReportID}`, value: {[reportPreview.reportActionID]: null}, }); } @@ -5214,12 +5214,12 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const changePolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, true); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatCreatedReportActionID}`, value: {[changePolicyReportAction.reportActionID]: changePolicyReportAction}, }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatCreatedReportActionID}`, value: { [changePolicyReportAction.reportActionID]: { ...changePolicyReportAction, @@ -5229,7 +5229,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticExpenseChatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatCreatedReportActionID}`, value: {[changePolicyReportAction.reportActionID]: null}, }); @@ -5332,8 +5332,8 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = { iouReportID, policyID, - policyExpenseChatReportID: optimisticExpenseChatReportID ?? String(CONST.DEFAULT_NUMBER_ID), - policyExpenseCreatedReportActionID: optimisticExpenseChatCreatedReportActionID ?? String(CONST.DEFAULT_NUMBER_ID), + policyExpenseChatReportID: optimisticPolicyExpenseChatReportID ?? String(CONST.DEFAULT_NUMBER_ID), + policyExpenseCreatedReportActionID: optimisticPolicyExpenseChatCreatedReportActionID ?? String(CONST.DEFAULT_NUMBER_ID), changePolicyReportActionID: changePolicyReportAction.reportActionID, }; From a307355d8e979c05717621bd3bc81817ae5c4f5d Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Fri, 28 Mar 2025 13:51:34 -0700 Subject: [PATCH 7/8] applied refactor diff --- src/libs/actions/Report.ts | 206 +++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 112 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3c920fd3c9eb..6216edeca412 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -87,7 +87,7 @@ import {rand64} from '@libs/NumberUtils'; import Parser from '@libs/Parser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; -import {extractPolicyIDFromPath, getDefaultApprover, getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; +import {extractPolicyIDFromPath, getDefaultApprover, getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyMember} from '@libs/PolicyUtils'; import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import Pusher from '@libs/Pusher'; import type {UserIsLeavingRoomEvent, UserIsTypingEvent} from '@libs/Pusher/types'; @@ -5127,38 +5127,99 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) { function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: string) { const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const policy = getPolicy(policyID); - const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); - // This flow only works for IOU reports - if (!policy || !iouReport || !isPolicyAdmin || !isIOUReportUsingReport(iouReport)) { + if (!policy || !iouReport) { return; } - // We do not want to create negative amount expenses - if (ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { + const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); + const submitterAccountID = iouReport.ownerAccountID; + const submitterEmail = PersonalDetailsUtils.getLoginByAccountID(submitterAccountID ?? CONST.DEFAULT_NUMBER_ID); + const submitterLogin = PhoneNumber.addSMSDomainIfPhoneNumber(submitterEmail); + + // This flow only works for admins moving an IOU report to a policy where the submitter is NOT yet a member of the policy + if (!isPolicyAdmin || !isIOUReportUsingReport(iouReport) || !submitterAccountID || !submitterEmail || isPolicyMember(submitterLogin, policyID)) { return; } - // Generate new variables for the policy - const policyName = policy.name ?? ''; - const iouReportID = iouReport.reportID; - const ownerAccountID = iouReport.ownerAccountID; - - // Create an optimistic policy expense chat for the submitter who's not a policy member - let optimisticPolicyExpenseChatReportID: string | undefined; - let optimisticPolicyExpenseChatCreatedReportActionID: string | undefined; - - if (ownerAccountID) { - const employeeEmail = allPersonalDetails?.[ownerAccountID]?.login ?? ''; - const employeeWorkspaceChat = createPolicyExpenseChats(policyID, {[employeeEmail]: ownerAccountID}, true); - optimisticPolicyExpenseChatReportID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportID; - optimisticPolicyExpenseChatCreatedReportActionID = employeeWorkspaceChat.reportCreationData[employeeEmail]?.reportActionID; + // We only allow moving IOU report to a policy if it doesn't have requests from multiple users, as we do not want to create negative amount expenses + if (ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { + return; } const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; + // Optimistically add the submitter to the workspace and create a workspace chat for them + const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; + const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = { + [submitterEmail]: submitterAccountID, + }; + + // Set up new member optimistic data + const role = CONST.POLICY.ROLE.USER; + + // Get personal details onyx data (similar to addMembersToWorkspace) + const {newAccountIDs, newLogins} = PersonalDetailsUtils.getNewAccountIDsAndLogins([submitterLogin], [submitterAccountID]); + const newPersonalDetailsOnyxData = PersonalDetailsUtils.getPersonalDetailsOnyxDataForOptimisticUsers(newLogins, newAccountIDs); + + // Build announce room members data for the new member + const announceRoomMembers = buildRoomMembersOnyxData(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID, [submitterAccountID]); + + // Create policy expense chat for the submitter + const policyExpenseChats = createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs); + const optimisticPolicyExpenseChatReportID = policyExpenseChats.reportCreationData[submitterEmail].reportID; + const optimisticPolicyExpenseChatCreatedReportActionID = policyExpenseChats.reportCreationData[submitterEmail].reportActionID; + + // Set up optimistic member state + const optimisticMembersState: OnyxCollectionInputValue = { + [submitterLogin]: { + role, + email: submitterLogin, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + submitsTo: getDefaultApprover(allPolicies?.[policyKey]), + }, + }; + + const successMembersState: OnyxCollectionInputValue = { + [submitterLogin]: {pendingAction: null}, + }; + + const failureMembersState: OnyxCollectionInputValue = { + [submitterLogin]: { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd'), + }, + }; + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: optimisticMembersState, + }, + }); + + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: successMembersState, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: policyKey, + value: { + employeeList: failureMembersState, + }, + }); + + optimisticData.push(...newPersonalDetailsOnyxData.optimisticData, ...policyExpenseChats.onyxOptimisticData, ...announceRoomMembers.optimisticData); + successData.push(...newPersonalDetailsOnyxData.finallyData, ...policyExpenseChats.onyxSuccessData, ...announceRoomMembers.successData); + failureData.push(...policyExpenseChats.onyxFailureData, ...announceRoomMembers.failureData); + // Next we need to convert the IOU report to Expense report. // We need to change: // - report type @@ -5167,26 +5228,26 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str // - update the chatReportID to point to the workspace chat if the policy has policy expense chat enabled const expenseReport = { ...iouReport, - chatReportID: policy.isPolicyExpenseChatEnabled ? optimisticPolicyExpenseChatReportID : undefined, + chatReportID: optimisticPolicyExpenseChatReportID, policyID, - policyName, - parentReportID: iouReport.parentReportID, + policyName: policy.name, + parentReportID: optimisticPolicyExpenseChatReportID, type: CONST.REPORT.TYPE.EXPENSE, total: -(iouReport?.total ?? 0), }; optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: expenseReport, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: iouReport, }); // The expense report transactions need to have the amount reversed to negative values - const reportTransactions = getReportTransactions(iouReportID); + const reportTransactions = getReportTransactions(reportID); // For performance reasons, we are going to compose a merge collection data for transactions const transactionsOptimisticData: Record = {}; @@ -5213,10 +5274,9 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str }); // We need to move the report preview action from the DM to the workspace chat. - const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.parentReportID}`]; - const parentReportActionID = iouReport.parentReportActionID; - const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReportActions?.[parentReportActionID] : undefined; const oldChatReportID = iouReport.chatReportID; + const reportPreviewActionID = iouReport.parentReportActionID; + const reportPreview = !!oldChatReportID && !!reportPreviewActionID ? allReportActions?.[oldChatReportID]?.[reportPreviewActionID] : undefined; if (reportPreview?.reportActionID) { optimisticData.push({ @@ -5247,12 +5307,12 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str const changePolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, true); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatCreatedReportActionID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[changePolicyReportAction.reportActionID]: changePolicyReportAction}, }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatCreatedReportActionID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { [changePolicyReportAction.reportActionID]: { ...changePolicyReportAction, @@ -5262,7 +5322,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatCreatedReportActionID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[changePolicyReportAction.reportActionID]: null}, }); @@ -5280,90 +5340,12 @@ function moveIOUReportToPolicyAndInviteSubmitter(reportID: string, policyID: str key: `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`, value: { hasOutstandingChildRequest: true, - iouReportID, + iouReportID: reportID, }, }); - // Optimistically add the submitter to the workspace and create a workspace chat for them - const submitterAccountID = iouReport.ownerAccountID; - const submitterEmail = PersonalDetailsUtils.getLoginByAccountID(submitterAccountID ?? CONST.DEFAULT_NUMBER_ID); - - if (!!submitterAccountID && !!submitterEmail) { - const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; - const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = { - [submitterEmail]: submitterAccountID, - }; - - // Set up new member optimistic data - const role = CONST.POLICY.ROLE.USER; - const submitterLogin = PhoneNumber.addSMSDomainIfPhoneNumber(submitterEmail); - - // Get personal details onyx data (similar to addMembersToWorkspace) - const {newAccountIDs, newLogins} = PersonalDetailsUtils.getNewAccountIDsAndLogins([submitterLogin], [submitterAccountID]); - const newPersonalDetailsOnyxData = PersonalDetailsUtils.getPersonalDetailsOnyxDataForOptimisticUsers(newLogins, newAccountIDs); - - // Build announce room members data for the new member - const announceRoomMembers = buildRoomMembersOnyxData(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID, [submitterAccountID]); - - // Create policy expense chat for the submitter - const membersChats = createPolicyExpenseChats(policyID, invitedEmailsToAccountIDs); - - // Set up optimistic member state - const optimisticMembersState: OnyxCollectionInputValue = { - [submitterLogin]: { - role, - email: submitterLogin, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - submitsTo: getDefaultApprover(allPolicies?.[policyKey]), - }, - }; - - const successMembersState: OnyxCollectionInputValue = { - [submitterLogin]: {pendingAction: null}, - }; - - const failureMembersState: OnyxCollectionInputValue = { - [submitterLogin]: { - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd'), - }, - }; - - // Add employee list update to optimistic data - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: { - employeeList: optimisticMembersState, - }, - }); - - // Add success and failure data - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: { - employeeList: successMembersState, - }, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: policyKey, - value: { - employeeList: failureMembersState, - }, - }); - - // Add personal details and chat data - optimisticData.push(...newPersonalDetailsOnyxData.optimisticData, ...membersChats.onyxOptimisticData, ...announceRoomMembers.optimisticData); - - successData.push(...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxSuccessData, ...announceRoomMembers.successData); - - failureData.push(...membersChats.onyxFailureData, ...announceRoomMembers.failureData); - } - const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = { - iouReportID, + iouReportID: reportID, policyID, policyExpenseChatReportID: optimisticPolicyExpenseChatReportID ?? String(CONST.DEFAULT_NUMBER_ID), policyExpenseCreatedReportActionID: optimisticPolicyExpenseChatCreatedReportActionID ?? String(CONST.DEFAULT_NUMBER_ID), From 4104f2ead8849a5db4caf6720a2b7a7ff8b6b7cc Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Fri, 28 Mar 2025 14:51:05 -0700 Subject: [PATCH 8/8] removed submodule update --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 719b3f13e0e5..90ab368dba4a 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 719b3f13e0e5a64cfb61c9b39afd96c4edd5d3e0 +Subproject commit 90ab368dba4a150d2381380055cb221abdd09987