From 48a81e441c980d952eaef014cf4dcdab1652803b Mon Sep 17 00:00:00 2001 From: borys3kk Date: Thu, 6 Mar 2025 14:50:12 +0100 Subject: [PATCH 01/15] added constants --- src/CONST.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 3dfa6b4b078d..e838ff0c8794 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1124,6 +1124,15 @@ const CONST = { REVIEW_DUPLICATES: 'reviewDuplicates', MARK_AS_CASH: 'markAsCash', }, + REPORT_PREVIEW_ACTIONS : { + VIEW: 'view', + REVIEW: 'review', + SUBMIT: 'submit', + APPROVE: 'approve', + PAY: 'pay', + EXPORT_TO_ACCOUNTING: 'exportToAccounting', + REMOVE_HOLD: 'removeHold' + }, ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts From 7441b93b067c2e91ed4a4bd5acf7d2af96dcf920 Mon Sep 17 00:00:00 2001 From: borys3kk Date: Thu, 6 Mar 2025 17:29:23 +0100 Subject: [PATCH 02/15] implemented required reportpreviewAction functions --- src/libs/ReportPreviewActionUtils.ts | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/libs/ReportPreviewActionUtils.ts diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts new file mode 100644 index 000000000000..aed3c628b1c3 --- /dev/null +++ b/src/libs/ReportPreviewActionUtils.ts @@ -0,0 +1,113 @@ +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, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; +import { + hasReportViolations, + isClosedReport, + isCurrentUserSubmitter, + isExpenseReport, + isHoldCreator, + isInvoiceReport, + isIOUReport, + isOpenReport, + isPayer, + isProcessingReport, + isReportApproved, + isSettled, +} from './ReportUtils'; +import {getSession} from './SessionUtils'; + +function canSubmit(report: Report) { + const isExpense = isExpenseReport(report); + const isSubmitter = isCurrentUserSubmitter(report.reportID); + const isOpen = isOpenReport(report); + const isManualSubmit = null; // TODO find out how to check it + const hasViolations = hasReportViolations(report.reportID); + + return isExpense && isSubmitter && isOpen && isManualSubmit && !hasViolations; +} + +function canApprove(report: Report, policy: Policy) { + const isExpense = isExpenseReport(report); + const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const isProcessing = isProcessingReport(report); + const isApprovalEnabled = policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; + const hasViolations = hasReportViolations(report.reportID); + + return isExpense && isApprover && isProcessing && isApprovalEnabled && !hasViolations; +} + +function canPay(report: Report, policy: Policy) { + const isReportPayer = isPayer(getSession(), report, false, policy); + const isExpense = isExpenseReport(report); + const isPaymentsEnabled = arePaymentsEnabled(policy); + const isApproved = isReportApproved({report}); + const isClosed = isClosedReport(report); + const hasViolations = hasReportViolations(report.reportID); + const isInvoice = isInvoiceReport(report); + const isIOU = isIOUReport(report); + const isProcessing = isProcessingReport(report); + + return isReportPayer && ((isExpense && isPaymentsEnabled && (isApproved || isClosed) && !hasViolations) || ((isInvoice || isIOU) && isReportPayer && isProcessing)); +} + +function canExport(report: Report, policy: Policy) { + const isExpense = isExpenseReport(report); + const isExporter = isPrefferedExporter(policy); + const isApproved = isReportApproved({report}); + const isReimbursed = isSettled(report); + const isClosed = isClosedReport(report); + const hasAccountingConnection = hasAccountingConnections(policy); + const syncEnabled = isAutoSyncEnabled(policy); + const hasViolations = hasReportViolations(report.reportID); + + return isExpense && isExporter && (isApproved || isReimbursed || isClosed) && hasAccountingConnection && !syncEnabled && !hasViolations; +} + +function canRemoveHold(report: Report, policy: Policy, reportTransactions: Transaction[]) { + const isExpense = isExpenseReport(report); + const isHolder = reportTransactions.some((transaction) => isHoldCreator(transaction, report.reportID)); + const isOpen = isOpenReport(report); + const isProcessing = isProcessingReport(report); + const isApproved = isReportApproved({report}); + const hasViolations = hasReportViolations(report.reportID); + + return isExpense && isHolder && (isOpen || isProcessing || isApproved) && !hasViolations; +} + +function canReview(report: Report, policy: Policy) { + const hasViolations = hasReportViolations(report.reportID); + const isSubmitter = isCurrentUserSubmitter(report.reportID); + const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); + const areWorkflowsEnabled = policy.areWorkflowsEnabled; + + return hasViolations && (isSubmitter || isApprover) && areWorkflowsEnabled; +} + +function getReportPreviewAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: TransactionViolation[]): ValueOf { + if (canSubmit(report)) { + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT; + } + if (canApprove(report, policy)) { + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE; + } + if (canPay(report, policy)) { + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY; + } + if (canExport(report, policy)) { + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.EXPORT_TO_ACCOUNTING; + } + if (canRemoveHold(report, policy, reportTransactions)) { + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.REMOVE_HOLD; + } + if (canReview(report, policy)) { + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW; + } + + return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW; +} + +export default getReportPreviewAction; From 6c79f759ff736b1c4fe28c516e635857c6b43482 Mon Sep 17 00:00:00 2001 From: borys3kk Date: Fri, 7 Mar 2025 11:32:31 +0100 Subject: [PATCH 03/15] updated isManualSubmit --- src/libs/ReportPreviewActionUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index aed3c628b1c3..71ec2d27db15 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -3,7 +3,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, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; +import {arePaymentsEnabled, getCorrectedAutoReportingFrequency, hasAccountingConnections, isAutoSyncEnabled, isPrefferedExporter} from './PolicyUtils'; import { hasReportViolations, isClosedReport, @@ -20,14 +20,14 @@ import { } from './ReportUtils'; import {getSession} from './SessionUtils'; -function canSubmit(report: Report) { +function canSubmit(report: Report, policy: Policy) { const isExpense = isExpenseReport(report); const isSubmitter = isCurrentUserSubmitter(report.reportID); const isOpen = isOpenReport(report); - const isManualSubmit = null; // TODO find out how to check it + const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL; const hasViolations = hasReportViolations(report.reportID); - return isExpense && isSubmitter && isOpen && isManualSubmit && !hasViolations; + return isExpense && isSubmitter && isOpen && isManualSubmitEnabled && !hasViolations; } function canApprove(report: Report, policy: Policy) { @@ -88,7 +88,7 @@ function canReview(report: Report, policy: Policy) { } function getReportPreviewAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: TransactionViolation[]): ValueOf { - if (canSubmit(report)) { + if (canSubmit(report, policy)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT; } if (canApprove(report, policy)) { From c4f4530ad749b1b7c071747d2594e3fcdb8a8bf0 Mon Sep 17 00:00:00 2001 From: borys3kk Date: Fri, 7 Mar 2025 23:17:43 +0100 Subject: [PATCH 04/15] test added --- src/libs/ReportPreviewActionUtils.ts | 12 +- tests/actions/ReportPreviewActionUtilsTest.ts | 165 ++++++++++++++++++ 2 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 tests/actions/ReportPreviewActionUtilsTest.ts diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index 71ec2d27db15..75ab7747d2f1 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -1,3 +1,4 @@ +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'; @@ -68,6 +69,7 @@ function canExport(report: Report, policy: Policy) { } function canRemoveHold(report: Report, policy: Policy, reportTransactions: Transaction[]) { + // remove policy from here (unused) const isExpense = isExpenseReport(report); const isHolder = reportTransactions.some((transaction) => isHoldCreator(transaction, report.reportID)); const isOpen = isOpenReport(report); @@ -83,11 +85,15 @@ function canReview(report: Report, policy: Policy) { const isSubmitter = isCurrentUserSubmitter(report.reportID); const isApprover = isApprovedMember(policy, getCurrentUserAccountID()); const areWorkflowsEnabled = policy.areWorkflowsEnabled; - return hasViolations && (isSubmitter || isApprover) && areWorkflowsEnabled; } -function getReportPreviewAction(report: Report, policy: Policy, reportTransactions: Transaction[], violations: TransactionViolation[]): ValueOf { +function getReportPreviewAction( + report: Report, + policy: Policy, + reportTransactions: Transaction[], + violations: OnyxCollection, +): ValueOf { if (canSubmit(report, policy)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT; } @@ -110,4 +116,4 @@ function getReportPreviewAction(report: Report, policy: Policy, reportTransactio return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW; } -export default getReportPreviewAction; +export {getReportPreviewAction, canSubmit, canApprove, canPay, canExport, canRemoveHold, canReview}; diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts new file mode 100644 index 000000000000..4ceda2d7c15b --- /dev/null +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -0,0 +1,165 @@ +import Onyx from 'react-native-onyx'; +import {canApprove, canExport, canPay, canRemoveHold, canReview, canSubmit} from '@libs/ReportPreviewActionUtils'; +import {hasReportViolations} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, ReportAction, ReportViolations, Transaction} 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('getReportPreviewAction', () => { + 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('canSubmit should return true for expense preview report with manual submit', 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.IMMEDIATE, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + expect(canSubmit(report, policy as Policy)).toBe(true); + }); + + it('canApprove should return true for report being processed', 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, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + expect(canApprove(report, policy as Policy)).toBe(true); + }); + + it('canPay should return true for expense report with payments enabled', 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 = { + role: CONST.POLICY.ROLE.ADMIN, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + expect(canPay(report, policy as Policy)).toBe(true); + }); + + it('canPay should return true for submitted invoice', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.INVOICE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + expect(canPay(report, policy as Policy)).toBe(true); + }); + + it('canExport should return true for finished reports', 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 = { + connections: { + intacct: { + config: { + export: { + exporter: CURRENT_USER_EMAIL, + }, + }, + }, + }, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + expect(canExport(report, policy as Policy)).toBe(true); + }); + + it('canRemoveHold should return true for reports where user is the holder', 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 = {}; + const REPORT_ACTION_ID = 'REPORT_ACTION_ID'; + const transaction = { + comment: { + hold: REPORT_ACTION_ID, + }, + } as unknown as Transaction; + const reportAction = { + reportActionID: REPORT_ACTION_ID, + actorAccountID: CURRENT_USER_ACCOUNT_ID, + } as unknown as ReportAction; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {REPORT_ACTION_ID: reportAction}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + expect(canRemoveHold(report, policy as Policy, [transaction])).toBe(true); + }); + + it('canReview should return true for reports where there are violations, user is submitter or approver and Workflows are enabled', async () => { + const report = { + reportID: REPORT_ID, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + } as unknown as Report; + const policy = { + areWorkflowsEnabled: true, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + const REPORT_VIOLATION = { + FIELD_REQUIRED: 'fieldRequired', + } as unknown as ReportViolations; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${REPORT_ID}`, REPORT_VIOLATION); + + expect(canReview(report, policy as Policy)).toBe(true); + }); +}); From fd953809db4701b81dfa20455ac09c929f5ac360 Mon Sep 17 00:00:00 2001 From: borys3kk Date: Mon, 10 Mar 2025 16:37:31 +0100 Subject: [PATCH 05/15] started adding UI changes --- .../ReportActionItem/ReportPreview.tsx | 58 ++++++++++++++++++- tests/actions/ReportPreviewActionUtilsTest.ts | 1 - 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index e6c5af32135f..5da9de54a403 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -25,6 +25,7 @@ import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransact import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; +import {exportToIntegration} from '@libs/actions/Report'; import ControlSelection from '@libs/ControlSelection'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; @@ -33,6 +34,7 @@ import Performance from '@libs/Performance'; import {getConnectedIntegration} from '@libs/PolicyUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; import {getReportActionText} from '@libs/ReportActionsUtils'; +import {getReportPreviewAction} from '@libs/ReportPreviewActionUtils'; import { areAllRequestsBeingSmartScanned as areAllRequestsBeingSmartScannedReportUtils, canBeExported, @@ -68,6 +70,7 @@ import { isReportOwner, isSettled, isTripRoom as isTripRoomReportUtils, + navigateToDetailsPage, } from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import { @@ -82,7 +85,7 @@ import { } from '@libs/TransactionUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; -import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidIOUActions, canSubmitReport, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU'; +import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidIOUActions, canSubmitReport, payInvoice, payMoneyRequest, submitReport, unholdRequest} from '@userActions/IOU'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -509,6 +512,57 @@ function ReportPreview({ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); }, [iouReportID]); + if (!iouReport) { + return null; + } + if (!policy) { + return null; + } + if (!transactions) { + return null; + } + if (!connectedIntegration) { + return null; + } + const reportPreviewAction = getReportPreviewAction(iouReport, policy, transactions, violations); + + function unholdRequest() { + // mock just for not having red in ide + return; + } + + // TODO check below translationKeys and functions + const reportPreviewActions = { + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT]: { + translationKey: 'common.submit', + callback: () => submitReport(iouReport), + }, + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE]: { + translationKey: 'iou.approve', + callback: () => confirmApproval(), + }, + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY]: { + translationKey: 'iou.pay', + callback: () => exportToIntegration(iouReport.reportID, connectedIntegration), + }, + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.EXPORT_TO_ACCOUNTING]: { + translationKey: 'iou.approve', + callback: () => exportToIntegration(iouReport.reportID, connectedIntegration), + }, + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.REMOVE_HOLD]: { + translationKey: 'iou.removeHold', + callback: () => unholdRequest(), + }, + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW]: { + translationKey: 'common.review', + callback: () => {}, + }, + [CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW]: { + translationKey: 'common.view', + callback: () => navigateToDetailsPage(iouReport), + }, + }; + return ( )} - {shouldShowSubmitButton && ( + {shouldShowSubmitButton && ( // added to reportPreviewActions