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(); + }); + }); + }); +});