diff --git a/src/CONST.ts b/src/CONST.ts
index 73375043bc50..aec496973c52 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -189,6 +189,7 @@ const CONST = {
UNIX_EPOCH: '1970-01-01 00:00:00.000',
MAX_DATE: '9999-12-31',
MIN_DATE: '0001-01-01',
+ ORDINAL_DAY_OF_MONTH: 'do',
},
SMS: {
DOMAIN: '@expensify.sms',
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index a5b0d6707421..c4c85136e846 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -88,8 +88,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
- () => chatReport?.isOwnPolicyExpenseChat && !(policy.harvesting?.enabled ?? policy.isHarvestingEnabled),
- [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled, policy.isHarvestingEnabled],
+ () => chatReport?.isOwnPolicyExpenseChat && !policy.harvesting?.enabled,
+ [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled],
);
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 311e63332f5c..cfe06b2c0a62 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -159,8 +159,8 @@ function ReportPreview({
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
- () => chatReport?.isOwnPolicyExpenseChat && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled),
- [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled, policy?.isHarvestingEnabled],
+ () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled,
+ [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);
const getDisplayAmount = (): string => {
diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts
index a76a7d3c75c4..3b42382b10f9 100644
--- a/src/libs/NextStepUtils.ts
+++ b/src/libs/NextStepUtils.ts
@@ -1,6 +1,29 @@
+import {format, lastDayOfMonth, setDate} from 'date-fns';
import Str from 'expensify-common/lib/str';
+import Onyx from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report, ReportNextStep} from '@src/types/onyx';
import type {Message} from '@src/types/onyx/ReportNextStep';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
+import type {EmptyObject} from '@src/types/utils/EmptyObject';
+import DateUtils from './DateUtils';
import EmailUtils from './EmailUtils';
+import * as PersonalDetailsUtils from './PersonalDetailsUtils';
+import * as ReportUtils from './ReportUtils';
+
+let currentUserAccountID = -1;
+Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (value) => {
+ if (!value) {
+ return;
+ }
+
+ currentUserAccountID = value?.accountID ?? -1;
+ },
+});
function parseMessage(messages: Message[] | undefined) {
let nextStepHTML = '';
@@ -27,5 +50,274 @@ function parseMessage(messages: Message[] | undefined) {
return `${formattedHtml}`;
}
-// eslint-disable-next-line import/prefer-default-export
-export {parseMessage};
+type BuildNextStepParameters = {
+ isPaidWithWallet?: boolean;
+};
+
+/**
+ * Generates an optimistic nextStep based on a current report status and other properties.
+ *
+ * @param report
+ * @param predictedNextStatus - a next expected status of the report
+ * @param parameters.isPaidWithWallet - Whether a report has been paid with the wallet or outside of Expensify
+ * @returns nextStep
+ */
+function buildNextStep(report: Report | EmptyObject, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null {
+ if (!ReportUtils.isExpenseReport(report)) {
+ return null;
+ }
+
+ const {policyID = '', ownerAccountID = -1, managerID = -1} = report;
+ const policy = ReportUtils.getPolicy(policyID);
+ const {submitsTo, harvesting, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy;
+ const isOwner = currentUserAccountID === ownerAccountID;
+ const isManager = currentUserAccountID === managerID;
+ const isSelfApproval = currentUserAccountID === submitsTo;
+ const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? '';
+ const managerDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo) ?? '';
+ const type: ReportNextStep['type'] = 'neutral';
+ let optimisticNextStep: ReportNextStep | null;
+
+ switch (predictedNextStatus) {
+ // Generates an optimistic nextStep once a report has been opened
+ case CONST.REPORT.STATUS_NUM.OPEN:
+ // Self review
+ optimisticNextStep = {
+ type,
+ title: 'Next Steps:',
+ message: [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'submit',
+ type: 'strong',
+ },
+ {
+ text: ' these expenses.',
+ },
+ ],
+ };
+
+ // Scheduled submit enabled
+ if (harvesting?.enabled && autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL) {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ ];
+ let harvestingSuffix = '';
+
+ if (autoReportingFrequency) {
+ const currentDate = new Date();
+ let autoSubmissionDate: Date | null = null;
+ let formattedDate = '';
+
+ if (autoReportingOffset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH) {
+ autoSubmissionDate = lastDayOfMonth(currentDate);
+ } else if (autoReportingOffset === CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH) {
+ const lastBusinessDayOfMonth = DateUtils.getLastBusinessDayOfMonth(currentDate);
+ autoSubmissionDate = setDate(currentDate, lastBusinessDayOfMonth);
+ } else if (autoReportingOffset !== undefined) {
+ autoSubmissionDate = setDate(currentDate, autoReportingOffset);
+ }
+
+ if (autoSubmissionDate) {
+ formattedDate = format(autoSubmissionDate, CONST.DATE.ORDINAL_DAY_OF_MONTH);
+ }
+
+ const harvestingSuffixes: Record, string> = {
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE]: 'later today',
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: 'on Sunday',
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: 'on the 1st and 16th of each month',
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY]: formattedDate ? `on the ${formattedDate} of each month` : '',
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: 'at the end of your trip',
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT]: '',
+ [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: '',
+ };
+
+ if (harvestingSuffixes[autoReportingFrequency]) {
+ harvestingSuffix = ` ${harvestingSuffixes[autoReportingFrequency]}`;
+ }
+ }
+
+ optimisticNextStep.message.push(
+ {
+ text: `automatically submit${harvestingSuffix}!`,
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ );
+ }
+
+ // Prevented self submitting
+ if (isPreventSelfApprovalEnabled && isSelfApproval) {
+ optimisticNextStep.message = [
+ {
+ text: "Oops! Looks like you're submitting to ",
+ },
+ {
+ text: 'yourself',
+ type: 'strong',
+ },
+ {
+ text: '. Approving your own reports is ',
+ },
+ {
+ text: 'forbidden',
+ type: 'strong',
+ },
+ {
+ text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.',
+ },
+ ];
+ }
+
+ break;
+
+ // Generates an optimistic nextStep once a report has been submitted
+ case CONST.REPORT.STATUS_NUM.SUBMITTED: {
+ const verb = isManager ? 'review' : 'approve';
+
+ // Another owner
+ optimisticNextStep = {
+ type,
+ title: 'Next Steps:',
+ message: [
+ {
+ text: ownerLogin,
+ type: 'strong',
+ },
+ {
+ text: ' is waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: verb,
+ type: 'strong',
+ },
+ {
+ text: ' these %expenses.',
+ },
+ ],
+ };
+
+ // Self review & another reviewer
+ if (isOwner) {
+ optimisticNextStep.message = [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: managerDisplayName,
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: verb,
+ type: 'strong',
+ },
+ {
+ text: ' %expenses.',
+ },
+ ];
+ }
+
+ break;
+ }
+
+ // Generates an optimistic nextStep once a report has been approved
+ case CONST.REPORT.STATUS_NUM.APPROVED:
+ // Self review
+ optimisticNextStep = {
+ type,
+ title: 'Next Steps:',
+ message: [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'review',
+ type: 'strong',
+ },
+ {
+ text: ' %expenses.',
+ },
+ ],
+ };
+
+ // Another owner
+ if (!isOwner) {
+ optimisticNextStep.title = 'Finished!';
+ optimisticNextStep.message = [
+ {
+ text: 'No further action required!',
+ },
+ ];
+ }
+
+ break;
+
+ // Generates an optimistic nextStep once a report has been paid
+ case CONST.REPORT.STATUS_NUM.REIMBURSED:
+ // Paid with wallet
+ optimisticNextStep = {
+ type,
+ title: 'Finished!',
+ message: [
+ {
+ text: 'You',
+ type: 'strong',
+ },
+ {
+ text: ' have marked these expenses as ',
+ },
+ {
+ text: 'paid',
+ type: 'strong',
+ },
+ ],
+ };
+
+ // Paid outside of Expensify
+ if (isPaidWithWallet === false) {
+ optimisticNextStep.message?.push({text: ' outside of Expensify'});
+ }
+
+ optimisticNextStep.message?.push({text: '.'});
+
+ break;
+
+ // Resets a nextStep
+ default:
+ optimisticNextStep = null;
+ }
+
+ return optimisticNextStep;
+}
+
+export {parseMessage, buildNextStep};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index e16f879b6913..05fd97f494b7 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -32,6 +32,7 @@ import * as IOUUtils from '@libs/IOUUtils';
import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import * as Localize from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
+import * as NextStepUtils from '@libs/NextStepUtils';
import * as NumberUtils from '@libs/NumberUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Permissions from '@libs/Permissions';
@@ -395,6 +396,7 @@ function buildOnyxDataForMoneyRequest(
policy?: OnyxTypes.Policy | EmptyObject,
policyTags?: OnyxTypes.PolicyTags,
policyCategories?: OnyxTypes.PolicyCategories,
+ optimisticNextStep?: OnyxTypes.ReportNextStep | null,
needsToBeManuallySubmitted = true,
): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] {
const isScanRequest = TransactionUtils.isScanRequest(transaction);
@@ -500,6 +502,14 @@ function buildOnyxDataForMoneyRequest(
});
}
+ if (!isEmptyObject(optimisticNextStep)) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`,
+ value: optimisticNextStep,
+ });
+ }
+
const successData: OnyxUpdate[] = [];
if (isNewChatReport) {
@@ -748,7 +758,7 @@ function getMoneyRequestInformation(
isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy ?? null);
// If the scheduled submit is turned off on the policy, user needs to manually submit the report which is indicated by GBR in LHN
- needsToBeManuallySubmitted = isFromPaidPolicy && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled);
+ needsToBeManuallySubmitted = isFromPaidPolicy && !policy?.harvesting?.enabled;
// If the linked expense report on paid policy is not draft, we need to create a new draft expense report
if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) {
@@ -863,6 +873,8 @@ function getMoneyRequestInformation(
}
: {};
+ const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, needsToBeManuallySubmitted ? CONST.REPORT.STATUS_NUM.OPEN : CONST.REPORT.STATUS_NUM.SUBMITTED);
+
// STEP 5: Build Onyx Data
const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest(
chatReport,
@@ -880,6 +892,7 @@ function getMoneyRequestInformation(
policy,
policyTags,
policyCategories,
+ optimisticNextStep,
needsToBeManuallySubmitted,
);
@@ -3129,6 +3142,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT
}
const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`] ?? null;
+ const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY});
const optimisticData: OnyxUpdate[] = [
{
@@ -3174,6 +3188,11 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT
key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
value: {[iouReport.policyID ?? '']: paymentMethodType},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`,
+ value: optimisticNextStep,
+ },
];
const successData: OnyxUpdate[] = [
@@ -3218,20 +3237,12 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT
key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
value: chatReport,
},
- ];
-
- if (currentNextStep) {
- optimisticData.push({
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`,
- value: null,
- });
- failureData.push({
+ {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`,
value: currentNextStep,
- });
- }
+ },
+ ];
// In case the report preview action is loaded locally, let's update it.
if (optimisticReportPreviewAction) {
@@ -3296,8 +3307,8 @@ function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency:
function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) {
const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null;
-
const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID);
+ const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED);
const optimisticReportActionsData: OnyxUpdate = {
onyxMethod: Onyx.METHOD.MERGE,
@@ -3320,7 +3331,12 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) {
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
},
};
- const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData];
+ const optimisticNextStepData: OnyxUpdate = {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
+ value: optimisticNextStep,
+ };
+ const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData, optimisticNextStepData];
const successData: OnyxUpdate[] = [
{
@@ -3344,20 +3360,12 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) {
},
},
},
- ];
-
- if (currentNextStep) {
- optimisticData.push({
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
- value: null,
- });
- failureData.push({
+ {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
value: currentNextStep,
- });
- }
+ },
+ ];
const parameters: ApproveMoneyRequestParams = {
reportID: expenseReport.reportID,
@@ -3369,11 +3377,11 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) {
function submitReport(expenseReport: OnyxTypes.Report) {
const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null;
-
const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID);
const parentReport = ReportUtils.getReport(expenseReport.parentReportID);
const policy = ReportUtils.getPolicy(expenseReport.policyID);
const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID;
+ const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED);
const optimisticData: OnyxUpdate[] = [
{
@@ -3397,6 +3405,11 @@ function submitReport(expenseReport: OnyxTypes.Report) {
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
+ value: optimisticNextStep,
+ },
];
if (parentReport?.reportID) {
@@ -3442,6 +3455,11 @@ function submitReport(expenseReport: OnyxTypes.Report) {
stateNum: CONST.REPORT.STATE_NUM.OPEN,
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
+ value: currentNextStep,
+ },
];
if (parentReport?.reportID) {
@@ -3455,19 +3473,6 @@ function submitReport(expenseReport: OnyxTypes.Report) {
});
}
- if (currentNextStep) {
- optimisticData.push({
- onyxMethod: Onyx.METHOD.SET,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
- value: null,
- });
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`,
- value: currentNextStep,
- });
- }
-
const parameters: SubmitReportParams = {
reportID: expenseReport.reportID,
managerAccountID: policy.submitsTo ?? expenseReport.managerID,
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 784cd546a961..6b339d6e3ed4 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -85,9 +85,6 @@ type Policy = {
/** The scheduled submit frequency set up on this policy */
autoReportingFrequency?: ValueOf;
- /** @deprecated Whether the scheduled submit is enabled */
- isHarvestingEnabled?: boolean;
-
/** Whether the scheduled submit is enabled */
harvesting?: {
enabled: boolean;
diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts
new file mode 100644
index 000000000000..568c641d2ac5
--- /dev/null
+++ b/tests/unit/NextStepUtilsTest.ts
@@ -0,0 +1,549 @@
+import {format, lastDayOfMonth, setDate} from 'date-fns';
+import Onyx from 'react-native-onyx';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy, Report, ReportNextStep} from '@src/types/onyx';
+import DateUtils from '../../src/libs/DateUtils';
+import * as NextStepUtils from '../../src/libs/NextStepUtils';
+import * as ReportUtils from '../../src/libs/ReportUtils';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+
+Onyx.init({keys: ONYXKEYS});
+
+describe('libs/NextStepUtils', () => {
+ describe('buildNextStep', () => {
+ const currentUserEmail = 'current-user@expensify.com';
+ const currentUserAccountID = 37;
+ const strangeEmail = 'stranger@expensify.com';
+ const strangeAccountID = 50;
+ const policyID = '1';
+ const policy: Policy = {
+ // Important props
+ id: policyID,
+ owner: currentUserEmail,
+ submitsTo: currentUserAccountID,
+ harvesting: {
+ enabled: false,
+ },
+ // Required props
+ name: 'Policy',
+ role: 'admin',
+ type: 'team',
+ outputCurrency: CONST.CURRENCY.USD,
+ isPolicyExpenseChatEnabled: true,
+ };
+ const optimisticNextStep: ReportNextStep = {
+ type: 'neutral',
+ title: '',
+ message: [],
+ };
+ const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report;
+
+ beforeAll(() => {
+ // @ts-expect-error Preset necessary values
+ Onyx.multiSet({
+ [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID},
+ [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]: policy,
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: {
+ [strangeAccountID]: {
+ accountID: strangeAccountID,
+ login: strangeEmail,
+ avatar: '',
+ },
+ },
+ }).then(waitForBatchedUpdates);
+ });
+
+ beforeEach(() => {
+ report.ownerAccountID = currentUserAccountID;
+ report.managerID = currentUserAccountID;
+ optimisticNextStep.title = '';
+ optimisticNextStep.message = [];
+
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy).then(waitForBatchedUpdates);
+ });
+
+ describe('it generates an optimistic nextStep once a report has been opened', () => {
+ test('self review', () => {
+ optimisticNextStep.title = 'Next Steps:';
+ optimisticNextStep.message = [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'submit',
+ type: 'strong',
+ },
+ {
+ text: ' these expenses.',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+
+ describe('scheduled submit enabled', () => {
+ beforeEach(() => {
+ optimisticNextStep.title = 'Next Steps:';
+ });
+
+ test('daily', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: 'automatically submit later today!',
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('weekly', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: 'automatically submit on Sunday!',
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('twice a month', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: 'automatically submit on the 1st and 16th of each month!',
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('monthly on the 2nd', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: 'automatically submit on the 2nd of each month!',
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
+ autoReportingOffset: 2,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('monthly on the last day', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: `automatically submit on the ${format(lastDayOfMonth(new Date()), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`,
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
+ autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('monthly on the last business day', () => {
+ const lastBusinessDayOfMonth = DateUtils.getLastBusinessDayOfMonth(new Date());
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: `automatically submit on the ${format(setDate(new Date(), lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`,
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY,
+ autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('trip', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'These expenses are scheduled to ',
+ },
+ {
+ text: 'automatically submit at the end of your trip!',
+ type: 'strong',
+ },
+ {
+ text: ' No further action required!',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('manual', () => {
+ optimisticNextStep.message = [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'submit',
+ type: 'strong',
+ },
+ {
+ text: ' these expenses.',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL,
+ harvesting: {
+ enabled: true,
+ },
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+ });
+
+ test('prevented self submitting', () => {
+ optimisticNextStep.title = 'Next Steps:';
+ optimisticNextStep.message = [
+ {
+ text: "Oops! Looks like you're submitting to ",
+ },
+ {
+ text: 'yourself',
+ type: 'strong',
+ },
+ {
+ text: '. Approving your own reports is ',
+ },
+ {
+ text: 'forbidden',
+ type: 'strong',
+ },
+ {
+ text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ submitsTo: currentUserAccountID,
+ isPreventSelfApprovalEnabled: true,
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+ });
+
+ describe('it generates an optimistic nextStep once a report has been submitted', () => {
+ test('self review', () => {
+ optimisticNextStep.title = 'Next Steps:';
+ optimisticNextStep.message = [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'review',
+ type: 'strong',
+ },
+ {
+ text: ' %expenses.',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+
+ test('another reviewer', () => {
+ report.managerID = strangeAccountID;
+ optimisticNextStep.title = 'Next Steps:';
+ optimisticNextStep.message = [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: strangeEmail,
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'approve',
+ type: 'strong',
+ },
+ {
+ text: ' %expenses.',
+ },
+ ];
+
+ return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ submitsTo: strangeAccountID,
+ }).then(() => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ test('another owner', () => {
+ report.ownerAccountID = strangeAccountID;
+ optimisticNextStep.title = 'Next Steps:';
+ optimisticNextStep.message = [
+ {
+ text: strangeEmail,
+ type: 'strong',
+ },
+ {
+ text: ' is waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'review',
+ type: 'strong',
+ },
+ {
+ text: ' these %expenses.',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ describe('it generates an optimistic nextStep once a report has been approved', () => {
+ test('self review', () => {
+ optimisticNextStep.title = 'Next Steps:';
+ optimisticNextStep.message = [
+ {
+ text: 'Waiting for ',
+ },
+ {
+ text: 'you',
+ type: 'strong',
+ },
+ {
+ text: ' to ',
+ },
+ {
+ text: 'review',
+ type: 'strong',
+ },
+ {
+ text: ' %expenses.',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+
+ test('another owner', () => {
+ report.ownerAccountID = strangeAccountID;
+ optimisticNextStep.title = 'Finished!';
+ optimisticNextStep.message = [
+ {
+ text: 'No further action required!',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED);
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ describe('it generates an optimistic nextStep once a report has been paid', () => {
+ test('paid with wallet', () => {
+ optimisticNextStep.title = 'Finished!';
+ optimisticNextStep.message = [
+ {
+ text: 'You',
+ type: 'strong',
+ },
+ {
+ text: ' have marked these expenses as ',
+ },
+ {
+ text: 'paid',
+ type: 'strong',
+ },
+ {
+ text: '.',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: true});
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+
+ test('paid outside of Expensify', () => {
+ optimisticNextStep.title = 'Finished!';
+ optimisticNextStep.message = [
+ {
+ text: 'You',
+ type: 'strong',
+ },
+ {
+ text: ' have marked these expenses as ',
+ },
+ {
+ text: 'paid',
+ type: 'strong',
+ },
+ {
+ text: ' outside of Expensify',
+ },
+ {
+ text: '.',
+ },
+ ];
+
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: false});
+
+ expect(result).toMatchObject(optimisticNextStep);
+ });
+ });
+
+ describe('it generates a nullable optimistic nextStep', () => {
+ test('closed status', () => {
+ const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.CLOSED);
+
+ expect(result).toBeNull();
+ });
+ });
+ });
+});