Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[No QA] @borys3kk feat implement get report preview action with tests #58238

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
9 changes: 9 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
389 changes: 217 additions & 172 deletions src/components/ReportActionItem/ReportPreview.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Timing from './actions/Timing';
import filterArrayByMatch from './filterArrayByMatch';
import isReportMessageAttachment from './isReportMessageAttachment';
import {formatPhoneNumber} from './LocalePhoneNumber';
import {translate, translateLocal} from './Localize';
import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from './LoginUtils';
Expand Down Expand Up @@ -113,7 +114,6 @@ import {
isIOUOwnedByCurrentUser,
isMoneyRequest,
isPolicyAdmin,
isReportMessageAttachment,
isUnread,
isAdminRoom as reportUtilsIsAdminRoom,
isAnnounceRoom as reportUtilsIsAnnounceRoom,
Expand Down
118 changes: 118 additions & 0 deletions src/libs/ReportPreviewActionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 {
hasViolations as hasAnyViolations,
isClosedReport,
isCurrentUserSubmitter,
isExpenseReport,
isHoldCreator,
isInvoiceReport,
isIOUReport,
isOpenReport,
isPayer,
isProcessingReport,
isReportApproved,
isSettled,
} from './ReportUtils';
import {getSession} from './SessionUtils';

function canSubmit(report: Report, policy: Policy, violations: OnyxCollection<TransactionViolation[]>) {
const isExpense = isExpenseReport(report);
const isSubmitter = isCurrentUserSubmitter(report.reportID);
const isOpen = isOpenReport(report);
const isManualSubmitEnabled = getCorrectedAutoReportingFrequency(policy) === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL;
const hasViolations = hasAnyViolations(report.reportID, violations);

return isExpense && isSubmitter && isOpen && isManualSubmitEnabled && !hasViolations;
}

function canApprove(report: Report, policy: Policy, violations: OnyxCollection<TransactionViolation[]>) {
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 = hasAnyViolations(report.reportID, violations);

return isExpense && isApprover && isProcessing && isApprovalEnabled && !hasViolations;
}

function canPay(report: Report, policy: Policy, violations: OnyxCollection<TransactionViolation[]>) {
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 = hasAnyViolations(report.reportID, violations);
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, violations: OnyxCollection<TransactionViolation[]>) {
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 = hasAnyViolations(report.reportID, violations);

return isExpense && isExporter && (isApproved || isReimbursed || isClosed) && hasAccountingConnection && !syncEnabled && !hasViolations;
}

function canRemoveHold(report: Report, reportTransactions: Transaction[], violations: OnyxCollection<TransactionViolation[]>) {
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 = hasAnyViolations(report.reportID, violations);

return isExpense && isHolder && (isOpen || isProcessing || isApproved) && !hasViolations;
}

function canReview(report: Report, policy: Policy, violations: OnyxCollection<TransactionViolation[]>) {
const hasViolations = hasAnyViolations(report.reportID, violations);
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: OnyxCollection<TransactionViolation[]>,
): ValueOf<typeof CONST.REPORT.REPORT_PREVIEW_ACTIONS> {
if (canSubmit(report, policy, violations)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT;
}
if (canApprove(report, policy, violations)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE;
}
if (canPay(report, policy, violations)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY;
}
if (canExport(report, policy, violations)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.EXPORT_TO_ACCOUNTING;
}
if (canRemoveHold(report, reportTransactions, violations)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.REMOVE_HOLD;
}
if (canReview(report, policy, violations)) {
return CONST.REPORT.REPORT_PREVIEW_ACTIONS.REVIEW;
}

return CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW;
}

export {getReportPreviewAction, canSubmit, canApprove, canPay, canExport, canRemoveHold, canReview, hasAnyViolations};
4 changes: 1 addition & 3 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import {createDraftTransaction, getIOUReportActionToApproveOrPay, setMoneyReques
import {createDraftWorkspace} from './actions/Policy/Policy';
import {autoSwitchToFocusMode} from './actions/PriorityMode';
import {hasCreditBankAccount} from './actions/ReimbursementAccount/store';
import {handleReportChanged, prepareOnboardingOnyxData} from './actions/Report';
import {handleReportChanged} from './actions/Report';
import {isAnonymousUser as isAnonymousUserSession} from './actions/Session';
import {convertToDisplayString} from './CurrencyUtils';
import DateUtils from './DateUtils';
Expand Down Expand Up @@ -9461,7 +9461,6 @@ export {
isReportFieldDisabled,
isReportFieldOfTypeTitle,
isReportManager,
isReportMessageAttachment,
isReportOwner,
isReportParticipant,
isSelfDM,
Expand Down Expand Up @@ -9548,7 +9547,6 @@ export {
getReportMetadata,
buildOptimisticSelfDMReport,
isHiddenForCurrentUser,
prepareOnboardingOnyxData,
getReportSubtitlePrefix,
};

Expand Down
4 changes: 3 additions & 1 deletion src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {ReportExportType} from '@components/ButtonWithDropdownMenu/types';
import {prepareOnboardingOnyxData} from '@libs/actions/Report';
import * as API from '@libs/API';
import type {
AddBillingCardAndRequestWorkspaceOwnerChangeParams,
Expand Down Expand Up @@ -2039,7 +2040,7 @@ function buildPolicyData(
};

if (!introSelected?.createWorkspace && engagementChoice && shouldAddOnboardingTasks) {
const onboardingData = ReportUtils.prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], adminsChatReportID, policyID);
const onboardingData = prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], adminsChatReportID, policyID);
if (!onboardingData) {
return {successData, optimisticData, failureData, params};
}
Expand Down Expand Up @@ -2395,6 +2396,7 @@ function buildOptimisticRecentlyUsedCurrencies(currency?: string) {
*
* @returns policyID of the workspace we have created
*/
// eslint-disable-next-line rulesdir/no-call-actions-from-actions
function createWorkspaceFromIOUPayment(iouReport: OnyxEntry<Report>): WorkspaceFromIOUCreationData | undefined {
// This flow only works for IOU reports
if (!ReportUtils.isIOUReportUsingReport(iouReport)) {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/report/PureReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import type {OnyxDataWithErrors} from '@libs/ErrorUtils';
import {getLatestErrorMessageField} from '@libs/ErrorUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import isReportMessageAttachment from '@libs/isReportMessageAttachment';
import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import Permissions from '@libs/Permissions';
Expand Down Expand Up @@ -117,7 +118,6 @@ import {
isArchivedNonExpenseReport,
isChatThread,
isCompletedTaskReport,
isReportMessageAttachment,
isTaskReport,
shouldDisplayThreadReplies as shouldDisplayThreadRepliesUtils,
} from '@libs/ReportUtils';
Expand Down
177 changes: 177 additions & 0 deletions tests/actions/ReportPreviewActionUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {canApprove, canExport, canPay, canRemoveHold, canReview, canSubmit} from '@libs/ReportPreviewActionUtils';
import * as ReportUtils from '@libs/ReportUtils';

Check failure on line 4 in tests/actions/ReportPreviewActionUtilsTest.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, ReportViolations, Transaction, TransactionViolation} from '@src/types/onyx';

const CURRENT_USER_ACCOUNT_ID = 1;
const CURRENT_USER_EMAIL = '[email protected]';

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;
const TRANSACTION_ID = 'TRANSACTION_ID';
const VIOLATIONS = {} as OnyxCollection<TransactionViolation[]>;

jest.mock('@libs/ReportUtils', () => ({
...jest.requireActual<typeof ReportUtils>('@libs/ReportUtils'),
hasViolations: jest.fn().mockReturnValue(false),
}));
describe('getReportPreviewAction', () => {
beforeAll(() => {
Onyx.init({
keys: ONYXKEYS,
});
});

beforeEach(async () => {
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, VIOLATIONS)).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, VIOLATIONS)).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, VIOLATIONS)).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, VIOLATIONS)).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, VIOLATIONS)).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 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, [transaction], VIOLATIONS)).toBe(true);
});

it('canReview should return true for reports where there are violations, user is submitter or approver and Workflows are enabled', async () => {
(ReportUtils.hasViolations as jest.Mock).mockReturnValue(true);
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);

await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${TRANSACTION_ID}`, [
{
name: CONST.VIOLATIONS.OVER_LIMIT,
} as TransactionViolation,
]);

expect(canReview(report, policy as Policy, VIOLATIONS)).toBe(true);
});
});
Loading