diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 3f7b6af9f951..e8da722ac27a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -8,7 +8,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; -import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, TaxRate} from '@src/types/onyx'; +import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, Report, TaxRate} from '@src/types/onyx'; import type {CardFeedData} from '@src/types/onyx/CardFeeds'; import type {ErrorFields, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon'; import type { @@ -31,10 +31,12 @@ import type { import type PolicyEmployee from '@src/types/onyx/PolicyEmployee'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {hasSynchronizationErrorMessage} from './actions/connections'; +import {getCategoryApproverRule} from './CategoryUtils'; import * as Localize from './Localize'; import Navigation from './Navigation/Navigation'; import * as NetworkStore from './Network/NetworkStore'; import {getAccountIDsByLogins, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; +import {getAllReportTransactions, getCategory, getTag} from './TransactionUtils'; type MemberEmailsToAccountIDs = Record; @@ -526,12 +528,37 @@ function getDefaultApprover(policy: OnyxEntry): string { } /** - * Returns the accountID to whom the given employeeAccountID submits reports to in the given Policy. + * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. */ -function getSubmitToAccountID(policy: OnyxEntry, employeeAccountID: number): number { +function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntry): number { + const employeeAccountID = expenseReport?.ownerAccountID ?? -1; const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; const defaultApprover = getDefaultApprover(policy); + let categoryAppover; + let tagApprover; + const allTransactions = getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); + + // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. + // Category approvers are prioritized, then tag approvers. + for (let i = 0; i < allTransactions.length; i++) { + const transaction = allTransactions.at(i); + const tag = getTag(transaction); + const category = getCategory(transaction); + categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + if (categoryAppover) { + return getAccountIDsByLogins([categoryAppover]).at(0) ?? -1; + } + + if (!tagApprover && getTagApproverRule(policy?.id ?? '-1', tag)?.approver) { + tagApprover = getTagApproverRule(policy?.id ?? '-1', tag)?.approver; + } + } + + if (tagApprover) { + return getAccountIDsByLogins([tagApprover]).at(0) ?? -1; + } + // For policy using the optional or basic workflow, the manager is the policy default approver. if (([CONST.POLICY.APPROVAL_MODE.OPTIONAL, CONST.POLICY.APPROVAL_MODE.BASIC] as Array>).includes(getApprovalWorkflow(policy))) { return getAccountIDsByLogins([defaultApprover]).at(0) ?? -1; @@ -545,8 +572,8 @@ function getSubmitToAccountID(policy: OnyxEntry, employeeAccountID: numb return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover]).at(0) ?? -1; } -function getSubmitToEmail(policy: OnyxEntry, employeeAccountID: number): string { - const submitToAccountID = getSubmitToAccountID(policy, employeeAccountID); +function getSubmitToEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { + const submitToAccountID = getSubmitToAccountID(policy, expenseReport); return getLoginsByAccountIDs([submitToAccountID]).at(0) ?? ''; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a62716975c01..91ff084f54dd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1309,7 +1309,7 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return false; } - const submitsToAccountID = PolicyUtils.getSubmitToAccountID(getPolicy(report.policyID), report.ownerAccountID ?? -1); + const submitsToAccountID = PolicyUtils.getSubmitToAccountID(getPolicy(report.policyID), report); return isProcessingReport(report) && submitsToAccountID === report.managerID; } @@ -3160,7 +3160,7 @@ function canEditMoneyRequest(reportAction: OnyxInputOrEntry, approverAcco function isAllowedToSubmitDraftExpenseReport(report: OnyxEntry): boolean { const policy = getPolicy(report?.policyID); - const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, report?.ownerAccountID ?? -1); + const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, report); return isAllowedToApproveExpenseReport(report, submitToAccountID); } @@ -8372,15 +8372,16 @@ function isExported(reportActions: OnyxEntry) { return Object.values(reportActions).some((action) => ReportActionsUtils.isExportIntegrationAction(action)); } -function getApprovalChain(policy: OnyxEntry, employeeAccountID: number, reportTotal: number): string[] { +function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; + const reportTotal = expenseReport?.total ?? 0; // If the policy is not on advanced approval mode, we should not use the approval chain even if it exists. if (!PolicyUtils.isControlOnAdvancedApprovalMode(policy)) { return approvalChain; } - let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, employeeAccountID); + let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, expenseReport); while (nextApproverEmail && !approvalChain.includes(nextApproverEmail)) { approvalChain.push(nextApproverEmail); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1db2555f3393..2dba6bcdb64b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7157,10 +7157,9 @@ function isLastApprover(approvalChain: string[]): boolean { } function getNextApproverAccountID(report: OnyxEntry) { - const ownerAccountID = report?.ownerAccountID ?? -1; const policy = PolicyUtils.getPolicy(report?.policyID); - const approvalChain = ReportUtils.getApprovalChain(policy, ownerAccountID, report?.total ?? 0); - const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, ownerAccountID); + const approvalChain = ReportUtils.getApprovalChain(policy, report); + const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, report); if (approvalChain.length === 0) { return submitToAccountID; @@ -7188,7 +7187,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: } const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(total, expenseReport?.currency ?? '', expenseReport?.reportID ?? '-1'); - const approvalChain = ReportUtils.getApprovalChain(PolicyUtils.getPolicy(expenseReport?.policyID), expenseReport?.ownerAccountID ?? -1, expenseReport?.total ?? 0); + const approvalChain = ReportUtils.getApprovalChain(PolicyUtils.getPolicy(expenseReport?.policyID), expenseReport); const predictedNextStatus = isLastApprover(approvalChain) ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; @@ -7548,7 +7547,7 @@ function submitReport(expenseReport: OnyxTypes.Report) { const parameters: SubmitReportParams = { reportID: expenseReport.reportID, - managerAccountID: PolicyUtils.getSubmitToAccountID(policy, expenseReport.ownerAccountID ?? -1) ?? expenseReport.managerID, + managerAccountID: PolicyUtils.getSubmitToAccountID(policy, expenseReport) ?? expenseReport.managerID, reportActionID: optimisticSubmittedReportAction.reportActionID, };