diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index fe8cc3506b3f..7f9ab3fe0dc9 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -79,16 +78,11 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); - const isRequestModifiable = !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); - const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); - let canDeleteRequest = canModifyRequest; + const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction); + const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction; - if (ReportUtils.isPaidGroupPolicyExpenseReport(moneyRequestReport)) { - // If it's a paid policy expense report, only allow deleting the request if it's in draft state or instantly submitted state or the user is the policy admin - canDeleteRequest = - canDeleteRequest && - (ReportUtils.isDraftExpenseReport(moneyRequestReport) || ReportUtils.isExpenseReportWithInstantSubmittedState(moneyRequestReport) || PolicyUtils.isPolicyAdmin(policy)); - } + // If the report supports adding transactions to it, then it also supports deleting transactions from it. + const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { if (isOnHold) { @@ -108,7 +102,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, }, [canDeleteRequest]); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; - if (isRequestModifiable) { + if (canHoldOrUnholdRequest) { const isRequestIOU = parentReport?.type === 'iou'; const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU; const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 70f87a8c7373..4b1fae2a738b 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -225,7 +225,7 @@ function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean { * Checks if policy's scheduled submit / auto reporting frequency is "instant". * Note: Free policies have "instant" submit always enabled. */ -function isInstantSubmitEnabled(policy: OnyxEntry): boolean { +function isInstantSubmitEnabled(policy: OnyxEntry | EmptyObject): boolean { return policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT || policy?.type === CONST.POLICY.TYPE.FREE; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2fb456681cf..31f4999063ab 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -959,14 +959,6 @@ function isProcessingReport(report: OnyxEntry | EmptyObject): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED; } -/** - * Returns true if the policy has `instant` reporting frequency and if the report is still being processed (i.e. submitted state) - */ -function isExpenseReportWithInstantSubmittedState(report: OnyxEntry | EmptyObject): boolean { - const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? null; - return isExpenseReport(report) && isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy); -} - /** * Check if the report is a single chat report that isn't a thread * and personal detail of participant is optimistic data @@ -1256,6 +1248,29 @@ function getChildReportNotificationPreference(reportAction: OnyxEntry): boolean { + if (!isMoneyRequestReport(moneyRequestReport)) { + return false; + } + + if (isReportApproved(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) { + return false; + } + + if (isGroupPolicy(moneyRequestReport) && isProcessingReport(moneyRequestReport) && !PolicyUtils.isInstantSubmitEnabled(getPolicy(moneyRequestReport?.policyID))) { + return false; + } + + return true; +} + /** * Can only delete if the author is this user and the action is an ADDCOMMENT action or an IOU action in an unsettled report, or if the user is a * policy admin @@ -1270,14 +1285,13 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: // For now, users cannot delete split actions const isSplitAction = reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; - if (isSplitAction || isSettled(String(reportAction?.originalMessage?.IOUReportID)) || (!isEmptyObject(report) && isReportApproved(report))) { + if (isSplitAction) { return false; } if (isActionOwner) { - if (!isEmptyObject(report) && isPaidGroupPolicyExpenseReport(report)) { - // If it's a paid policy expense report, only allow deleting the request if it's a draft or is instantly submitted or the user is the policy admin - return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report) || PolicyUtils.isPolicyAdmin(policy); + if (!isEmptyObject(report) && isMoneyRequestReport(report)) { + return canAddOrDeleteTransactions(report); } return true; } @@ -2957,11 +2971,10 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency); const policy = getPolicy(policyID); - const isFree = policy?.type === CONST.POLICY.TYPE.FREE; + const isInstantSubmitEnabled = PolicyUtils.isInstantSubmitEnabled(policy); - // Define the state and status of the report based on whether the policy is free or paid - const stateNum = isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN; - const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; + const stateNum = isInstantSubmitEnabled ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN; + const statusNum = isInstantSubmitEnabled ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; const expenseReport: OptimisticExpenseReport = { reportID: generateReportID(), @@ -4287,7 +4300,6 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return false; } - // In case of expense reports, we have to look at the parent workspace chat to get the isOwnPolicyExpenseChat property let isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat ?? false; if (isExpenseReport(report) && getParentReport(report)) { isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat); @@ -4301,12 +4313,8 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o // User can request money in any IOU report, unless paid, but user can only request money in an expense report // which is tied to their workspace chat. if (isMoneyRequestReport(report)) { - const isOwnExpenseReport = isExpenseReport(report) && isOwnPolicyExpenseChat; - if (isOwnExpenseReport && PolicyUtils.isPaidGroupPolicy(policy)) { - return isDraftExpenseReport(report) || isExpenseReportWithInstantSubmittedState(report); - } - - return (isOwnExpenseReport || isIOUReport(report)) && !isReportApproved(report) && !isSettled(report?.reportID); + const canAddTransactions = canAddOrDeleteTransactions(report); + return isGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions; } // In case of policy expense chat, users can only request money from their own policy expense chat @@ -5075,6 +5083,17 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry | undefined | null, chatReport: OnyxEntry | null): boolean { + return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5106,7 +5125,6 @@ export { isPublicAnnounceRoom, isConciergeChatReport, isProcessingReport, - isExpenseReportWithInstantSubmittedState, isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyAccountIDs, hasExpensifyGuidesEmails, @@ -5278,6 +5296,8 @@ export { canEditRoomVisibility, canEditPolicyDescription, getPolicyDescriptionText, + canAddOrDeleteTransactions, + shouldCreateNewMoneyRequestReport, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5f9657755b02..5d92d2086a57 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -828,37 +828,26 @@ function getMoneyRequestInformation( // STEP 2: Get the money request report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report. // If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic money request report. let iouReport: OnyxEntry = null; - const shouldCreateNewMoneyRequestReport = !moneyRequestReportID && (!chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport)); if (moneyRequestReportID) { iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`] ?? null; - } else if (!shouldCreateNewMoneyRequestReport) { + } else { iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`] ?? null; } - let isFromPaidPolicy = false; - if (isPolicyExpenseChat) { - isFromPaidPolicy = PolicyUtils.isPaidGroupPolicy(policy ?? null); + const shouldCreateNewMoneyRequestReport = ReportUtils.shouldCreateNewMoneyRequestReport(iouReport, chatReport); - // If the linked expense report on paid policy is not draft and not instantly submitted, we need to create a new draft expense report - if (iouReport && isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport) && !ReportUtils.isExpenseReportWithInstantSubmittedState(iouReport)) { - iouReport = null; - } - } - - if (iouReport) { - if (isPolicyExpenseChat) { - iouReport = {...iouReport}; - if (iouReport?.currency === currency && typeof iouReport.total === 'number') { - // Because of the Expense reports are stored as negative values, we subtract the total from the amount - iouReport.total -= amount; - } - } else { - iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency); - } - } else { + if (!iouReport || shouldCreateNewMoneyRequestReport) { iouReport = isPolicyExpenseChat ? ReportUtils.buildOptimisticExpenseReport(chatReport.reportID, chatReport.policyID ?? '', payeeAccountID, amount, currency) : ReportUtils.buildOptimisticIOUReport(payeeAccountID, payerAccountID, amount, chatReport.reportID, currency); + } else if (isPolicyExpenseChat) { + iouReport = {...iouReport}; + if (iouReport?.currency === currency && typeof iouReport.total === 'number') { + // Because of the Expense reports are stored as negative values, we subtract the total from the amount + iouReport.total -= amount; + } + } else { + iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency); } // STEP 3: Build optimistic receipt and transaction @@ -1843,10 +1832,8 @@ function createSplitsAndOnyxData( } // STEP 2: Get existing IOU/Expense report and update its total OR build a new optimistic one - // For Control policy expense chats, if the report is already approved, create a new expense report let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null; - const shouldCreateNewOneOnOneIOUReport = - !oneOnOneIOUReport || (isOwnPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport)); + const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport); if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) { oneOnOneIOUReport = isOwnPolicyExpenseChat @@ -2484,8 +2471,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA } let oneOnOneIOUReport: OneOnOneIOUReport = oneOnOneChatReport?.iouReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`] : null; - const shouldCreateNewOneOnOneIOUReport = - !oneOnOneIOUReport || (isPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport)); + const shouldCreateNewOneOnOneIOUReport = ReportUtils.shouldCreateNewMoneyRequestReport(oneOnOneIOUReport, oneOnOneChatReport); if (!oneOnOneIOUReport || shouldCreateNewOneOnOneIOUReport) { oneOnOneIOUReport = isPolicyExpenseChat diff --git a/tests/unit/ReportUtilsTest.js b/tests/unit/ReportUtilsTest.js index a5b0a5d3c151..9fbea1df862e 100644 --- a/tests/unit/ReportUtilsTest.js +++ b/tests/unit/ReportUtilsTest.js @@ -410,21 +410,26 @@ describe('ReportUtils', () => { }); }); - it("it is a non-open expense report tied to user's own paid policy expense chat", () => { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, { - reportID: '101', - chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - isOwnPolicyExpenseChat: true, - }).then(() => { + it("it is a submitted report tied to user's own policy expense chat and the policy does not have Instant Submit frequency", () => { + const paidPolicy = { + id: '3f54cca8', + type: CONST.POLICY.TYPE.TEAM, + }; + Promise.all([ + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy), + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, { + reportID: '101', + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: true, + }), + ]).then(() => { const report = { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.EXPENSE, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, parentReportID: '101', - }; - const paidPolicy = { - type: CONST.POLICY.TYPE.TEAM, + policyID: paidPolicy.id, }; const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); expect(moneyRequestOptions.length).toBe(0); @@ -498,7 +503,7 @@ describe('ReportUtils', () => { }); }); - it("it is an open expense report tied to user's own paid policy expense chat", () => { + it("it is an open expense report tied to user's own policy expense chat", () => { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}103`, { reportID: '103', chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, @@ -542,6 +547,33 @@ describe('ReportUtils', () => { expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)).toBe(true); }); + + it("it is a submitted expense report in user's own policyExpenseChat and the policy has Instant Submit frequency", () => { + const paidPolicy = { + id: 'ef72dfeb', + type: CONST.POLICY.TYPE.TEAM, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + }; + Promise.all([ + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${paidPolicy.id}`, paidPolicy), + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}101`, { + reportID: '101', + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: true, + }), + ]).then(() => { + const report = { + ...LHNTestUtils.getFakeReport(), + type: CONST.REPORT.TYPE.EXPENSE, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + parentReportID: '101', + policyID: paidPolicy.id, + }; + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs[0]]); + expect(moneyRequestOptions.length).toBe(1); + }); + }); }); describe('return multiple money request option if', () => {