Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type MoveIOUReportToPolicyAndInviteSubmitterParams = {
iouReportID: string;
policyID: string;
policyExpenseChatReportID: string;
policyExpenseCreatedReportActionID: string;
changePolicyReportActionID: string;
};

export default MoveIOUReportToPolicyAndInviteSubmitterParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ 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';
export type {default as MoveIOUReportToExistingPolicyParams} from './MoveIOUReportToExistingPolicyParams';
export type {default as ChangeReportPolicyParams} from './ChangeReportPolicyParams';
export type {default as ResetBankAccountSetupParams} from './ResetBankAccountSetupParams';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,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',
Expand Down Expand Up @@ -600,6 +601,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.MOVE_IOU_REPORT_TO_EXISTING_POLICY]: Parameters.MoveIOUReportToExistingPolicyParams;
[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;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9495,6 +9495,7 @@ export {
buildOptimisticModifiedExpenseReportAction,
buildOptimisticMoneyRequestEntities,
buildOptimisticMovedReportAction,
buildOptimisticChangePolicyReportAction,
buildOptimisticMovedTrackedExpenseModifiedReportAction,
buildOptimisticRenamedRoomReportAction,
buildOptimisticRoomDescriptionUpdatedReportAction,
Expand Down Expand Up @@ -9817,7 +9818,6 @@ export {
isSelectedManagerMcTest,
isTestTransactionReport,
getReportSubtitlePrefix,
buildOptimisticChangePolicyReportAction,
getPolicyChangeMessage,
getExpenseReportStateAndStatus,
buildOptimisticResolvedDuplicatesReportAction,
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/Policy/Member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,7 @@ export {
importPolicyMembers,
downloadMembersCSV,
clearInviteDraft,
buildRoomMembersOnyxData,
openPolicyMemberProfilePage,
};

Expand Down
277 changes: 275 additions & 2 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +29,7 @@ import type {
MarkAsExportedParams,
MarkAsUnreadParams,
MoveIOUReportToExistingPolicyParams,
MoveIOUReportToPolicyAndInviteSubmitterParams,
OpenReportParams,
OpenRoomMembersPageParams,
ReadNewestActionParams,
Expand Down Expand Up @@ -86,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, getPolicy} 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';
Expand All @@ -113,6 +114,7 @@ import {
findSelfDMReportID,
formatReportLastMessageText,
generateReportID,
getAllPolicyReports,
getChatByParticipants,
getChildReportNotificationPreference,
getDefaultNotificationPreferenceForReport,
Expand Down Expand Up @@ -168,6 +170,7 @@ import type {
PersonalDetails,
PersonalDetailsList,
Policy,
PolicyEmployee,
PolicyReportField,
QuickAction,
RecentlyUsedReportFields,
Expand All @@ -186,6 +189,8 @@ import {clearByKey} from './CachedPDFPaths';
import {setDownload} from './Download';
import {close} from './Modal';
import navigateFromNotification from './navigateFromNotification';
import {buildRoomMembersOnyxData} from './Policy/Member';
import {createPolicyExpenseChats} from './Policy/Policy';
import {
createUpdateCommentMatcher,
resolveCommentDeletionConflicts,
Expand Down Expand Up @@ -387,6 +392,37 @@ Onyx.connect({
callback: (value) => (nvpDismissedProductTraining = value),
});

const allPolicies: OnyxCollection<Policy> = {};
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 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.multiSet(cleanUpSetQueries);
delete allPolicies[key];
return;
}

allPolicies[key] = val;
},
});

let environmentURL: string;
Environment.getEnvironmentURL().then((url: string) => (environmentURL = url));

Expand Down Expand Up @@ -5083,6 +5119,242 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) {
API.write(WRITE_COMMANDS.MOVE_IOU_REPORT_TO_EXISTING_POLICY, parameters, {optimisticData, successData, failureData});
}

/**
* 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(reportID: string, policyID: string) {
const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const policy = getPolicy(policyID);

if (!policy || !iouReport) {
return;
}

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;
}

// 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<PolicyEmployee> = {
[submitterLogin]: {
role,
email: submitterLogin,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
submitsTo: getDefaultApprover(allPolicies?.[policyKey]),
},
};

const successMembersState: OnyxCollectionInputValue<PolicyEmployee> = {
[submitterLogin]: {pendingAction: null},
};

const failureMembersState: OnyxCollectionInputValue<PolicyEmployee> = {
[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
// - 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,
chatReportID: optimisticPolicyExpenseChatReportID,
policyID,
policyName: policy.name,
parentReportID: optimisticPolicyExpenseChatReportID,
type: CONST.REPORT.TYPE.EXPENSE,
total: -(iouReport?.total ?? 0),
};
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: expenseReport,
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: iouReport,
});

// The expense report transactions need to have the amount reversed to negative values
const reportTransactions = getReportTransactions(reportID);

// For performance reasons, we are going to compose a merge collection data for transactions
const transactionsOptimisticData: Record<string, Transaction> = {};
const transactionFailureData: Record<string, Transaction> = {};
reportTransactions.forEach((transaction) => {
transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = {
...transaction,
amount: -transaction.amount,
modifiedAmount: transaction.modifiedAmount ? -transaction.modifiedAmount : 0,
Comment on lines +5258 to +5259

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have negated convertedAmount so fixed in #91079

};

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 oldChatReportID = iouReport.chatReportID;
const reportPreviewActionID = iouReport.parentReportActionID;
const reportPreview = !!oldChatReportID && !!reportPreviewActionID ? allReportActions?.[oldChatReportID]?.[reportPreviewActionID] : undefined;

if (reportPreview?.reportActionID) {
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 the reportPreview action to workspace chat
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatReportID}`,
value: {[reportPreview.reportActionID]: {...reportPreview, created: DateUtils.getDBTime()}},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticPolicyExpenseChatReportID}`,
value: {[reportPreview.reportActionID]: null},
});
}

// Create the CHANGE_POLICY report action and add it to the expense report which indicates to the user where the report has been moved
const changePolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, true);
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[changePolicyReportAction.reportActionID]: changePolicyReportAction},
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[changePolicyReportAction.reportActionID]: {
...changePolicyReportAction,
pendingAction: null,
},
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[changePolicyReportAction.reportActionID]: null},
});

// To optimistically remove the GBR from the DM we need to update the hasOutstandingChildRequest param to false
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`,
value: {
hasOutstandingChildRequest: false,
iouReportID: null,
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`,
value: {
hasOutstandingChildRequest: true,
iouReportID: reportID,
},
});

const parameters: MoveIOUReportToPolicyAndInviteSubmitterParams = {
iouReportID: reportID,
policyID,
policyExpenseChatReportID: optimisticPolicyExpenseChatReportID ?? String(CONST.DEFAULT_NUMBER_ID),
policyExpenseCreatedReportActionID: optimisticPolicyExpenseChatCreatedReportActionID ?? String(CONST.DEFAULT_NUMBER_ID),
changePolicyReportActionID: changePolicyReportAction.reportActionID,
};

API.write(WRITE_COMMANDS.MOVE_IOU_REPORT_TO_POLICY_AND_INVITE_SUBMITTER, parameters, {optimisticData, successData, failureData});
}

/**
* Dismisses the change report policy educational modal so that it doesn't show up again.
*/
Expand Down Expand Up @@ -5431,6 +5703,7 @@ export {
saveReportDraft,
prepareOnboardingOnyxData,
moveIOUReportToPolicy,
moveIOUReportToPolicyAndInviteSubmitter,
dismissChangePolicyModal,
changeReportPolicy,
};
Loading
Loading