From 81cf18979ba9c934d4eed36deb729ef2fd307e4d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 14 Nov 2024 16:55:46 +0700 Subject: [PATCH 01/16] Update correct next approver with category/tag rules --- src/libs/ReportUtils.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b220c2db20b6..5211ea986bd9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -61,6 +61,7 @@ import * as IOU from './actions/IOU'; import * as PolicyActions from './actions/Policy/Policy'; import * as store from './actions/ReimbursementAccount/store'; import * as SessionUtils from './actions/Session'; +import {getCategoryApproverRule} from './CategoryUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; @@ -6351,7 +6352,7 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV // - Belong to the same workspace // And if any have a violation, then it should have a RBR const allReports = Object.values(ReportConnection.getAllReports() ?? {}) as Report[]; - const potentialReports = allReports.filter((r) => r.ownerAccountID === currentUserAccountID && (r.stateNum ?? 0) <= 1 && r.policyID === report.policyID); + const potentialReports = allReports.filter((r) => r?.ownerAccountID === currentUserAccountID && (r.stateNum ?? 0) <= 1 && r.policyID === report.policyID); return potentialReports.some( (potentialReport) => hasViolations(potentialReport.reportID, transactionViolations) || hasWarningTypeViolations(potentialReport.reportID, transactionViolations), ); @@ -8368,9 +8369,47 @@ function isExported(reportActions: OnyxEntry) { return Object.values(reportActions).some((action) => ReportActionsUtils.isExportIntegrationAction(action)); } +function getRuleApprovers(policy: OnyxEntry, expenseReport: OnyxEntry) { + const categoryAppovers: string[] = []; + const tagApprovers: string[] = []; + const allReportTransactions = TransactionUtils.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 < allReportTransactions.length; i++) { + const transaction = allReportTransactions.at(i); + const tag = TransactionUtils.getTag(transaction); + const category = TransactionUtils.getCategory(transaction); + const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const tagApprover = PolicyUtils.getTagApproverRule(policy?.id ?? '-1', tag)?.approver; + if (categoryAppover) { + categoryAppovers.push(categoryAppover); + } + + if (tagApprover) { + tagApprovers.push(tagApprover); + } + } + + return [...categoryAppovers, ...tagApprovers]; +} + function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; const reportTotal = expenseReport?.total ?? 0; + const submitterEmail = PersonalDetailsUtils.getLoginsByAccountIDs([expenseReport?.ownerAccountID ?? -1]).at(0) ?? ''; + + // Get category/tag approver list + const ruleApprovers = 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 (approvalChain.includes(ruleApprover) || ruleApprover === submitterEmail) { + return; + } + approvalChain.push(ruleApprover); + }); // If the policy is not on advanced approval mode, we should not use the approval chain even if it exists. if (!PolicyUtils.isControlOnAdvancedApprovalMode(policy)) { From 9fa35aba365bb10caa0048a7087f34c3532d50c7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 25 Nov 2024 22:36:08 +0700 Subject: [PATCH 02/16] sort all transactons correctly --- src/libs/PolicyUtils.ts | 4 +-- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils/index.ts | 40 +++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index dc1c07388293..e47e19f3d330 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -37,7 +37,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; @@ -538,7 +538,7 @@ function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntr let categoryAppover; let tagApprover; - const allTransactions = getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); + const allTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-1'); // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. // Category approvers are prioritized, then tag approvers. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ff9e85c24e82..e598289a373c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8387,7 +8387,7 @@ function isExported(reportActions: OnyxEntry) { function getRuleApprovers(policy: OnyxEntry, expenseReport: OnyxEntry) { const categoryAppovers: string[] = []; const tagApprovers: string[] = []; - const allReportTransactions = TransactionUtils.getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); + const allReportTransactions = TransactionUtils.getAllSortedTransactions(expenseReport?.reportID ?? '-1'); // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. // Category approvers are prioritized, then tag approvers. diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 1fc7cad2f456..44058cd416d0 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -25,7 +25,20 @@ import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; +import type { + Beta, + OnyxInputOrEntry, + Policy, + RecentWaypoint, + Report, + ReportAction, + ReviewDuplicates, + TaxRate, + TaxRates, + Transaction, + TransactionViolation, + TransactionViolations, +} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -1218,6 +1231,30 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry> { + // We need sort all transactions by sorting the parent report actions because `created` of the transaction only has format `YYYY-MM-DD` which can cause the wrong sorting + const allCreatedIOUActions = Object.values(ReportActionsUtils.getAllReportActions(iouReportID)) + ?.filter((reportAction): reportAction is ReportAction => { + if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) { + return false; + } + const message = ReportActionsUtils.getOriginalMessage(reportAction); + if (!message?.IOUTransactionID) { + return false; + } + return true; + }) + .sort((actionA, actionB) => (actionA.created < actionB.created ? -1 : 1)); + + return allCreatedIOUActions.map((iouAction) => { + const transactionID = ReportActionsUtils.getOriginalMessage(iouAction)?.IOUTransactionID ?? '-1'; + return getTransaction(transactionID); + }); +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -1301,6 +1338,7 @@ export { getCardName, hasReceiptSource, shouldShowAttendees, + getAllSortedTransactions, }; export type {TransactionChanges}; From a04d1c851bd29b986a724b41c6c135ca66f822e6 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 5 Dec 2024 22:39:19 +0700 Subject: [PATCH 03/16] fix transaction order --- .../ReportActionItem/ReportPreview.tsx | 1 + src/libs/TransactionUtils/index.ts | 25 +++++++------------ src/libs/actions/IOU.ts | 3 ++- src/types/onyx/Transaction.ts | 3 +++ 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 19ab01a27c57..9e01353f6845 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -156,6 +156,7 @@ function ReportPreview({ const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); + console.log(TransactionUtils.getAllSortedTransactions(iouReport?.reportID ?? '-1')); const isApproved = ReportUtils.isReportApproved(iouReport, action); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy); const numberOfRequests = allTransactions.length; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index ede9cc69cc54..08fac743f4dc 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1248,23 +1248,16 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry> { - // We need sort all transactions by sorting the parent report actions because `created` of the transaction only has format `YYYY-MM-DD` which can cause the wrong sorting - const allCreatedIOUActions = Object.values(ReportActionsUtils.getAllReportActions(iouReportID)) - ?.filter((reportAction): reportAction is ReportAction => { - if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) { - return false; - } - const message = ReportActionsUtils.getOriginalMessage(reportAction); - if (!message?.IOUTransactionID) { - return false; - } - return true; - }) - .sort((actionA, actionB) => (actionA.created < actionB.created ? -1 : 1)); + return getAllReportTransactions(iouReportID).sort((transA, transB) => { + if (transA.created < transB.created) { + return -1; + } + + if (transA.created > transB.created) { + return 1; + } - return allCreatedIOUActions.map((iouAction) => { - const transactionID = ReportActionsUtils.getOriginalMessage(iouAction)?.IOUTransactionID ?? '-1'; - return getTransaction(transactionID); + return (transA.inserted ?? '') < (transB.inserted ?? '') ? -1 : 1; }); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 438fba5fef94..9cc2010199cb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7123,6 +7123,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const managerID = isLastApprover(approvalChain) ? expenseReport?.managerID : getNextApproverAccountID(expenseReport); const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, predictedNextStatus); + console.log(expenseReport, predictedNextStatus, optimisticNextStep); const chatReport = ReportUtils.getReportOrDraftReport(expenseReport?.chatReportID); const optimisticReportActionsData: OnyxUpdate = { @@ -7262,7 +7263,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: optimisticHoldReportExpenseActionIDs, }; - API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); + // API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); } function unapproveExpenseReport(expenseReport: OnyxEntry) { diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 547e41463c70..27ecb4ff3bf1 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -461,6 +461,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' >; From c79a16141f209d2753255632e4f4a08624feab54 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 5 Dec 2024 22:40:10 +0700 Subject: [PATCH 04/16] remove log --- src/components/ReportActionItem/ReportPreview.tsx | 1 - src/libs/actions/IOU.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 9e01353f6845..19ab01a27c57 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -156,7 +156,6 @@ function ReportPreview({ const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); - console.log(TransactionUtils.getAllSortedTransactions(iouReport?.reportID ?? '-1')); const isApproved = ReportUtils.isReportApproved(iouReport, action); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy); const numberOfRequests = allTransactions.length; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9cc2010199cb..680f86aa4697 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7123,7 +7123,6 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const managerID = isLastApprover(approvalChain) ? expenseReport?.managerID : getNextApproverAccountID(expenseReport); const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, predictedNextStatus); - console.log(expenseReport, predictedNextStatus, optimisticNextStep); const chatReport = ReportUtils.getReportOrDraftReport(expenseReport?.chatReportID); const optimisticReportActionsData: OnyxUpdate = { From 77eccd92260ff996267aecf27469088e383af6a7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 5 Dec 2024 23:01:18 +0700 Subject: [PATCH 05/16] fix lint --- src/libs/DebugUtils.ts | 2 ++ src/libs/TransactionUtils/index.ts | 14 +------------- src/libs/actions/IOU.ts | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 5687333370f0..81479d18ae38 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -925,6 +925,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) return validateString(value); case 'created': case 'modifiedCreated': + case 'inserted': case 'posted': return validateDate(value); case 'isLoading': @@ -1050,6 +1051,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/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 08fac743f4dc..2ff6e6cbac13 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -25,19 +25,7 @@ import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { - OnyxInputOrEntry, - Policy, - RecentWaypoint, - Report, - ReportAction, - ReviewDuplicates, - TaxRate, - TaxRates, - Transaction, - TransactionViolation, - TransactionViolations, -} from '@src/types/onyx'; +import type {OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 680f86aa4697..438fba5fef94 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7262,7 +7262,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: optimisticHoldReportExpenseActionIDs, }; - // API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); } function unapproveExpenseReport(expenseReport: OnyxEntry) { From 152fa1b3a6162e47a664d2910fa9275096f32f83 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 13 Dec 2024 14:38:52 +0700 Subject: [PATCH 06/16] update inserted in buildOptimisticTransaction --- src/libs/TransactionUtils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 11017d89c993..4e46d9e05cbe 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(), }; } From 54601b2f3e5bb3bc6d3ecd06dfc9ecac70f651f6 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 16 Dec 2024 14:08:46 +0700 Subject: [PATCH 07/16] add submitsTo of owner to approver chain --- src/libs/Permissions.ts | 1 + src/libs/PolicyUtils.ts | 46 ++++++++++++++++++----------------- src/libs/ReportUtils.ts | 22 ++++++++++------- src/libs/SubscriptionUtils.ts | 1 + 4 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 4a7bba3932a3..989b1eb5bc2b 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -6,6 +6,7 @@ import * as SessionUtils from './SessionUtils'; const isAccountIDEven = (accountID: number) => accountID % 2 === 0; function canUseAllBetas(betas: OnyxEntry): boolean { + return true; return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index c274b8aaa84d..a4cd3e7f9904 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -537,33 +537,35 @@ function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { /** * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. */ -function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { +function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry, isDefault = false): number { const employeeAccountID = expenseReport?.ownerAccountID ?? -1; const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; const defaultApprover = getDefaultApprover(policy); - let categoryAppover; - let tagApprover; - const allTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-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 (!isDefault) { + let categoryAppover; + let tagApprover; + const allTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-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 ?? '-1', tag)?.approver) { - tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; + if (!tagApprover && getTagApproverRule(policy ?? '-1', tag)?.approver) { + tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; + } } - } - if (tagApprover) { - return getAccountIDsByLogins([tagApprover]).at(0) ?? -1; + if (tagApprover) { + return getAccountIDsByLogins([tagApprover]).at(0) ?? -1; + } } // For policy using the optional or basic workflow, the manager is the policy default approver. @@ -579,8 +581,8 @@ 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); +function getSubmitToEmail(policy: OnyxEntry, expenseReport: OnyxEntry, isDefault = false): string { + const submitToAccountID = getSubmitToAccountID(policy, expenseReport, isDefault); return getLoginsByAccountIDs([submitToAccountID]).at(0) ?? ''; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2d594f8726f1..497c1948c91f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8498,6 +8498,7 @@ function getRuleApprovers(policy: OnyxEntry, expenseReport: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; + const fullApprovalChain: string[] = []; const reportTotal = expenseReport?.total ?? 0; const submitterEmail = PersonalDetailsUtils.getLoginsByAccountIDs([expenseReport?.ownerAccountID ?? -1]).at(0) ?? ''; @@ -8507,24 +8508,27 @@ function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry { // Don't push submiiter to approve as a rule approver - if (approvalChain.includes(ruleApprover) || ruleApprover === submitterEmail) { + if (fullApprovalChain.includes(ruleApprover) || ruleApprover === submitterEmail) { return; } - approvalChain.push(ruleApprover); + fullApprovalChain.push(ruleApprover); }); - // 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, expenseReport); + let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, expenseReport, true); 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/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 87f750957abc..7530cfd7ea37 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -442,6 +442,7 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { + return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; From 5514599bd6dda118a6ef6a990e77470e253e84b9 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 16 Dec 2024 14:13:35 +0700 Subject: [PATCH 08/16] revert hardcode --- src/libs/SubscriptionUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 7530cfd7ea37..87f750957abc 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -442,7 +442,6 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { - return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; From 28b59d7592e7e4cce1f0bf9e1e6a66b70383d69d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 18 Dec 2024 14:05:10 +0700 Subject: [PATCH 09/16] refactor function --- src/libs/Permissions.ts | 1 - src/libs/PolicyUtils.ts | 83 +++++++++++++++++++++++++++-------------- src/libs/ReportUtils.ts | 30 +-------------- 3 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 996aed107e62..f77f992ede37 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -6,7 +6,6 @@ import * as SessionUtils from './SessionUtils'; const isAccountIDEven = (accountID: number) => accountID % 2 === 0; function canUseAllBetas(betas: OnyxEntry): boolean { - return true; return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a4cd3e7f9904..90e6b3c62bd3 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -534,38 +534,60 @@ function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { return policy?.approver ?? policy?.owner ?? ''; } +function getRuleApprovers(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { + const categoryAppovers: string[] = []; + const tagApprovers: string[] = []; + const allReportTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-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 < allReportTransactions.length; i++) { + const transaction = allReportTransactions.at(i); + const tag = getTag(transaction); + const category = getCategory(transaction); + const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; + if (categoryAppover) { + categoryAppovers.push(categoryAppover); + } + + if (tagApprover) { + tagApprovers.push(tagApprover); + } + } + + return [...categoryAppovers, ...tagApprovers]; +} + +function getManagerAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { + const employeeAccountID = expenseReport?.ownerAccountID ?? -1; + 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))) { + return getAccountIDsByLogins([defaultApprover]).at(0) ?? -1; + } + + const employee = policy?.employeeList?.[employeeLogin]; + if (!employee) { + return -1; + } + + return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover]).at(0) ?? -1; +} + /** * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. */ -function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry, isDefault = false): number { +function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { const employeeAccountID = expenseReport?.ownerAccountID ?? -1; const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; const defaultApprover = getDefaultApprover(policy); - if (!isDefault) { - let categoryAppover; - let tagApprover; - const allTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-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 ?? '-1', tag)?.approver) { - tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; - } - } - - if (tagApprover) { - return getAccountIDsByLogins([tagApprover]).at(0) ?? -1; - } + const ruleApprovers = getRuleApprovers(policy, expenseReport); + if (ruleApprovers.length > 0) { + return getAccountIDsByLogins([ruleApprovers.at(0) ?? '']).at(0) ?? -1; } // For policy using the optional or basic workflow, the manager is the policy default approver. @@ -581,11 +603,16 @@ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseR return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover]).at(0) ?? -1; } -function getSubmitToEmail(policy: OnyxEntry, expenseReport: OnyxEntry, isDefault = false): string { - const submitToAccountID = getSubmitToAccountID(policy, expenseReport, isDefault); +function getSubmitToEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { + const submitToAccountID = getSubmitToAccountID(policy, expenseReport); return getLoginsByAccountIDs([submitToAccountID]).at(0) ?? ''; } +function getManagerAccountEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { + const managerAccountID = getManagerAccountID(policy, expenseReport); + return getLoginsByAccountIDs([managerAccountID]).at(0) ?? ''; +} + /** * Returns the email of the account to forward the report to depending on the approver's approval limit. * Used for advanced approval mode only. @@ -1258,6 +1285,8 @@ export { getActivePolicy, isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, + getManagerAccountEmail, + getRuleApprovers, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 27287a22c0ef..fc0cc77d6f81 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -64,7 +64,6 @@ import * as PriorityModeActions from './actions/PriorityMode'; import * as store from './actions/ReimbursementAccount/store'; import * as ReportHelperActions from './actions/Report'; import * as SessionUtils from './actions/Session'; -import {getCategoryApproverRule} from './CategoryUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; @@ -8471,31 +8470,6 @@ function isExported(reportActions: OnyxEntry) { return Object.values(reportActions).some((action) => ReportActionsUtils.isExportIntegrationAction(action)); } -function getRuleApprovers(policy: OnyxEntry, expenseReport: OnyxEntry) { - const categoryAppovers: string[] = []; - const tagApprovers: string[] = []; - const allReportTransactions = TransactionUtils.getAllSortedTransactions(expenseReport?.reportID ?? '-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 < allReportTransactions.length; i++) { - const transaction = allReportTransactions.at(i); - const tag = TransactionUtils.getTag(transaction); - const category = TransactionUtils.getCategory(transaction); - const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; - const tagApprover = PolicyUtils.getTagApproverRule(policy?.id ?? '-1', tag)?.approver; - if (categoryAppover) { - categoryAppovers.push(categoryAppover); - } - - if (tagApprover) { - tagApprovers.push(tagApprover); - } - } - - return [...categoryAppovers, ...tagApprovers]; -} - function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; const fullApprovalChain: string[] = []; @@ -8503,7 +8477,7 @@ function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry { @@ -8514,7 +8488,7 @@ function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry Date: Wed, 18 Dec 2024 15:38:37 +0700 Subject: [PATCH 10/16] Update src/libs/PolicyUtils.ts Co-authored-by: wentao --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 90e6b3c62bd3..f674c89de608 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -537,7 +537,7 @@ function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { function getRuleApprovers(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { const categoryAppovers: string[] = []; const tagApprovers: string[] = []; - const allReportTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-1'); + 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. From 550a6aea96749fa79c5262f13684c9425daf7a9e Mon Sep 17 00:00:00 2001 From: nkdengineer <161821005+nkdengineer@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:38:46 +0700 Subject: [PATCH 11/16] Update src/libs/TransactionUtils/index.ts Co-authored-by: wentao --- src/libs/TransactionUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 4e46d9e05cbe..2304132e79f1 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1243,7 +1243,7 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry> { +function getAllSortedTransactions(iouReportID?: string): Array> { return getAllReportTransactions(iouReportID).sort((transA, transB) => { if (transA.created < transB.created) { return -1; From aa2d12f0d5ae3ad1f086d62aebe69d40e09aeeda Mon Sep 17 00:00:00 2001 From: nkdengineer <161821005+nkdengineer@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:39:03 +0700 Subject: [PATCH 12/16] Update src/libs/PolicyUtils.ts Co-authored-by: wentao --- src/libs/PolicyUtils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f674c89de608..3ef49b5dafca 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -589,6 +589,8 @@ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseR if (ruleApprovers.length > 0) { return getAccountIDsByLogins([ruleApprovers.at(0) ?? '']).at(0) ?? -1; } + + return getManagerAccountID(policy, expenseReport); // 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))) { From 272b7e37b0c86bab6a7143eb517527d8adac0328 Mon Sep 17 00:00:00 2001 From: nkdengineer <161821005+nkdengineer@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:39:22 +0700 Subject: [PATCH 13/16] Update src/libs/PolicyUtils.ts Co-authored-by: wentao --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 3ef49b5dafca..579cdd5d21ef 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -560,7 +560,7 @@ function getRuleApprovers(policy: OnyxEntry | SearchPolicy, expenseRepor } function getManagerAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry) { - const employeeAccountID = expenseReport?.ownerAccountID ?? -1; + const employeeAccountID = expenseReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; const defaultApprover = getDefaultApprover(policy); From 6585ba84b9412250bccb80e3462f3951558977dd Mon Sep 17 00:00:00 2001 From: nkdengineer <161821005+nkdengineer@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:40:06 +0700 Subject: [PATCH 14/16] Update src/libs/PolicyUtils.ts Co-authored-by: wentao --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 579cdd5d21ef..d6aa20c77fa9 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -546,7 +546,7 @@ function getRuleApprovers(policy: OnyxEntry | SearchPolicy, expenseRepor const tag = getTag(transaction); const category = getCategory(transaction); const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; - const tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; + const tagApprover = getTagApproverRule(policy, tag)?.approver; if (categoryAppover) { categoryAppovers.push(categoryAppover); } From 91a6363b887a2ec77a2d76e799451269844a4534 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 18 Dec 2024 15:41:46 +0700 Subject: [PATCH 15/16] remove getSubmitToEmail --- src/libs/PolicyUtils.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d6aa20c77fa9..84af2f2bfb30 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -581,33 +581,12 @@ function getManagerAccountID(policy: OnyxEntry | SearchPolicy, expenseRe * 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 ?? -1; - const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; - const defaultApprover = getDefaultApprover(policy); - const ruleApprovers = getRuleApprovers(policy, expenseReport); if (ruleApprovers.length > 0) { return getAccountIDsByLogins([ruleApprovers.at(0) ?? '']).at(0) ?? -1; } - - return getManagerAccountID(policy, expenseReport); - - // 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; - } - const employee = policy?.employeeList?.[employeeLogin]; - if (!employee) { - return -1; - } - - 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) ?? ''; + return getManagerAccountID(policy, expenseReport); } function getManagerAccountEmail(policy: OnyxEntry, expenseReport: OnyxEntry): string { @@ -1272,7 +1251,6 @@ export { getCurrentTaxID, areSettingsInErrorFields, settingsPendingAction, - getSubmitToEmail, getForwardsToAccount, getSubmitToAccountID, getWorkspaceAccountID, From a7c0c804e11c34d9b744787667acca1e8ecee7dc Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 18 Dec 2024 21:43:00 +0700 Subject: [PATCH 16/16] return early if the policy is submit and close --- src/libs/PolicyUtils.ts | 2 +- src/libs/ReportUtils.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 84af2f2bfb30..03eae9913a98 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -582,7 +582,7 @@ function getManagerAccountID(policy: OnyxEntry | SearchPolicy, expenseRe */ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { const ruleApprovers = getRuleApprovers(policy, expenseReport); - if (ruleApprovers.length > 0) { + if (ruleApprovers.length > 0 && !isSubmitAndClose(policy)) { return getAccountIDsByLogins([ruleApprovers.at(0) ?? '']).at(0) ?? -1; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fc0cc77d6f81..43780530e614 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8476,6 +8476,10 @@ function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry