Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c1a5e67
add build approval chain
rushatgabhane Jul 7, 2024
a8e29ef
fix lint
rushatgabhane Jul 7, 2024
a9afa16
rename to get
rushatgabhane Jul 7, 2024
a7adbba
add next steps for multiple approvers
rushatgabhane Jul 7, 2024
38c65d0
Update src/libs/PolicyUtils.ts
rushatgabhane Jul 8, 2024
5500e9c
Update src/libs/PolicyUtils.ts
rushatgabhane Jul 8, 2024
a4fdf8e
Update src/libs/PolicyUtils.ts
rushatgabhane Jul 8, 2024
bae63de
fix conflict
rushatgabhane Aug 2, 2024
5aa1ce9
fix conflict
rushatgabhane Aug 2, 2024
b08590d
Merge branch 'main' of github.com:rushatgabhane/exfy into additional-…
rushatgabhane Aug 5, 2024
a697d1b
rm next approval step
rushatgabhane Aug 5, 2024
ae18457
rm has advanced approval
rushatgabhane Aug 5, 2024
c2f2352
keep state as submitted if more than one approver
rushatgabhane Aug 5, 2024
5f2be76
get display name for next approver
rushatgabhane Aug 5, 2024
d387a8e
early return if AA is not enabled
rushatgabhane Aug 5, 2024
2e5612c
check if last approver
rushatgabhane Aug 5, 2024
0d5ce30
fix last approver logic
rushatgabhane Aug 5, 2024
7f0d770
fix next approver logic
rushatgabhane Aug 5, 2024
a78ce47
use ownerAccountID for building approval chain
rushatgabhane Aug 5, 2024
ecbbbaa
fix crash when next approver is undefined
rushatgabhane Aug 5, 2024
43edb0b
Merge branch 'main' of github.com:rushatgabhane/exfy into additional-…
rushatgabhane Aug 5, 2024
8787b3f
Merge branch 'main' of github.com:rushatgabhane/exfy into additional-…
rushatgabhane Aug 7, 2024
1bbbc24
extract display name to a func
rushatgabhane Aug 7, 2024
7f5885c
move approval chain, related methods to report utils
rushatgabhane Aug 7, 2024
1fea5ea
add docs
rushatgabhane Aug 7, 2024
cd89b9e
move submit to policyutils
rushatgabhane Aug 7, 2024
38441ea
handle self submit
rushatgabhane Aug 7, 2024
dcb4926
Revert "handle self submit"
rushatgabhane Aug 7, 2024
ab691ac
cleanup
rushatgabhane Aug 7, 2024
05f5276
Merge branch 'main' into additional-approver
rushatgabhane Aug 7, 2024
916ba40
improve readability of last approver
rushatgabhane Aug 9, 2024
c5bd485
Merge branch 'additional-approver' of github.com:rushatgabhane/exfy i…
rushatgabhane Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/libs/NextStepUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {Message} from '@src/types/onyx/ReportNextStep';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import DateUtils from './DateUtils';
import EmailUtils from './EmailUtils';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PolicyUtils from './PolicyUtils';
import * as ReportUtils from './ReportUtils';

Expand Down Expand Up @@ -59,6 +60,20 @@ function parseMessage(messages: Message[] | undefined) {
return `<next-step>${formattedHtml}</next-step>`;
}

function getNextApproverDisplayName(policy: Policy, ownerAccountID: number, submitToAccountID: number, report: OnyxEntry<Report>) {
const approvalChain = ReportUtils.getApprovalChain(policy, ownerAccountID, report?.total ?? 0);
if (approvalChain.length === 0) {
return ReportUtils.getDisplayNameForParticipant(submitToAccountID);
}

const nextApproverEmail = approvalChain.length === 1 ? approvalChain[0] : approvalChain[approvalChain.indexOf(currentUserEmail) + 1];
if (!nextApproverEmail) {
return ReportUtils.getDisplayNameForParticipant(submitToAccountID);
}

return PersonalDetailsUtils.getPersonalDetailByEmail(nextApproverEmail)?.displayName ?? nextApproverEmail;
}

/**
* Generates an optimistic nextStep based on a current report status and other properties.
*
Expand All @@ -78,7 +93,8 @@ function buildNextStep(report: OnyxEntry<Report>, predictedNextStatus: ValueOf<t
const autoReportingFrequency = PolicyUtils.getCorrectedAutoReportingFrequency(policy);
const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, ownerAccountID);
const ownerDisplayName = ReportUtils.getDisplayNameForParticipant(ownerAccountID);
const managerDisplayName = ReportUtils.getDisplayNameForParticipant(submitToAccountID);
const nextApproverDisplayName = getNextApproverDisplayName(policy, ownerAccountID, submitToAccountID, report);

const reimburserAccountID = PolicyUtils.getReimburserAccountID(policy);
const reimburserDisplayName = ReportUtils.getDisplayNameForParticipant(reimburserAccountID);
const type: ReportNextStep['type'] = 'neutral';
Expand Down Expand Up @@ -180,7 +196,7 @@ function buildNextStep(report: OnyxEntry<Report>, predictedNextStatus: ValueOf<t
text: 'Waiting for ',
},
{
text: managerDisplayName,
text: nextApproverDisplayName,
type: 'strong',
},
{
Expand Down
32 changes: 31 additions & 1 deletion src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,32 @@ function getSubmitToAccountID(policy: OnyxEntry<Policy>, employeeAccountID: numb
return getAccountIDsByLogins([employee.submitsTo ?? defaultApprover])[0];
}

function getSubmitToEmail(policy: OnyxEntry<Policy>, employeeAccountID: number): string {
const submitToAccountID = getSubmitToAccountID(policy, employeeAccountID);
return getLoginsByAccountIDs([submitToAccountID])[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.
*/
function getForwardsToAccount(policy: OnyxEntry<Policy>, employeeEmail: string, reportTotal: number): string {
if (!isControlOnAdvancedApprovalMode(policy)) {
return '';
}

const employee = policy?.employeeList?.[employeeEmail];
if (!employee) {
return '';
}

const positiveReportTotal = Math.abs(reportTotal);
if (employee.approvalLimit && employee.overLimitForwardsTo && positiveReportTotal > employee.approvalLimit) {
return employee.overLimitForwardsTo;
}
return employee.forwardsTo ?? '';
}

