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
41 changes: 1 addition & 40 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import DateUtils from './DateUtils';
import {translateLocal} from './Localize';
import Navigation from './Navigation/Navigation';
import {isOffline as isOfflineNetworkStore} from './Network/NetworkStore';
import {getAccountIDsByLogins, getLoginByAccountID, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils';
import {getAccountIDsByLogins, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils';
import {getAllSortedTransactions, getCategory, getTag} from './TransactionUtils';
import {isPublicDomain} from './ValidationUtils';

Expand Down Expand Up @@ -1121,44 +1121,6 @@ const sortWorkspacesBySelected = (workspace1: WorkspaceDetails, workspace2: Work
return workspace1.name?.toLowerCase().localeCompare(workspace2.name?.toLowerCase() ?? '') ?? 0;
};

/**
* Determines whether the report can be moved to the workspace.
*/
const isWorkspaceEligibleForReportChange = (newPolicy: OnyxEntry<Policy>, report: OnyxEntry<Report>, oldPolicy: OnyxEntry<Policy>, currentUserLogin: string | undefined): boolean => {
const currentUserAccountID = getCurrentUserAccountID();
const isCurrentUserMember = !!currentUserLogin && !!newPolicy?.employeeList?.[currentUserLogin];
if (!isCurrentUserMember) {
return false;
}

const isAdmin = isUserPolicyAdmin(newPolicy, currentUserLogin);
if (report?.stateNum && report?.stateNum > CONST.REPORT.STATE_NUM.SUBMITTED && !isAdmin) {
return false;
}

// Submitters: workspaces where the submitter is a member of
const isCurrentUserSubmitter = report?.ownerAccountID === currentUserAccountID;
if (isCurrentUserSubmitter) {
return true;
}

// Approvers: workspaces where both the approver AND submitter are members of
const reportApproverAccountID = getSubmitToAccountID(oldPolicy, report);
const isCurrentUserApprover = currentUserAccountID === reportApproverAccountID;
if (isCurrentUserApprover) {
const reportSubmitterLogin = report?.ownerAccountID ? getLoginByAccountID(report?.ownerAccountID) : undefined;
const isReportSubmitterMember = !!reportSubmitterLogin && !!newPolicy?.employeeList?.[reportSubmitterLogin];
return isCurrentUserApprover && isReportSubmitterMember;
}

// Admins: same as approvers OR workspaces where the admin is an admin of (note that the submitter is invited to the workspace in this case)
if (isPolicyOwner(newPolicy, currentUserAccountID) || isAdmin) {
return true;
}

return false;
};

/**
* Takes removes pendingFields and errorFields from a customUnit
*/
Expand Down Expand Up @@ -1569,7 +1531,6 @@ export {
getPolicyNameByID,
getMostFrequentEmailDomain,
getDescriptionForPolicyDomainCard,
isWorkspaceEligibleForReportChange,
getManagerAccountID,
isPrefferedExporter,
areAllGroupPoliciesExpenseChatDisabled,
Expand Down
73 changes: 6 additions & 67 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
import {isApprover as isApproverUtils} from './actions/Policy/Member';
import {getCurrentUserAccountID, getCurrentUserEmail} from './actions/Report';
import {getCurrentUserAccountID} from './actions/Report';
import {
arePaymentsEnabled as arePaymentsEnabledUtils,
getAllPolicies,
Expand All @@ -12,9 +12,7 @@ import {
getSubmitToAccountID,
hasAccountingConnections,
hasIntegrationAutoSync,
hasNoPolicyOtherThanPersonalType,
isPrefferedExporter,
isWorkspaceEligibleForReportChange,
} from './PolicyUtils';
import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils';
import {
Expand All @@ -29,8 +27,8 @@ import {
isPayer as isPayerUtils,
isProcessingReport as isProcessingReportUtils,
isReportApproved as isReportApprovedUtils,
isReportManager as isReportManagerUtils,
isSettled,
isWorkspaceEligibleForReportChange,
} from './ReportUtils';
import {getSession} from './SessionUtils';
import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils';
Expand Down Expand Up @@ -310,69 +308,10 @@ function isHoldActionForTransation(report: Report, reportTransaction: Transactio
return isProcessingReport;
}

function isChangeWorkspaceAction(report: Report, reportTransactions: Transaction[], violations: OnyxCollection<TransactionViolation[]>, policy?: Policy): boolean {
const isExpenseReport = isExpenseReportUtils(report);
const isReportSubmitter = isCurrentUserSubmitter(report.reportID);
const areWorkflowsEnabled = !!(policy && policy.areWorkflowsEnabled);
const isClosedReport = isClosedReportUtils(report);

function isChangeWorkspaceAction(report: Report): boolean {
const policies = getAllPolicies();
const currentUserEmail = getCurrentUserEmail();
const policiesEligibleForChange = policies.filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, policy, currentUserEmail));

if (policiesEligibleForChange.length <= 1) {
return false;
}

if (isExpenseReport && isReportSubmitter && !areWorkflowsEnabled && isClosedReport) {
return true;
}

const isOpenReport = isOpenReportUtils(report);
const isProcessingReport = isProcessingReportUtils(report);

if (isReportSubmitter && (isOpenReport || isProcessingReport)) {
return true;
}

const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID());

if (isReportApprover && isProcessingReport) {
return true;
}

const isReportPayer = isPayerUtils(getSession(), report, false, policy);
const isReportApproved = isReportApprovedUtils({report});

if (isReportPayer && (isReportApproved || isClosedReport)) {
return true;
}

const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const isReportReimbursed = isSettled(report);
const transactionIDs = reportTransactions.map((t) => t.transactionID);
const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations);

const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations);

const userControlsReport = isReportSubmitter || isReportApprover || isAdmin;
const hasReceiptMatchViolation = hasAllPendingRTERViolations || (userControlsReport && shouldShowBrokenConnectionViolation);
const isReportExported = isExportedUtils(getReportActions(report));
const isReportFinished = isReportApproved || isReportReimbursed || isClosedReport;

if (isAdmin && ((!isReportExported && isReportFinished) || hasReceiptMatchViolation)) {
return true;
}

const isIOUReport = isIOUReportUtils(report);
const hasOnlyPersonalWorkspace = hasNoPolicyOtherThanPersonalType();
const isReportReceiver = isReportManagerUtils(report);

if (isIOUReport && !hasOnlyPersonalWorkspace && isReportReceiver && isReportReimbursed) {
return true;
}

return false;
const session = getSession();
return policies.filter((newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, session)).length > 0;
}

function isDeleteAction(report: Report, reportTransactions: Transaction[]): boolean {
Expand Down Expand Up @@ -445,7 +384,7 @@ function getSecondaryReportActions(

options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD);

if (isChangeWorkspaceAction(report, reportTransactions, violations, policy)) {
if (isChangeWorkspaceAction(report)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
}

Expand Down
74 changes: 74 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/o
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
import {createDraftTransaction, getIOUReportActionToApproveOrPay, setMoneyRequestParticipants, unholdRequest} from './actions/IOU';
import {isApprover as isApproverMember} from './actions/Policy/Member';
import {createDraftWorkspace} from './actions/Policy/Policy';
import {autoSwitchToFocusMode} from './actions/PriorityMode';
import {hasCreditBankAccount} from './actions/ReimbursementAccount/store';
Expand Down Expand Up @@ -102,6 +103,7 @@ import {
getAccountIDsByLogins,
getDisplayNameOrDefault,
getEffectiveDisplayName,
getLoginByAccountID,
getLoginsByAccountIDs,
getPersonalDetailByEmail,
getPersonalDetailsByIDs,
Expand Down Expand Up @@ -10337,6 +10339,77 @@ function isExported(reportActions: OnyxEntry<ReportActions> | ReportAction[]) {
return Object.values(reportActions).some((action) => isExportIntegrationAction(action));
}

function verifyState(report: OnyxEntry<Report>, validStates: Array<ValueOf<typeof CONST.REPORT.STATE_NUM>>): boolean {
if (report?.stateNum === undefined || report?.stateNum === null) {
return false;
}
return validStates.includes(report?.stateNum);
}

function verifyStatus(report: OnyxEntry<Report>, validStatuses: Array<ValueOf<typeof CONST.REPORT.STATUS_NUM>>): boolean {
if (report?.statusNum === undefined || report?.statusNum === null) {
return false;
}
return validStatuses.includes(report?.statusNum);
}

/**
* Determines whether the report can be moved to the workspace.
*/
function isWorkspaceEligibleForReportChange(newPolicy: OnyxEntry<Policy>, report: OnyxEntry<Report>, session: OnyxEntry<Session>): boolean {
if (!session?.accountID) {
return false;
}

const isIOU = isIOUReport(report);
const submitterLogin = report?.ownerAccountID && getLoginByAccountID(report?.ownerAccountID);
const isSubmitterMember = !!submitterLogin && !!newPolicy?.employeeList?.[submitterLogin];
const managerLogin = report?.managerID && getLoginByAccountID(report?.managerID);
const isManagerMember = !!managerLogin && !!newPolicy?.employeeList?.[managerLogin];
const isCurrentUserAdmin = isPolicyAdminPolicyUtils(newPolicy, session?.email);
const isPaidGroupPolicyType = isPaidGroupPolicyPolicyUtils(newPolicy);
const isReportOpenOrSubmitted = verifyState(report, [CONST.REPORT.STATE_NUM.OPEN, CONST.REPORT.STATE_NUM.SUBMITTED]);

// For IOUs, the sender and receiver can only change the workspace if:
// 1. The sender AND receiver are both members of the new policy OR
// 2. The sender OR receiver is an admin of the new policy. In this case, changing the policy also invites the non-member to the policy
if (isIOU && isReportOpenOrSubmitted && isPaidGroupPolicyType && ((isSubmitterMember && isManagerMember) || isCurrentUserAdmin)) {
return true;
}

// From this point on, reports must be of type Expense, the policy must be a paid type.
// The submitter and manager must also be policy members OR the current user is an admin so they can invite the non-members to the policy.
const isExpenseReportType = isExpenseReport(report);
if (!isExpenseReportType || !isPaidGroupPolicyType || !((isSubmitterMember && isManagerMember) || isCurrentUserAdmin)) {
return false;
}

const isCurrentUserReportSubmitter = session.accountID === report?.ownerAccountID;
if (isCurrentUserReportSubmitter && isReportOpenOrSubmitted) {
return true;
}

const isCurrentUserReportApprover = isApproverMember(newPolicy, session.accountID);
if (isCurrentUserReportApprover && verifyState(report, [CONST.REPORT.STATE_NUM.SUBMITTED])) {
return true;
}

const isCurrentUserReportPayer = isPayer(session, report, false, newPolicy);
if (isCurrentUserReportPayer && verifyState(report, [CONST.REPORT.STATE_NUM.APPROVED])) {
return true;
}

if (
isCurrentUserAdmin &&
verifyState(report, [CONST.REPORT.STATE_NUM.APPROVED]) &&
verifyStatus(report, [CONST.REPORT.STATUS_NUM.APPROVED, CONST.REPORT.STATUS_NUM.REIMBURSED, CONST.REPORT.STATUS_NUM.CLOSED])
) {
return true;
}

return false;
}

function getApprovalChain(policy: OnyxEntry<Policy>, expenseReport: OnyxEntry<Report>): string[] {
const approvalChain: string[] = [];
const fullApprovalChain: string[] = [];
Expand Down Expand Up @@ -10928,6 +11001,7 @@ export {
generateReportAttributes,
getReportPersonalDetailsParticipants,
isAllowedToSubmitDraftExpenseReport,
isWorkspaceEligibleForReportChange,
};

export type {
Expand Down
6 changes: 3 additions & 3 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4677,12 +4677,12 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) {
const changePolicyReportAction = buildOptimisticChangePolicyReportAction(iouReport.policyID, policyID, true);
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportId}`,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[changePolicyReportAction.reportActionID]: changePolicyReportAction},
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportId}`,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {
[changePolicyReportAction.reportActionID]: {
...changePolicyReportAction,
Expand All @@ -4692,7 +4692,7 @@ function moveIOUReportToPolicy(reportID: string, policyID: string) {
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportId}`,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[changePolicyReportAction.reportActionID]: null},
});

Expand Down
19 changes: 9 additions & 10 deletions src/pages/ReportChangeWorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ 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 {getPolicy, isPolicyAdmin, isPolicyMember} from '@libs/PolicyUtils';
import {isIOUReport, isMoneyRequestReport, isMoneyRequestReportPendingDeletion, isWorkspaceEligibleForReportChange} from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
Expand All @@ -34,10 +34,9 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
const {translate} = useLocalize();

const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const oldPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`];
const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false});
const shouldShowLoadingIndicator = isLoadingApp && !isOffline;

const selectPolicy = useCallback(
Expand All @@ -48,22 +47,22 @@ function ReportChangeWorkspacePage({report}: ReportChangeWorkspacePageProps) {
Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID));
if (isIOUReport(reportID) && isPolicyAdmin(getPolicy(policyID)) && report.ownerAccountID && !isPolicyMember(getLoginByAccountID(report.ownerAccountID), policyID)) {
moveIOUReportToPolicyAndInviteSubmitter(reportID, policyID);
} else if (isIOUReport(reportID) && isPolicyMember(currentUserLogin, policyID)) {
} else if (isIOUReport(reportID) && isPolicyMember(session?.email, policyID)) {
moveIOUReportToPolicy(reportID, policyID);
} else {
changeReportPolicy(reportID, policyID);
}
},
[currentUserLogin, report, reportID],
[session?.email, report, reportID],
);

const {sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput} = useWorkspaceList({
policies,
currentUserLogin,
currentUserLogin: session?.email,
shouldShowPendingDeletePolicy: false,
selectedPolicyID: report.policyID,
searchTerm: debouncedSearchTerm,
additionalFilter: (newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, oldPolicy, currentUserLogin),
additionalFilter: (newPolicy) => isWorkspaceEligibleForReportChange(newPolicy, report, session),
});

if (!isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report) || (!report.total && !report.unheldTotal)) {
Expand Down
Loading