diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 9343164db1e8..cfc56559ed35 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -921,6 +921,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) return validateString(value); case 'created': case 'modifiedCreated': + case 'inserted': case 'posted': return validateDate(value); case 'isLoading': @@ -1046,6 +1047,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) cardNumber: CONST.RED_BRICK_ROAD_PENDING_ACTION, managedCard: CONST.RED_BRICK_ROAD_PENDING_ACTION, posted: CONST.RED_BRICK_ROAD_PENDING_ACTION, + inserted: CONST.RED_BRICK_ROAD_PENDING_ACTION, }, 'string', ); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 025d40a54660..458adfd311d4 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -39,7 +39,7 @@ 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'; +import {getAllSortedTransactions, getCategory, getTag} from './TransactionUtils'; type MemberEmailsToAccountIDs = Record; @@ -542,37 +542,35 @@ function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { return policy?.approver ?? policy?.owner ?? ''; } -/** - * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. - */ -function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { - const employeeAccountID = expenseReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; - 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)); +function getRuleApprovers(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { + const categoryAppovers: string[] = []; + const tagApprovers: string[] = []; + const allReportTransactions = getAllSortedTransactions(expenseReport?.reportID); // 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); + for (let i = 0; i < allReportTransactions.length; i++) { + const transaction = allReportTransactions.at(i); const tag = getTag(transaction); const category = getCategory(transaction); - categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const tagApprover = getTagApproverRule(policy, tag)?.approver; if (categoryAppover) { - return getAccountIDsByLogins([categoryAppover]).at(0) ?? -1; + categoryAppovers.push(categoryAppover); } - if (!tagApprover && getTagApproverRule(policy, tag)?.approver) { - tagApprover = getTagApproverRule(policy, tag)?.approver; + if (tagApprover) { + tagApprovers.push(tagApprover); } } - if (tagApprover) { - return getAccountIDsByLogins([tagApprover]).at(0) ?? -1; - } + return [...categoryAppovers, ...tagApprovers]; +} + +function getManagerAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { + const employeeAccountID = expenseReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; + const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; + const defaultApprover = getDefaultApprover(policy); // 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))) { @@ -587,9 +585,21 @@ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseR return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover]).at(0) ?? -1; } -function getSubmitToEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { - const submitToAccountID = getSubmitToAccountID(policy, expenseReport); - return getLoginsByAccountIDs([submitToAccountID]).at(0) ?? ''; +/** + * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. + */ +function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { + const ruleApprovers = getRuleApprovers(policy, expenseReport); + if (ruleApprovers.length > 0 && !isSubmitAndClose(policy)) { + return getAccountIDsByLogins([ruleApprovers.at(0) ?? '']).at(0) ?? -1; + } + + return getManagerAccountID(policy, expenseReport); +} + +function getManagerAccountEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { + const managerAccountID = getManagerAccountID(policy, expenseReport); + return getLoginsByAccountIDs([managerAccountID]).at(0) ?? ''; } /** @@ -1263,7 +1273,6 @@ export { getCurrentTaxID, areSettingsInErrorFields, settingsPendingAction, - getSubmitToEmail, getForwardsToAccount, getSubmitToAccountID, getWorkspaceAccountID, @@ -1279,6 +1288,8 @@ export { getUserFriendlyWorkspaceType, isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, + getManagerAccountEmail, + getRuleApprovers, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6bffaca697c7..f589bb997005 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8472,20 +8472,41 @@ function isExported(reportActions: OnyxEntry) { function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; + const fullApprovalChain: string[] = []; const reportTotal = expenseReport?.total ?? 0; + const submitterEmail = PersonalDetailsUtils.getLoginsByAccountIDs([expenseReport?.ownerAccountID ?? -1]).at(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)) { + if (PolicyUtils.isSubmitAndClose(policy)) { return approvalChain; } - let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, expenseReport); + // Get category/tag approver list + const ruleApprovers = PolicyUtils.getRuleApprovers(policy, expenseReport); + + // Push rule approvers to approvalChain list before submitsTo/forwardsTo approvers + ruleApprovers.forEach((ruleApprover) => { + // Don't push submiiter to approve as a rule approver + if (fullApprovalChain.includes(ruleApprover) || ruleApprover === submitterEmail) { + return; + } + fullApprovalChain.push(ruleApprover); + }); + + let nextApproverEmail = PolicyUtils.getManagerAccountEmail(policy, expenseReport); while (nextApproverEmail && !approvalChain.includes(nextApproverEmail)) { approvalChain.push(nextApproverEmail); nextApproverEmail = PolicyUtils.getForwardsToAccount(policy, nextApproverEmail, reportTotal); } - return approvalChain; + + approvalChain.forEach((approver) => { + if (fullApprovalChain.includes(approver)) { + return; + } + + fullApprovalChain.push(approver); + }); + return fullApprovalChain; } /** diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 81a738f724e0..2304132e79f1 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -189,6 +189,7 @@ function buildOptimisticTransaction( billable, reimbursable, attendees, + inserted: DateUtils.getDBTime(), }; } @@ -1239,6 +1240,23 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry> { + return getAllReportTransactions(iouReportID).sort((transA, transB) => { + if (transA.created < transB.created) { + return -1; + } + + if (transA.created > transB.created) { + return 1; + } + + return (transA.inserted ?? '') < (transB.inserted ?? '') ? -1 : 1; + }); +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -1322,6 +1340,7 @@ export { getCardName, hasReceiptSource, shouldShowAttendees, + getAllSortedTransactions, getFormattedPostedDate, }; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index abd0a2c7a2d6..b6d7326d85cb 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -503,6 +503,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** The card transaction's posted date */ posted?: string; + + /** The inserted time of the transaction */ + inserted?: string; }, keyof Comment | keyof TransactionCustomUnit | 'attendees' >;