From ad51938cd166877ad3218a3ce9b1c032d3eb40a0 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 3 Mar 2025 15:59:59 +0100 Subject: [PATCH 1/7] Bootstrap secondary actions getter --- src/CONST.ts | 13 ++ src/libs/PolicyUtils.ts | 14 ++ src/libs/ReportActionsUtils.ts | 5 + src/libs/ReportSecondaryActionUtils.ts | 202 +++++++++++++++++++++++++ src/libs/ReportUtils.ts | 5 + src/libs/TransactionUtils/index.ts | 61 ++++---- 6 files changed, 271 insertions(+), 29 deletions(-) create mode 100644 src/libs/ReportSecondaryActionUtils.ts diff --git a/src/CONST.ts b/src/CONST.ts index 258a83190f32..49daebf87616 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1109,6 +1109,19 @@ const CONST = { MIN_INITIAL_REPORT_ACTION_COUNT: 15, UNREPORTED_REPORTID: '0', SPLIT_REPORTID: '-2', + SECONDARY_ACTIONS: { + SUBMIT: 'submit', + APPROVE: 'approve', + UNAPPROVE: 'unapprove', + CANCEL_PAYMENT: 'cancelPayment', + EXPORT_TO_ACCOUNTING: 'exportToAccounting', + MARK_AS_EXPORTED: 'markAsExported', + HOLD: 'hold', + DOWNLOAD: 'download', + CHANGE_WORKSPACE: 'changeWorkspace', + VIEW_DETAILS: 'viewDetails', + DELETE: 'delete', + }, ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 92f47b91e8ee..41c2640dc2ac 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1354,6 +1354,19 @@ function canEnablePreventSelfApprovals(policy: OnyxEntry): boolean { return employeeEmails.length > 1; } +function isAutoSyncEnabled(policy: Policy) { + const values = [ + policy.connections?.intacct?.config?.autoSync?.enabled, + policy.connections?.netsuite?.config?.autoSync?.enabled, + policy.connections?.netsuiteQuickStart?.config?.autoSync?.enabled, + policy.connections?.quickbooksDesktop?.config?.autoSync?.enabled, + policy.connections?.quickbooksOnline?.config?.autoSync?.enabled, + policy.connections?.xero?.config?.autoSync?.enabled, + ]; + + return values.some((value) => !!value); +} + export { canEditTaxRate, canEnablePreventSelfApprovals, @@ -1492,6 +1505,7 @@ export { getPolicyNameByID, getMostFrequentEmailDomain, getDescriptionForPolicyDomainCard, + isAutoSyncEnabled, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3055909ef649..63c2df2f0f00 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2186,6 +2186,10 @@ function getReportActionsLength() { return Object.keys(allReportActions ?? {}).length; } +function getReportActions(report: Report) { + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]; +} + function wasActionCreatedWhileOffline(action: ReportAction, isOffline: boolean, lastOfflineAt: Date | undefined, lastOnlineAt: Date | undefined, locale: Locale): boolean { // The user has never gone offline or never come back online if (!lastOfflineAt || !lastOnlineAt) { @@ -2338,6 +2342,7 @@ export { getWorkspaceTagUpdateMessage, getWorkspaceReportFieldUpdateMessage, getWorkspaceReportFieldDeleteMessage, + getReportActions, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts new file mode 100644 index 000000000000..3d302a5e67ae --- /dev/null +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -0,0 +1,202 @@ +import type {OnyxCollection} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; +import {isApprover as isApprovedMember} from './actions/Policy/Member'; +import {getCurrentUserAccountID} from './actions/Report'; +import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; +import {getReportActions, getReportActionsLength} from './ReportActionsUtils'; +import { + isClosedReport, + isCurrentUserSubmitter, + isExpenseReport, + isExported, + isHoldCreator, + isInvoiceReport, + isIOUReport, + isOpenReport, + isPayer, + isProcessingReport, + isReportApproved, +} from './ReportUtils'; +import {getSession} from './SessionUtils'; +import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils'; + +function isSubmitAction(report: Report, policy: Policy): boolean { + const isExpense = isExpenseReport(report); + + if (!isExpense) { + return false; + } + + const isSubmitter = isCurrentUserSubmitter(report.reportID); + const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + if (!isSubmitter && !isApprover) { + return false; + } + const isOpen = isOpenReport(report); + if (!isOpen) { + return false; + } + + const autoReportingFrequency = getCorrectedAutoReportingFrequency(policy); + + const isScheduledSubmitEnabled = policy?.harvesting?.enabled && autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL; + + return !!isScheduledSubmitEnabled; +} + +function isApproveAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { + const isExpense = isExpenseReport(report); + const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isProcessing = isProcessingReport(report); + const hasDuplicates = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID)); + + if (isExpense && isApprover && isProcessing && hasDuplicates) { + return true; + } + + const transactionIDs = reportTransactions.map((t) => t.transactionID); + + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations); + + if (hasAllPendingRTERViolations) { + return true; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); + + const userControllsReport = isApprover || isAdmin; + return userControllsReport && shouldShowBrokenConnectionViolation; +} + +function isUnapproveAction(report: Report, policy: Policy): boolean { + const isExpense = isExpenseReport(report); + const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isApproved = isReportApproved({report}); + + return isExpense && isApprover && isApproved; +} + +function isCancelPaymentAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { + const isExpense = isExpenseReport(report); + if (!isExpense) { + return false; + } + + const isPaidElsewhere = report.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + + if (isPaidElsewhere) { + return true; + } + + const isPaymentProcessing = true; // TODO + const hasDailyNachaCutoffPassed = false; // TODO + return isPaymentProcessing && !hasDailyNachaCutoffPassed; +} + +function isExportAction(report: Report, policy: Policy): boolean { + const isInvoice = isInvoiceReport(report); + const isSender = true; // TODO is sender the same as submitter? + if (isInvoice && isSender) { + return true; + } + + const isExpense = isExpenseReport(report); + + const hasAccountingConnection = hasAccountingConnections(policy); + + if (!isExpense || !hasAccountingConnection) { + return false; + } + + const isApproved = isReportApproved({report}); + const isReportPayer = isPayer(getSession(), report, false, policy); + const isPaymentsEnabled = arePaymentsEnabled(policy); + const isClosed = isClosedReport(report); + + if (isReportPayer && isPaymentsEnabled && (isApproved || isClosed)) { + return true; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isReimbursed = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + const syncEnabled = isAutoSyncEnabled(policy); + const isReportExported = isExported(getReportActions(report)); + const isFinished = isApproved || isReimbursed || isClosed; + + return isAdmin && isFinished && syncEnabled && !isReportExported; +} + +function isMarkAsExportedAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { + return false; +} + +function isHoldAction(report: Report, policy: Policy, reportTransactions: Transaction[]): boolean { + const isExpense = isExpenseReport(report); + if (!isExpense) { + return false; + } + + const isOnHold = reportTransactions.some(isOnHoldTransactionUtils); + + if (isOnHold) { + return false; + } + + const isOpen = isOpenReport(report); + const isProcessing = isProcessingReport(report); + const isApproved = isReportApproved({report}); + + return isOpen || isProcessing || isApproved; +} + + +function getSecondaryAction( + report: Report, + policy: Policy, + reportTransactions: Transaction[], + violations: OnyxCollection, +): Array> { + const options: Array> = []; + options.push(CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD); + options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS); + + if (isSubmitAction(report, policy)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT); + } + + if (isApproveAction(report, policy, reportTransactions, violations)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.APPROVE); + } + + if (isUnapproveAction(report, policy)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE); + } + + if (isCancelPaymentAction(report, policy, reportTransactions, violations)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT); + } + + if (isExportAction(report, policy)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING); + } + + if (isMarkAsExportedAction(report, policy, reportTransactions, violations)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED); + } + + if (isHoldAction(report, policy, reportTransactions)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD); + } + + if (isChangeWorkspaceAction(report, policy, reportTransactions, violations)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); + } + + return options; +} + +export default getSecondaryAction; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d5c16a6ced03..a06b9badc76f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1589,6 +1589,10 @@ function isProcessingReport(report: OnyxEntry): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED; } +function isOpenReport(report: OnyxEntry): boolean { + return report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; // TODO Verify +} + function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { if (!report) { return false; @@ -9435,6 +9439,7 @@ export { isPolicyExpenseChat, isPolicyExpenseChatAdmin, isProcessingReport, + isOpenReport, isReportIDApproved, isAwaitingFirstLevelApproval, isPublicAnnounceRoom, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 5745dd0b7dc8..39ec2c7a3e35 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -812,43 +812,45 @@ function isBrokenConnectionViolation(violation: TransactionViolation) { ); } +function shouldShowBrokenConnectionViolationInternal(brokenConnectionViolations: TransactionViolation[], report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy) { + if (brokenConnectionViolations.length === 0) { + return false; + } + + if (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) { + return true; + } + + if (isOpenExpenseReport(report)) { + return true; + } + + return isProcessingReport(report) && isInstantSubmitEnabled(policy); +} + /** - * Check if user should see broken connection violation warning. + * Check if user should see broken connection violation warning based on violations list. */ -function shouldShowBrokenConnectionViolation( - transactionOrIDList: Transaction | string[] | undefined, +function shouldShowBrokenConnectionViolation(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, transactionViolations: TransactionViolation[]): boolean { + const brokenConnectionViolations = transactionViolations.filter((violation) => isBrokenConnectionViolation(violation)); + + return shouldShowBrokenConnectionViolationInternal(brokenConnectionViolations, report, policy); +} + +/** + * Check if user should see broken connection violation warning based on selected transactions. + */ +function shouldShowBrokenConnectionViolationForMultipleTransactions( + transactionIDs: string[], report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, - transactionViolations: TransactionViolation[] | OnyxCollection | undefined, + transactionViolations: OnyxCollection, ): boolean { - if (!transactionOrIDList) { - return false; - } - let violations: TransactionViolation[]; - if (Array.isArray(transactionOrIDList)) { - if (Array.isArray(transactionViolations)) { - // This should not be possible except in the case of incorrect type assertions. Generally TS should prevent this at compile time. - throw new Error('Invalid argument combination. If a transactionIDList is passed in, then an OnyxCollection of violations is expected'); - } - violations = transactionOrIDList.flatMap((id) => transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []); - } else { - if (!Array.isArray(transactionViolations)) { - // This should not be possible except in the case of incorrect type assertions. Generally TS should prevent this at compile time. - throw new Error('Invalid argument combination. If a single transaction is passed in, then an array of violations for that transaction is expected'); - } - violations = transactionViolations; - } + const violations = transactionIDs.flatMap((id) => transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []); const brokenConnectionViolations = violations.filter((violation) => isBrokenConnectionViolation(violation)); - if (brokenConnectionViolations.length > 0) { - if (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) { - return true; - } - return isOpenExpenseReport(report) || (isProcessingReport(report) && isInstantSubmitEnabled(policy)); - } - - return false; + return shouldShowBrokenConnectionViolationInternal(brokenConnectionViolations, report, policy); } function checkIfShouldShowMarkAsCashButton(hasRTERVPendingViolation: boolean, shouldDisplayBrokenConnectionViolation: boolean, report: OnyxEntry, policy: OnyxEntry) { @@ -1524,6 +1526,7 @@ export { hasViolation, hasBrokenConnectionViolation, shouldShowBrokenConnectionViolation, + shouldShowBrokenConnectionViolationForMultipleTransactions, hasNoticeTypeViolation, hasWarningTypeViolation, isCustomUnitRateIDForP2P, From 8c98298df60b1bd205cc5ada90796246725f6787 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Tue, 4 Mar 2025 14:59:58 +0100 Subject: [PATCH 2/7] Implement secondary actions --- src/libs/ReportSecondaryActionUtils.ts | 146 +++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 3d302a5e67ae..0b1e38c37afe 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -5,19 +5,19 @@ import type {Policy, Report, Transaction, TransactionViolation} from '@src/types import {isApprover as isApprovedMember} from './actions/Policy/Member'; import {getCurrentUserAccountID} from './actions/Report'; import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; -import {getReportActions, getReportActionsLength} from './ReportActionsUtils'; +import {getReportActions} from './ReportActionsUtils'; import { isClosedReport, isCurrentUserSubmitter, isExpenseReport, isExported, - isHoldCreator, isInvoiceReport, isIOUReport, isOpenReport, isPayer, isProcessingReport, isReportApproved, + isSettled, } from './ReportUtils'; import {getSession} from './SessionUtils'; import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils'; @@ -31,10 +31,13 @@ function isSubmitAction(report: Report, policy: Policy): boolean { const isSubmitter = isCurrentUserSubmitter(report.reportID); const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + if (!isSubmitter && !isApprover) { return false; } + const isOpen = isOpenReport(report); + if (!isOpen) { return false; } @@ -80,8 +83,9 @@ function isUnapproveAction(report: Report, policy: Policy): boolean { return isExpense && isApprover && isApproved; } -function isCancelPaymentAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { +function isCancelPaymentAction(report: Report): boolean { const isExpense = isExpenseReport(report); + if (!isExpense) { return false; } @@ -100,6 +104,7 @@ function isCancelPaymentAction(report: Report, policy: Policy, reportTransaction function isExportAction(report: Report, policy: Policy): boolean { const isInvoice = isInvoiceReport(report); const isSender = true; // TODO is sender the same as submitter? + if (isInvoice && isSender) { return true; } @@ -130,18 +135,58 @@ function isExportAction(report: Report, policy: Policy): boolean { return isAdmin && isFinished && syncEnabled && !isReportExported; } -function isMarkAsExportedAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { +function isMarkAsExportedAction(report: Report, policy: Policy): boolean { + const isInvoice = isInvoiceReport(report); + const isSender = true; // TODO + + if (isInvoice && isSender) { + return true; + } + + const isExpense = isExpenseReport(report); + + if (!isExpense) { + return false; + } + + const isReportPayer = isPayer(getSession(), report, false, policy); + const isPaymentsEnabled = arePaymentsEnabled(policy); + const isApproved = isReportApproved({report}); + const isClosed = isClosedReport(report); + const isClosedOrApproved = isClosed || isApproved; + + if (isReportPayer && isPaymentsEnabled && isClosedOrApproved) { + return true; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isReimbursed = isSettled(report); + const hasAccountingConnection = hasAccountingConnections(policy); + const syncEnabled = isAutoSyncEnabled(policy); + const isFinished = isClosedOrApproved || isReimbursed; + + if (isAdmin && isFinished && hasAccountingConnection && syncEnabled) { + return true; + } + + const isExporter = isPrefferedExporter(policy); + + if (isExporter && isFinished && hasAccountingConnection && !syncEnabled) { + return true; + } + return false; } -function isHoldAction(report: Report, policy: Policy, reportTransactions: Transaction[]): boolean { +function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean { const isExpense = isExpenseReport(report); + if (!isExpense) { return false; } const isOnHold = reportTransactions.some(isOnHoldTransactionUtils); - + if (isOnHold) { return false; } @@ -153,6 +198,85 @@ function isHoldAction(report: Report, policy: Policy, reportTransactions: Transa return isOpen || isProcessing || isApproved; } +function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { + const isExpense = isExpenseReport(report); + const isSubmitter = isCurrentUserSubmitter(report.reportID); + const areWorkflowsEnabled = policy.areWorkflowsEnabled; + const isClosed = isClosedReport(report); + + if (isExpense && isSubmitter && !areWorkflowsEnabled && isClosed) { + return true; + } + + const isOpen = isOpenReport(report); + const isProcessing = isProcessingReport(report); + + if (isSubmitter && (isOpen || isProcessing)) { + return true; + } + + const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + + if (isApprover && isProcessing) { + return true; + } + + const isReportPayer = isPayer(getSession(), report, false, policy); + const isApproved = isReportApproved({report}); + + if (isReportPayer && (isApproved || isClosed)) { + return true; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isReimbursed = isSettled(report); + const transactionIDs = reportTransactions.map((t) => t.transactionID); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations); + + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); + + const userControlsReport = isSubmitter || isApprover || isAdmin; + const hasReceiptMatchViolation = hasAllPendingRTERViolations || (userControlsReport && shouldShowBrokenConnectionViolation); + + if (isAdmin && ((!isExported && (isApproved || isReimbursed || isClosed)) || hasReceiptMatchViolation)) { + return true; + } + + const isIOU = isIOUReport(report); + const hasWorkspaces = true; // TODO + const isReceiver = true; // TODO + if (isIOU && hasWorkspaces && isReceiver && isReimbursed) { + return true; + } + + const isSender = true; // TODO + + if (isSender && isProcessing) { + return true; + } + + return false; +} + +function isDeleteAction(report: Report): boolean { + const isExpense = isExpenseReport(report); + + if (!isExpense) { + return false; + } + + const isSubmitter = isCurrentUserSubmitter(report.reportID); + + if (!isSubmitter) { + return false; + } + + const isOpen = isOpenReport(report); + const isProcessing = isProcessingReport(report); + const isApproved = isReportApproved({report}); + + return isOpen || isProcessing || isApproved; +} function getSecondaryAction( report: Report, @@ -176,7 +300,7 @@ function getSecondaryAction( options.push(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE); } - if (isCancelPaymentAction(report, policy, reportTransactions, violations)) { + if (isCancelPaymentAction(report)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT); } @@ -184,11 +308,11 @@ function getSecondaryAction( options.push(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING); } - if (isMarkAsExportedAction(report, policy, reportTransactions, violations)) { + if (isMarkAsExportedAction(report, policy)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED); } - if (isHoldAction(report, policy, reportTransactions)) { + if (isHoldAction(report, reportTransactions)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD); } @@ -196,6 +320,10 @@ function getSecondaryAction( options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); } + if (isDeleteAction(report)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.DELETE); + } + return options; } From 265eeb38682659f7a9b10f56627a2c358b996328 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Tue, 4 Mar 2025 15:00:35 +0100 Subject: [PATCH 3/7] Remove contradiction from isSettled --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b0a7fe135071..6cf9e7edbd46 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1265,7 +1265,7 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string return false; } - if (isEmptyObject(report) || report.isWaitingOnBankAccount) { + if (isEmptyObject(report)) { return false; } From 1f7eb7bf32504fef5d5a77520a00b66e87763e8a Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 5 Mar 2025 15:22:10 +0100 Subject: [PATCH 4/7] Apply responses for reviews --- src/libs/ReportSecondaryActionUtils.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 0b1e38c37afe..c3ea4ac2f25a 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; import {isApprover as isApprovedMember} from './actions/Policy/Member'; import {getCurrentUserAccountID} from './actions/Report'; -import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; +import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, hasNoPolicyOtherThanPersonalType, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; import {getReportActions} from './ReportActionsUtils'; import { isClosedReport, @@ -17,6 +17,7 @@ import { isPayer, isProcessingReport, isReportApproved, + isReportManager, isSettled, } from './ReportUtils'; import {getSession} from './SessionUtils'; @@ -103,7 +104,7 @@ function isCancelPaymentAction(report: Report): boolean { function isExportAction(report: Report, policy: Policy): boolean { const isInvoice = isInvoiceReport(report); - const isSender = true; // TODO is sender the same as submitter? + const isSender = isCurrentUserSubmitter(report.reportID); if (isInvoice && isSender) { return true; @@ -137,7 +138,7 @@ function isExportAction(report: Report, policy: Policy): boolean { function isMarkAsExportedAction(report: Report, policy: Policy): boolean { const isInvoice = isInvoiceReport(report); - const isSender = true; // TODO + const isSender = isCurrentUserSubmitter(report.reportID); if (isInvoice && isSender) { return true; @@ -180,13 +181,13 @@ function isMarkAsExportedAction(report: Report, policy: Policy): boolean { function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean { const isExpense = isExpenseReport(report); - + if (!isExpense) { return false; } const isOnHold = reportTransactions.some(isOnHoldTransactionUtils); - + if (isOnHold) { return false; } @@ -237,20 +238,21 @@ function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransacti const userControlsReport = isSubmitter || isApprover || isAdmin; const hasReceiptMatchViolation = hasAllPendingRTERViolations || (userControlsReport && shouldShowBrokenConnectionViolation); + const isReportExported = isExported(getReportActions(report)); - if (isAdmin && ((!isExported && (isApproved || isReimbursed || isClosed)) || hasReceiptMatchViolation)) { + if (isAdmin && ((!isReportExported && (isApproved || isReimbursed || isClosed)) || hasReceiptMatchViolation)) { return true; } const isIOU = isIOUReport(report); - const hasWorkspaces = true; // TODO - const isReceiver = true; // TODO - if (isIOU && hasWorkspaces && isReceiver && isReimbursed) { + const hasOnlyPersonalWorkspace = hasNoPolicyOtherThanPersonalType(); + const isReceiver = isReportManager(report); + if (isIOU && !hasOnlyPersonalWorkspace && isReceiver && isReimbursed) { return true; } - const isSender = true; // TODO - + const isSender = isCurrentUserSubmitter(report.reportID); + // it's already satisified in line 215 if (isSender && isProcessing) { return true; } From e45b47197b223a4a06ed200228f00aef777911c3 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 5 Mar 2025 15:22:26 +0100 Subject: [PATCH 5/7] Add tests --- tests/unit/ReportSecondaryActionUtilsTest.ts | 409 +++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 tests/unit/ReportSecondaryActionUtilsTest.ts diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts new file mode 100644 index 000000000000..10daf0863d30 --- /dev/null +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -0,0 +1,409 @@ +import Onyx from 'react-native-onyx'; +import getSecondaryAction from '@libs/ReportSecondaryActionUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; + +const CURRENT_USER_ACCOUNT_ID = 1; +const CURRENT_USER_EMAIL = 'tester@mail.com'; + +const SESSION = { + email: CURRENT_USER_EMAIL, + accountID: CURRENT_USER_ACCOUNT_ID, +}; + +const PERSONAL_DETAILS = { + accountID: CURRENT_USER_ACCOUNT_ID, + login: CURRENT_USER_EMAIL, +}; + +const REPORT_ID = 1; + +describe('getSecondaryAction', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + Onyx.clear(); + await Onyx.merge(ONYXKEYS.SESSION, SESSION); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[CURRENT_USER_ACCOUNT_ID]: PERSONAL_DETAILS}); + }); + + it('should always return default options', () => { + const report = {} as unknown as Report; + const policy = {} as unknown as Policy; + // await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = [CONST.REPORT.SECONDARY_ACTIONS.DOWNLOAD, CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]; + expect(getSecondaryAction(report, policy, [], {})).toEqual(result); + }); + + it('includes SUBMIT option', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + } as unknown as Report; + const policy = { + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + harvesting: { + enabled: true, + }, + } as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT)).toBe(true); + }); + + it('includes APPROVE option for approver and report with duplicates', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + } as unknown as Report; + const policy = { + approver: CURRENT_USER_EMAIL, + } as unknown as Policy; + const TRANSACTION_ID = 'TRANSACTION_ID'; + const transaction = { + transactionID: TRANSACTION_ID, + } as unknown as Transaction; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID}`, transaction); + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`, [ + { + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + } as TransactionViolation, + ]); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [transaction], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE)).toBe(true); + }); + + it('includes APPROVE option for report with RTER violations for all transactions', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + } as unknown as Report; + const policy = {} as unknown as Policy; + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + } as unknown as Transaction; + + const violation = { + name: CONST.VIOLATIONS.RTER, + data: { + pendingPattern: true, + rterType: CONST.RTER_VIOLATION_TYPES.SEVEN_DAY_HOLD, + }, + } as unknown as TransactionViolation; + + const result = getSecondaryAction(report, policy, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE)).toBe(true); + }); + + it('includes APPROVE option for admin and report with broken connection', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + } as unknown as Report; + const policy = {role: CONST.POLICY.ROLE.ADMIN} as unknown as Policy; + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + } as unknown as Transaction; + + const violation = { + name: CONST.VIOLATIONS.RTER, + data: { + rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION, + }, + } as unknown as TransactionViolation; + + const result = getSecondaryAction(report, policy, [transaction], {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`]: [violation]}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.APPROVE)).toBe(true); + }); + + it('includes UNAPPROVE option', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = {approver: CURRENT_USER_EMAIL} as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE)).toBe(true); + }); + + it('includes CANCEL_PAYMENT option', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = {} as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT)).toBe(true); + }); + + it('includes EXPORT option for invoice submitter', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.INVOICE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + } as unknown as Report; + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING)).toBe(true); + }); + + it('includes EXPORT option for expense report with payments enabled', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {}}, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING)).toBe(true); + }); + + it('includes EXPORT option for expense report with payments disabled', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {autosync: {enabled: true}}}}, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.EXPORT_TO_ACCOUNTING)).toBe(true); + }); + + it('includes MARK_AS_EXPORTED option for invoice report sender', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.INVOICE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + } as unknown as Report; + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true); + }); + + it('includes MARK_AS_EXPORTED option for expense report with payments enabled', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true); + }); + + it('includes MARK_AS_EXPORTED option for expense report with payments disabled', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {autosync: {enabled: true}}}}, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true); + }); + + it('includes MARK_AS_EXPORTED option for expense report preffered exporter', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = { + connections: {[CONST.POLICY.CONNECTIONS.NAME.QBD]: {config: {export: {exporter: CURRENT_USER_EMAIL}}}}, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED)).toBe(true); + }); + + it('includes HOLD option ', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + } as unknown as Report; + const policy = {} as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.HOLD)).toBe(true); + }); + + it('includes CHANGE_WORKSPACE option for closed expense report submitter', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + } as unknown as Report; + const policy = { + areWorkflowsEnabled: false, + } as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true); + }); + + it('includes CHANGE_WORKSPACE option for opened expense report submitter', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + } as unknown as Report; + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true); + }); + + it('includes CHANGE_WORKSPACE option for opened expense report submitter', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + const policy = { + approver: CURRENT_USER_EMAIL, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true); + }); + + it('includes CHANGE_WORKSPACE option for approved expense report payer', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true); + }); + + it('includes CHANGE_WORKSPACE option for not exported expense report admin', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + } as unknown as Policy; + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true); + }); + + it('includes CHANGE_WORKSPACE option for IOU report receiver', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.IOU, + managerID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + } as unknown as Report; + const POLICY_ID = 'policyID'; + const policy = { + policyID: POLICY_ID, + type: CONST.POLICY.TYPE.TEAM, + } as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy); + + const result = getSecondaryAction(report, {} as Policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE)).toBe(true); + }); + + it('includes DELETE option for expense report submitter', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + } as unknown as Report; + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryAction(report, policy, [], {}); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true); + }); +}); From 19e344a85f162c589390b69533ceb401993f792f Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Thu, 6 Mar 2025 12:20:54 +0100 Subject: [PATCH 6/7] Add nacha cutoff condition --- src/libs/ReportSecondaryActionUtils.ts | 38 ++++++++++++------ tests/unit/ReportSecondaryActionUtilsTest.ts | 42 ++++++++++++++++++-- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index c3ea4ac2f25a..aaa15749b839 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -1,11 +1,12 @@ +import {fromZonedTime, toZonedTime} from 'date-fns-tz'; import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; import {isApprover as isApprovedMember} from './actions/Policy/Member'; import {getCurrentUserAccountID} from './actions/Report'; import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, hasNoPolicyOtherThanPersonalType, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; -import {getReportActions} from './ReportActionsUtils'; +import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils'; import { isClosedReport, isCurrentUserSubmitter, @@ -84,7 +85,7 @@ function isUnapproveAction(report: Report, policy: Policy): boolean { return isExpense && isApprover && isApproved; } -function isCancelPaymentAction(report: Report): boolean { +function isCancelPaymentAction(report: Report, reportTransactions: Transaction[]): boolean { const isExpense = isExpenseReport(report); if (!isExpense) { @@ -97,8 +98,27 @@ function isCancelPaymentAction(report: Report): boolean { return true; } - const isPaymentProcessing = true; // TODO - const hasDailyNachaCutoffPassed = false; // TODO + const isPaymentProcessing = isSettled(report); + + const payActions = reportTransactions.reduce((acc, transaction) => { + const action = getIOUActionForReportID(report.reportID, transaction.transactionID); + if (action && isPayAction(action)) { + acc.push(action); + } + return acc; + }, [] as ReportAction[]); + + const hasDailyNachaCutoffPassed = payActions.some((action) => { + const actionCreated = fromZonedTime(action.created, 'UTC'); + const actionCreatedTime = actionCreated.getTime(); + const day = actionCreated.getDate(); + const month = actionCreated.getMonth() + 1; // getMonth return 0-11 + const year = actionCreated.getFullYear(); + const cutoff = `${year}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day} 23:45:00`; + const cutoffTime = toZonedTime(cutoff, 'UTC').getTime(); + + return actionCreatedTime > cutoffTime; + }); return isPaymentProcessing && !hasDailyNachaCutoffPassed; } @@ -251,12 +271,6 @@ function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransacti return true; } - const isSender = isCurrentUserSubmitter(report.reportID); - // it's already satisified in line 215 - if (isSender && isProcessing) { - return true; - } - return false; } @@ -302,7 +316,7 @@ function getSecondaryAction( options.push(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE); } - if (isCancelPaymentAction(report)) { + if (isCancelPaymentAction(report, reportTransactions)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT); } diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index 10daf0863d30..560a6003e6b7 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx'; import getSecondaryAction from '@libs/ReportSecondaryActionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; const CURRENT_USER_ACCOUNT_ID = 1; const CURRENT_USER_EMAIL = 'tester@mail.com'; @@ -157,13 +157,13 @@ describe('getSecondaryAction', () => { expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE)).toBe(true); }); - it('includes CANCEL_PAYMENT option', () => { + it('includes CANCEL_PAYMENT option for report paid elsewhere', () => { const report = { reportID: REPORT_ID, type: CONST.REPORT.TYPE.EXPENSE, ownerAccountID: CURRENT_USER_ACCOUNT_ID, stateNum: CONST.REPORT.STATE_NUM.APPROVED, - statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, } as unknown as Report; const policy = {} as unknown as Policy; @@ -171,6 +171,42 @@ describe('getSecondaryAction', () => { expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT)).toBe(true); }); + it('includes CANCEL_PAYMENT option for report before nacha cutoff', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + } as unknown as Report; + const policy = {} as unknown as Policy; + const TRANSACTION_ID = 'transaction_id'; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const ACTION_ID = 'action_id'; + const reportAction = { + actionID: ACTION_ID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + message: { + IOUTransactionID: TRANSACTION_ID, + type: CONST.IOU.REPORT_ACTION_TYPE.PAY, + }, + created: '2025-03-06 18:00:00.000', + } as unknown as ReportAction; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[ACTION_ID]: reportAction}); + + const result = getSecondaryAction( + report, + policy, + [ + { + transactionID: TRANSACTION_ID, + } as unknown as Transaction, + ], + {}, + ); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.CANCEL_PAYMENT)).toBe(true); + }); + it('includes EXPORT option for invoice submitter', async () => { const report = { reportID: REPORT_ID, From cd79a0fcf02174326b4f3eeaece17915829ecb4a Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 7 Mar 2025 10:14:48 +0100 Subject: [PATCH 7/7] Rename variables --- src/libs/ReportPrimaryActionUtils.ts | 98 +++++------ src/libs/ReportSecondaryActionUtils.ts | 220 +++++++++++++------------ 2 files changed, 161 insertions(+), 157 deletions(-) diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index fb42a1b1689f..16a755427a3e 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -2,49 +2,49 @@ import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; -import {isApprover as isApprovedMember} from './actions/Policy/Member'; +import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {getCurrentUserAccountID} from './actions/Report'; -import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; +import {arePaymentsEnabled as arePaymentsEnabledUtils, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; import { - isClosedReport, + isClosedReport as isClosedReportUtils, isCurrentUserSubmitter, - isExpenseReport, + isExpenseReport as isExpenseReportUtils, isHoldCreator, - isInvoiceReport, - isIOUReport, - isOpenReport, + isInvoiceReport as isInvoiceReportUtils, + isIOUReport as isIOUReportUtils, + isOpenReport as isOpenReportUtils, isPayer, - isProcessingReport, - isReportApproved, + isProcessingReport as isProcessingReportUtils, + isReportApproved as isReportApprovedUtils, isSettled, } from './ReportUtils'; import {getSession} from './SessionUtils'; import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils'; function isSubmitAction(report: Report, policy: Policy) { - const isExpense = isExpenseReport(report); - const isSubmitter = isCurrentUserSubmitter(report.reportID); - const isOpen = isOpenReport(report); + const isExpenseReport = isExpenseReportUtils(report); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const isOpenReport = isOpenReportUtils(report); const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL; - return isExpense && isSubmitter && isOpen && isManualSubmitEnabled; + return isExpenseReport && isReportSubmitter && isOpenReport && isManualSubmitEnabled; } function isApproveAction(report: Report, policy: Policy, reportTransactions: Transaction[]) { - const isExpense = isExpenseReport(report); - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isExpenseReport = isExpenseReportUtils(report); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); const isApprovalEnabled = policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; - if (!isExpense || !isApprover || !isApprovalEnabled) { + if (!isExpenseReport || !isReportApprover || !isApprovalEnabled) { return false; } - const isOneExpenseReport = isExpense && reportTransactions.length === 1; - const isOnHold = reportTransactions.some(isOnHoldTransactionUtils); - const isProcessing = isProcessingReport(report); - const isOneExpenseReportOnHold = isOneExpenseReport && isOnHold; + const isOneExpenseReport = isExpenseReport && reportTransactions.length === 1; + const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils); + const isProcessingReport = isProcessingReportUtils(report); + const isOneExpenseReportOnHold = isOneExpenseReport && isReportOnHold; - if (isProcessing || isOneExpenseReportOnHold) { + if (isProcessingReport || isOneExpenseReportOnHold) { return true; } @@ -52,22 +52,22 @@ function isApproveAction(report: Report, policy: Policy, reportTransactions: Tra } function isPayAction(report: Report, policy: Policy) { - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); const isReportPayer = isPayer(getSession(), report, false, policy); - const isPaymentsEnabled = arePaymentsEnabled(policy); - const isApproved = isReportApproved({report}); - const isClosed = isClosedReport(report); - const isFinished = isApproved || isClosed; + const arePaymentsEnabled = arePaymentsEnabledUtils(policy); + const isReportApproved = isReportApprovedUtils({report}); + const isReportClosed = isClosedReportUtils(report); + const isReportFinished = isReportApproved || isReportClosed; - if (isReportPayer && isExpense && isPaymentsEnabled && isFinished) { + if (isReportPayer && isExpenseReport && arePaymentsEnabled && isReportFinished) { return true; } - const isProcessing = isProcessingReport(report); - const isInvoice = isInvoiceReport(report); - const isIOU = isIOUReport(report); + const isProcessingReport = isProcessingReportUtils(report); + const isInvoiceReport = isInvoiceReportUtils(report); + const isIOUReport = isIOUReportUtils(report); - if ((isInvoice || isIOU) && isProcessing) { + if ((isInvoiceReport || isIOUReport) && isProcessingReport) { return true; } @@ -80,8 +80,8 @@ function isExportAction(report: Report, policy: Policy) { return false; } - const isExporter = isPrefferedExporter(policy); - if (!isExporter) { + const isReportExporter = isPrefferedExporter(policy); + if (!isReportExporter) { return false; } @@ -90,11 +90,11 @@ function isExportAction(report: Report, policy: Policy) { return false; } - const isReimbursed = isSettled(report); - const isApproved = isReportApproved({report}); - const isClosed = isClosedReport(report); + const isReportReimbursed = isSettled(report); + const isReportApproved = isReportApprovedUtils({report}); + const isReportClosed = isClosedReportUtils(report); - if (isApproved || isReimbursed || isClosed) { + if (isReportApproved || isReportReimbursed || isReportClosed) { return true; } @@ -102,10 +102,10 @@ function isExportAction(report: Report, policy: Policy) { } function isRemoveHoldAction(report: Report, reportTransactions: Transaction[]) { - const isOnHold = reportTransactions.some(isOnHoldTransactionUtils); + const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils); const isHolder = reportTransactions.some((transaction) => isHoldCreator(transaction, report.reportID)); - return isOnHold && isHolder; + return isReportOnHold && isHolder; } function isReviewDuplicatesAction(report: Report, policy: Policy, reportTransactions: Transaction[]) { @@ -115,15 +115,15 @@ function isReviewDuplicatesAction(report: Report, policy: Policy, reportTransact return false; } - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); - const isSubmitter = isCurrentUserSubmitter(report.reportID); - const isProcessing = isProcessingReport(report); - const isOpen = isOpenReport(report); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const isProcessingReport = isProcessingReportUtils(report); + const isReportOpen = isOpenReportUtils(report); - const isSubmitterOrApprover = isSubmitter || isApprover; - const isActive = isOpen || isProcessing; + const isSubmitterOrApprover = isReportSubmitter || isReportApprover; + const isReportActive = isReportOpen || isProcessingReport; - if (isSubmitterOrApprover && isActive) { + if (isSubmitterOrApprover && isReportActive) { return true; } @@ -138,13 +138,13 @@ function isMarkAsCashAction(report: Report, policy: Policy, reportTransactions: return true; } - const isSubmitter = isCurrentUserSubmitter(report.reportID); - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); - const userControlsReport = isSubmitter || isApprover || isAdmin; + const userControlsReport = isReportSubmitter || isReportApprover || isAdmin; return userControlsReport && shouldShowBrokenConnectionViolation; } diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index aaa15749b839..80545b4404e6 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -1,46 +1,52 @@ -import {fromZonedTime, toZonedTime} from 'date-fns-tz'; import type {OnyxCollection} from 'react-native-onyx'; 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 isApprovedMember} from './actions/Policy/Member'; +import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {getCurrentUserAccountID} from './actions/Report'; -import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, hasNoPolicyOtherThanPersonalType, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; +import { + arePaymentsEnabled as arePaymentsEnabledUtils, + getCorrectedAutoReportingFrequency, + hasAccountingConnections, + hasNoPolicyOtherThanPersonalType, + isAutoSyncEnabled, + isPrefferedExporter, +} from './PolicyUtils'; import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils'; import { - isClosedReport, + isClosedReport as isClosedReportUtils, isCurrentUserSubmitter, - isExpenseReport, - isExported, - isInvoiceReport, - isIOUReport, - isOpenReport, - isPayer, - isProcessingReport, - isReportApproved, - isReportManager, + isExpenseReport as isExpenseReportUtils, + isExported as isExportedUtils, + isInvoiceReport as isInvoiceReportUtils, + isIOUReport as isIOUReportUtils, + isOpenReport as isOpenReportUtils, + isPayer as isPayerUtils, + isProcessingReport as isProcessingReportUtils, + isReportApproved as isReportApprovedUtils, + isReportManager as isReportManagerUtils, isSettled, } from './ReportUtils'; import {getSession} from './SessionUtils'; import {allHavePendingRTERViolation, isDuplicate, isOnHold as isOnHoldTransactionUtils, shouldShowBrokenConnectionViolationForMultipleTransactions} from './TransactionUtils'; function isSubmitAction(report: Report, policy: Policy): boolean { - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); - if (!isExpense) { + if (!isExpenseReport) { return false; } - const isSubmitter = isCurrentUserSubmitter(report.reportID); - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); - if (!isSubmitter && !isApprover) { + if (!isReportSubmitter && !isReportApprover) { return false; } - const isOpen = isOpenReport(report); + const isOpenReport = isOpenReportUtils(report); - if (!isOpen) { + if (!isOpenReport) { return false; } @@ -52,12 +58,12 @@ function isSubmitAction(report: Report, policy: Policy): boolean { } function isApproveAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { - const isExpense = isExpenseReport(report); - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); - const isProcessing = isProcessingReport(report); - const hasDuplicates = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID)); + const isExpenseReport = isExpenseReportUtils(report); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isProcessingReport = isProcessingReportUtils(report); + const reportHasDuplicatedTransactions = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID)); - if (isExpense && isApprover && isProcessing && hasDuplicates) { + if (isExpenseReport && isReportApprover && isProcessingReport && reportHasDuplicatedTransactions) { return true; } @@ -73,28 +79,28 @@ function isApproveAction(report: Report, policy: Policy, reportTransactions: Tra const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); - const userControllsReport = isApprover || isAdmin; - return userControllsReport && shouldShowBrokenConnectionViolation; + const userControlsReport = isReportApprover || isAdmin; + return userControlsReport && shouldShowBrokenConnectionViolation; } function isUnapproveAction(report: Report, policy: Policy): boolean { - const isExpense = isExpenseReport(report); - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); - const isApproved = isReportApproved({report}); + const isExpenseReport = isExpenseReportUtils(report); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isReportApproved = isReportApprovedUtils({report}); - return isExpense && isApprover && isApproved; + return isExpenseReport && isReportApprover && isReportApproved; } function isCancelPaymentAction(report: Report, reportTransactions: Transaction[]): boolean { - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); - if (!isExpense) { + if (!isExpenseReport) { return false; } - const isPaidElsewhere = report.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + const isReportPaidElsewhere = report.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; - if (isPaidElsewhere) { + if (isReportPaidElsewhere) { return true; } @@ -109,90 +115,86 @@ function isCancelPaymentAction(report: Report, reportTransactions: Transaction[] }, [] as ReportAction[]); const hasDailyNachaCutoffPassed = payActions.some((action) => { - const actionCreated = fromZonedTime(action.created, 'UTC'); - const actionCreatedTime = actionCreated.getTime(); - const day = actionCreated.getDate(); - const month = actionCreated.getMonth() + 1; // getMonth return 0-11 - const year = actionCreated.getFullYear(); - const cutoff = `${year}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day} 23:45:00`; - const cutoffTime = toZonedTime(cutoff, 'UTC').getTime(); - - return actionCreatedTime > cutoffTime; + const now = new Date(); + const paymentDatetime = new Date(action.created); + const nowUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds())); + const cutoffTimeUTC = new Date(Date.UTC(paymentDatetime.getUTCFullYear(), paymentDatetime.getUTCMonth(), paymentDatetime.getUTCDate(), 23, 45, 0)); + return nowUTC.getTime() < cutoffTimeUTC.getTime(); }); return isPaymentProcessing && !hasDailyNachaCutoffPassed; } function isExportAction(report: Report, policy: Policy): boolean { - const isInvoice = isInvoiceReport(report); - const isSender = isCurrentUserSubmitter(report.reportID); + const isInvoiceReport = isInvoiceReportUtils(report); + const isReportSender = isCurrentUserSubmitter(report.reportID); - if (isInvoice && isSender) { + if (isInvoiceReport && isReportSender) { return true; } - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); const hasAccountingConnection = hasAccountingConnections(policy); - if (!isExpense || !hasAccountingConnection) { + if (!isExpenseReport || !hasAccountingConnection) { return false; } - const isApproved = isReportApproved({report}); - const isReportPayer = isPayer(getSession(), report, false, policy); - const isPaymentsEnabled = arePaymentsEnabled(policy); - const isClosed = isClosedReport(report); + const isReportApproved = isReportApprovedUtils({report}); + const isReportPayer = isPayerUtils(getSession(), report, false, policy); + const arePaymentsEnabled = arePaymentsEnabledUtils(policy); + const isReportClosed = isClosedReportUtils(report); - if (isReportPayer && isPaymentsEnabled && (isApproved || isClosed)) { + if (isReportPayer && arePaymentsEnabled && (isReportApproved || isReportClosed)) { return true; } const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const isReimbursed = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + const isReportReimbursed = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; const syncEnabled = isAutoSyncEnabled(policy); - const isReportExported = isExported(getReportActions(report)); - const isFinished = isApproved || isReimbursed || isClosed; + const isReportExported = isExportedUtils(getReportActions(report)); + const isReportFinished = isReportApproved || isReportReimbursed || isReportClosed; - return isAdmin && isFinished && syncEnabled && !isReportExported; + return isAdmin && isReportFinished && syncEnabled && !isReportExported; } function isMarkAsExportedAction(report: Report, policy: Policy): boolean { - const isInvoice = isInvoiceReport(report); - const isSender = isCurrentUserSubmitter(report.reportID); + const isInvoiceReport = isInvoiceReportUtils(report); + const isReportSender = isCurrentUserSubmitter(report.reportID); - if (isInvoice && isSender) { + if (isInvoiceReport && isReportSender) { return true; } - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); - if (!isExpense) { + if (!isExpenseReport) { return false; } - const isReportPayer = isPayer(getSession(), report, false, policy); - const isPaymentsEnabled = arePaymentsEnabled(policy); - const isApproved = isReportApproved({report}); - const isClosed = isClosedReport(report); - const isClosedOrApproved = isClosed || isApproved; + const isReportPayer = isPayerUtils(getSession(), report, false, policy); + const arePaymentsEnabled = arePaymentsEnabledUtils(policy); + const isReportApproved = isReportApprovedUtils({report}); + const isReportClosed = isClosedReportUtils(report); + const isReportClosedOrApproved = isReportClosed || isReportApproved; - if (isReportPayer && isPaymentsEnabled && isClosedOrApproved) { + if (isReportPayer && arePaymentsEnabled && isReportClosedOrApproved) { return true; } const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const isReimbursed = isSettled(report); + const isReportReimbursed = isSettled(report); const hasAccountingConnection = hasAccountingConnections(policy); const syncEnabled = isAutoSyncEnabled(policy); - const isFinished = isClosedOrApproved || isReimbursed; + const isReportFinished = isReportClosedOrApproved || isReportReimbursed; - if (isAdmin && isFinished && hasAccountingConnection && syncEnabled) { + if (isAdmin && isReportFinished && hasAccountingConnection && syncEnabled) { return true; } const isExporter = isPrefferedExporter(policy); - if (isExporter && isFinished && hasAccountingConnection && !syncEnabled) { + if (isExporter && isReportFinished && hasAccountingConnection && !syncEnabled) { return true; } @@ -200,74 +202,76 @@ function isMarkAsExportedAction(report: Report, policy: Policy): boolean { } function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean { - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); - if (!isExpense) { + if (!isExpenseReport) { return false; } - const isOnHold = reportTransactions.some(isOnHoldTransactionUtils); + const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils); - if (isOnHold) { + if (isReportOnHold) { return false; } - const isOpen = isOpenReport(report); - const isProcessing = isProcessingReport(report); - const isApproved = isReportApproved({report}); + const isOpenReport = isOpenReportUtils(report); + const isProcessingReport = isProcessingReportUtils(report); + const isReportApproved = isReportApprovedUtils({report}); - return isOpen || isProcessing || isApproved; + return isOpenReport || isProcessingReport || isReportApproved; } function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { - const isExpense = isExpenseReport(report); - const isSubmitter = isCurrentUserSubmitter(report.reportID); + const isExpenseReport = isExpenseReportUtils(report); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); const areWorkflowsEnabled = policy.areWorkflowsEnabled; - const isClosed = isClosedReport(report); + const isClosedReport = isClosedReportUtils(report); - if (isExpense && isSubmitter && !areWorkflowsEnabled && isClosed) { + if (isExpenseReport && isReportSubmitter && !areWorkflowsEnabled && isClosedReport) { return true; } - const isOpen = isOpenReport(report); - const isProcessing = isProcessingReport(report); + const isOpenReport = isOpenReportUtils(report); + const isProcessingReport = isProcessingReportUtils(report); - if (isSubmitter && (isOpen || isProcessing)) { + if (isReportSubmitter && (isOpenReport || isProcessingReport)) { return true; } - const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); - if (isApprover && isProcessing) { + if (isReportApprover && isProcessingReport) { return true; } - const isReportPayer = isPayer(getSession(), report, false, policy); - const isApproved = isReportApproved({report}); + const isReportPayer = isPayerUtils(getSession(), report, false, policy); + const isReportApproved = isReportApprovedUtils({report}); - if (isReportPayer && (isApproved || isClosed)) { + if (isReportPayer && (isReportApproved || isClosedReport)) { return true; } const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const isReimbursed = isSettled(report); + 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 = isSubmitter || isApprover || isAdmin; + const userControlsReport = isReportSubmitter || isReportApprover || isAdmin; const hasReceiptMatchViolation = hasAllPendingRTERViolations || (userControlsReport && shouldShowBrokenConnectionViolation); - const isReportExported = isExported(getReportActions(report)); + const isReportExported = isExportedUtils(getReportActions(report)); + const isReportFinished = isReportApproved || isReportReimbursed || isClosedReport; - if (isAdmin && ((!isReportExported && (isApproved || isReimbursed || isClosed)) || hasReceiptMatchViolation)) { + if (isAdmin && ((!isReportExported && isReportFinished) || hasReceiptMatchViolation)) { return true; } - const isIOU = isIOUReport(report); + const isIOUReport = isIOUReportUtils(report); const hasOnlyPersonalWorkspace = hasNoPolicyOtherThanPersonalType(); - const isReceiver = isReportManager(report); - if (isIOU && !hasOnlyPersonalWorkspace && isReceiver && isReimbursed) { + const isReportReceiver = isReportManagerUtils(report); + + if (isIOUReport && !hasOnlyPersonalWorkspace && isReportReceiver && isReportReimbursed) { return true; } @@ -275,23 +279,23 @@ function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransacti } function isDeleteAction(report: Report): boolean { - const isExpense = isExpenseReport(report); + const isExpenseReport = isExpenseReportUtils(report); - if (!isExpense) { + if (!isExpenseReport) { return false; } - const isSubmitter = isCurrentUserSubmitter(report.reportID); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); - if (!isSubmitter) { + if (!isReportSubmitter) { return false; } - const isOpen = isOpenReport(report); - const isProcessing = isProcessingReport(report); - const isApproved = isReportApproved({report}); + const isReportOpen = isOpenReportUtils(report); + const isProcessingReport = isProcessingReportUtils(report); + const isReportApproved = isReportApprovedUtils({report}); - return isOpen || isProcessing || isApproved; + return isReportOpen || isProcessingReport || isReportApproved; } function getSecondaryAction(