diff --git a/src/CONST.ts b/src/CONST.ts index ba3f24be43d7..f29a57985d12 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1116,6 +1116,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', + }, PRIMARY_ACTIONS: { SUBMIT: 'submit', APPROVE: 'approve', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 6bcd4afc9246..7744e1fe470a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2182,6 +2182,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) { @@ -2333,6 +2337,7 @@ export { getWorkspaceTagUpdateMessage, getWorkspaceReportFieldUpdateMessage, getWorkspaceReportFieldDeleteMessage, + getReportActions, }; export type {LastVisibleMessage}; 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 new file mode 100644 index 000000000000..80545b4404e6 --- /dev/null +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -0,0 +1,350 @@ +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 isApproverUtils} from './actions/Policy/Member'; +import {getCurrentUserAccountID} from './actions/Report'; +import { + arePaymentsEnabled as arePaymentsEnabledUtils, + getCorrectedAutoReportingFrequency, + hasAccountingConnections, + hasNoPolicyOtherThanPersonalType, + isAutoSyncEnabled, + isPrefferedExporter, +} from './PolicyUtils'; +import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils'; +import { + isClosedReport as isClosedReportUtils, + isCurrentUserSubmitter, + 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 isExpenseReport = isExpenseReportUtils(report); + + if (!isExpenseReport) { + return false; + } + + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + + if (!isReportSubmitter && !isReportApprover) { + return false; + } + + const isOpenReport = isOpenReportUtils(report); + + if (!isOpenReport) { + 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 isExpenseReport = isExpenseReportUtils(report); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isProcessingReport = isProcessingReportUtils(report); + const reportHasDuplicatedTransactions = reportTransactions.some((transaction) => isDuplicate(transaction.transactionID)); + + if (isExpenseReport && isReportApprover && isProcessingReport && reportHasDuplicatedTransactions) { + 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 userControlsReport = isReportApprover || isAdmin; + return userControlsReport && shouldShowBrokenConnectionViolation; +} + +function isUnapproveAction(report: Report, policy: Policy): boolean { + const isExpenseReport = isExpenseReportUtils(report); + const isReportApprover = isApproverUtils(policy, getCurrentUserAccountID()); + const isReportApproved = isReportApprovedUtils({report}); + + return isExpenseReport && isReportApprover && isReportApproved; +} + +function isCancelPaymentAction(report: Report, reportTransactions: Transaction[]): boolean { + const isExpenseReport = isExpenseReportUtils(report); + + if (!isExpenseReport) { + return false; + } + + const isReportPaidElsewhere = report.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + + if (isReportPaidElsewhere) { + return true; + } + + 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 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 isInvoiceReport = isInvoiceReportUtils(report); + const isReportSender = isCurrentUserSubmitter(report.reportID); + + if (isInvoiceReport && isReportSender) { + return true; + } + + const isExpenseReport = isExpenseReportUtils(report); + + const hasAccountingConnection = hasAccountingConnections(policy); + + if (!isExpenseReport || !hasAccountingConnection) { + return false; + } + + const isReportApproved = isReportApprovedUtils({report}); + const isReportPayer = isPayerUtils(getSession(), report, false, policy); + const arePaymentsEnabled = arePaymentsEnabledUtils(policy); + const isReportClosed = isClosedReportUtils(report); + + if (isReportPayer && arePaymentsEnabled && (isReportApproved || isReportClosed)) { + return true; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isReportReimbursed = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + const syncEnabled = isAutoSyncEnabled(policy); + const isReportExported = isExportedUtils(getReportActions(report)); + const isReportFinished = isReportApproved || isReportReimbursed || isReportClosed; + + return isAdmin && isReportFinished && syncEnabled && !isReportExported; +} + +function isMarkAsExportedAction(report: Report, policy: Policy): boolean { + const isInvoiceReport = isInvoiceReportUtils(report); + const isReportSender = isCurrentUserSubmitter(report.reportID); + + if (isInvoiceReport && isReportSender) { + return true; + } + + const isExpenseReport = isExpenseReportUtils(report); + + if (!isExpenseReport) { + return false; + } + + 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 && arePaymentsEnabled && isReportClosedOrApproved) { + return true; + } + + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isReportReimbursed = isSettled(report); + const hasAccountingConnection = hasAccountingConnections(policy); + const syncEnabled = isAutoSyncEnabled(policy); + const isReportFinished = isReportClosedOrApproved || isReportReimbursed; + + if (isAdmin && isReportFinished && hasAccountingConnection && syncEnabled) { + return true; + } + + const isExporter = isPrefferedExporter(policy); + + if (isExporter && isReportFinished && hasAccountingConnection && !syncEnabled) { + return true; + } + + return false; +} + +function isHoldAction(report: Report, reportTransactions: Transaction[]): boolean { + const isExpenseReport = isExpenseReportUtils(report); + + if (!isExpenseReport) { + return false; + } + + const isReportOnHold = reportTransactions.some(isOnHoldTransactionUtils); + + if (isReportOnHold) { + return false; + } + + const isOpenReport = isOpenReportUtils(report); + const isProcessingReport = isProcessingReportUtils(report); + const isReportApproved = isReportApprovedUtils({report}); + + return isOpenReport || isProcessingReport || isReportApproved; +} + +function isChangeWorkspaceAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: OnyxCollection): boolean { + const isExpenseReport = isExpenseReportUtils(report); + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const areWorkflowsEnabled = policy.areWorkflowsEnabled; + const isClosedReport = isClosedReportUtils(report); + + 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; +} + +function isDeleteAction(report: Report): boolean { + const isExpenseReport = isExpenseReportUtils(report); + + if (!isExpenseReport) { + return false; + } + + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + + if (!isReportSubmitter) { + return false; + } + + const isReportOpen = isOpenReportUtils(report); + const isProcessingReport = isProcessingReportUtils(report); + const isReportApproved = isReportApprovedUtils({report}); + + return isReportOpen || isProcessingReport || isReportApproved; +} + +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, reportTransactions)) { + 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)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.MARK_AS_EXPORTED); + } + + if (isHoldAction(report, reportTransactions)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.HOLD); + } + + if (isChangeWorkspaceAction(report, policy, reportTransactions, violations)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); + } + + if (isDeleteAction(report)) { + options.push(CONST.REPORT.SECONDARY_ACTIONS.DELETE); + } + + return options; +} + +export default getSecondaryAction; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 454c25a0b6d1..fd637594495c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1266,7 +1266,7 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string return false; } - if (isEmptyObject(report) || report.isWaitingOnBankAccount) { + if (isEmptyObject(report)) { return false; } diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts new file mode 100644 index 000000000000..560a6003e6b7 --- /dev/null +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -0,0 +1,445 @@ +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, ReportAction, 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 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.REIMBURSED, + } 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 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, + 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); + }); +});