/**
* Returns the accountID of the policy reimburser, if not available — falls back to the policy owner.
*/
Expand Down Expand Up @@ -916,7 +942,6 @@ export {
getPolicyBrickRoadIndicatorStatus,
getPolicyEmployeeListByIdWithoutCurrentUser,
getSortedTagKeys,
getSubmitToAccountID,
getTagList,
getTagListName,
getTagLists,
Expand Down Expand Up @@ -984,6 +1009,8 @@ export {
getIntegrationLastSuccessfulDate,
getCurrentConnectionName,
getCustomersOrJobsLabelNetSuite,
getDefaultApprover,
getApprovalWorkflow,
getReimburserAccountID,
isControlPolicy,
isNetSuiteCustomSegmentRecord,
Expand All @@ -994,6 +1021,9 @@ export {
getCurrentTaxID,
areSettingsInErrorFields,
settingsPendingAction,
getSubmitToEmail,
getForwardsToAccount,
getSubmitToAccountID,
};

export type {MemberEmailsToAccountIDs};
18 changes: 18 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7483,6 +7483,23 @@ function isExported(reportActions: OnyxEntry<ReportActions>) {
return Object.values(reportActions).some((action) => ReportActionsUtils.isExportIntegrationAction(action));
}

function getApprovalChain(policy: OnyxEntry<Policy>, employeeAccountID: number, reportTotal: number): string[] {
const approvalChain: string[] = [];

// If the policy is not on advanced approval mode, we should not use the approval chain even if it exists.
if (!PolicyUtils.isControlOnAdvancedApprovalMode(policy)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB - Maybe in a follow-up we can change this to only checking if the policy is not on advanced approval mode - it shouldn't matter if it's control or not (currently advanced approval is only available in control, but this "technically" could change at some point, and we'd have to update this)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhh shoot i used an existing function. i should have asked

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix in a follow up

return approvalChain;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB - Eventually we should change this to return the policy's default approver in this case. But not needed at this exact moment probably since we only call this function for advanced approval policies i believe

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah here some cleanup would be fine. i didn't want the function to be used by non advanced policy otherwise we'll have to test every flow

}

let nextApproverEmail = PolicyUtils.getSubmitToEmail(policy, employeeAccountID);

while (nextApproverEmail && !approvalChain.includes(nextApproverEmail)) {
approvalChain.push(nextApproverEmail);
nextApproverEmail = PolicyUtils.getForwardsToAccount(policy, nextApproverEmail, reportTotal);
}
return approvalChain;
}

export {
addDomainToShortMention,
completeShortMention,
Expand Down Expand Up @@ -7778,6 +7795,7 @@ export {
getReport,
getReportNameValuePairs,
hasReportViolations,
getApprovalChain,
isIndividualInvoiceRoom,
};

Expand Down
19 changes: 16 additions & 3 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6829,6 +6829,13 @@ function hasIOUToApproveOrPay(chatReport: OnyxEntry<OnyxTypes.Report>, excludedI
});
}

function isLastApprover(approvalChain: string[]): boolean {
if (approvalChain.length === 0) {
return true;
}
return approvalChain[approvalChain.length - 1] === currentUserEmail;
}

function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, full?: boolean) {
if (expenseReport?.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(expenseReport.policyID)) {
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(expenseReport.policyID));
Expand All @@ -6842,7 +6849,13 @@ function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, full?:
total = expenseReport?.unheldTotal;
}
const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(total, expenseReport?.currency ?? '', expenseReport?.reportID ?? '-1');
const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED);

const approvalChain = ReportUtils.getApprovalChain(PolicyUtils.getPolicy(expenseReport?.policyID), expenseReport?.ownerAccountID ?? -1, expenseReport?.total ?? 0);

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;

const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, predictedNextStatus);
const chatReport = getReportOrDraftReport(expenseReport?.chatReportID);

const optimisticReportActionsData: OnyxUpdate = {
Expand All @@ -6862,8 +6875,8 @@ function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, full?:
...expenseReport,
lastMessageText: ReportActionsUtils.getReportActionText(optimisticApprovedReportAction),
lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticApprovedReportAction),
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
stateNum: predictedNextState,
statusNum: predictedNextStatus,
pendingFields: {
partial: full ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
Expand Down