Skip to content

Commit 70603fd

Browse files
authored
Merge pull request Expensify#75090 from ShridharGoel/waitingPay
Update approval-mode changes to refresh submitted report next steps
2 parents 9539f65 + 516a043 commit 70603fd

4 files changed

Lines changed: 201 additions & 6 deletions

File tree

src/libs/NextStepUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ function buildNextStepNew(params: BuildNextStepNewParams): ReportNextStepDepreca
432432
: {
433433
text: shouldShowFixMessage ? ownerDisplayName : policyOwnerDisplayName,
434434
type: 'strong',
435+
clickToCopyText: (shouldShowFixMessage ? ownerAccountID : policy?.ownerAccountID) === currentUserAccountIDParam ? (currentUserEmailParam ?? '') : '',
435436
},
436437
{
437438
text: ' to ',

src/libs/actions/Policy/Policy.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ import GoogleTagManager from '@libs/GoogleTagManager';
8282
// eslint-disable-next-line @typescript-eslint/no-deprecated
8383
import {translate, translateLocal} from '@libs/Localize';
8484
import Log from '@libs/Log';
85+
import {buildNextStepNew} from '@libs/NextStepUtils';
8586
import * as NumberUtils from '@libs/NumberUtils';
87+
import Permissions from '@libs/Permissions';
8688
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
8789
import * as PhoneNumber from '@libs/PhoneNumber';
8890
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -101,6 +103,7 @@ import type {OnboardingAccounting} from '@src/CONST';
101103
import ONYXKEYS from '@src/ONYXKEYS';
102104
import type {
103105
BankAccountList,
106+
Beta,
104107
CardFeeds,
105108
DuplicateWorkspace,
106109
IntroSelected,
@@ -124,6 +127,7 @@ import type {ErrorFields, Errors, PendingAction} from '@src/types/onyx/OnyxCommo
124127
import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, ProhibitedExpenses, Rate, TaxRate, UberReceiptPartner} from '@src/types/onyx/Policy';
125128
import type {CustomFieldType} from '@src/types/onyx/PolicyEmployee';
126129
import type {NotificationPreference} from '@src/types/onyx/Report';
130+
import type ReportNextStepDeprecated from '@src/types/onyx/ReportNextStepDeprecated';
127131
import type {OnyxData} from '@src/types/onyx/Request';
128132
import {isEmptyObject} from '@src/types/utils/EmptyObject';
129133
import {buildOptimisticMccGroup, buildOptimisticPolicyCategories, buildOptimisticPolicyWithExistingCategories} from './Category';
@@ -208,6 +212,12 @@ type SetWorkspaceReimbursementActionParams = {
208212
shouldUpdateLastPaymentMethod?: boolean;
209213
};
210214

215+
type SetWorkspaceApprovalModeAdditionalData = {
216+
reportNextSteps?: OnyxCollection<ReportNextStepDeprecated>;
217+
transactionViolations?: OnyxCollection<TransactionViolations>;
218+
betas?: Beta[];
219+
};
220+
211221
const deprecatedAllPolicies: OnyxCollection<Policy> = {};
212222
Onyx.connect({
213223
key: ONYXKEYS.COLLECTION.POLICY,
@@ -803,7 +813,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string | undefined, au
803813
API.write(WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET, params, {optimisticData, failureData, successData});
804814
}
805815

806-
function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMode: ValueOf<typeof CONST.POLICY.APPROVAL_MODE>) {
816+
function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMode: ValueOf<typeof CONST.POLICY.APPROVAL_MODE>, additionalData?: SetWorkspaceApprovalModeAdditionalData) {
807817
// This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850
808818
// eslint-disable-next-line @typescript-eslint/no-deprecated
809819
const policy = getPolicy(policyID);
@@ -812,8 +822,63 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo
812822
approver,
813823
approvalMode,
814824
};
825+
const updatedPolicy = {
826+
...(policy ?? {}),
827+
...value,
828+
} as OnyxEntry<Policy>;
829+
830+
const currentUserAccountID = deprecatedSessionAccountID ?? CONST.DEFAULT_NUMBER_ID;
831+
const currentUserEmail = deprecatedSessionEmail ?? '';
832+
833+
const nextStepOptimisticData: OnyxUpdate[] = [];
834+
const nextStepFailureData: OnyxUpdate[] = [];
835+
const shouldUpdateNextSteps = additionalData?.reportNextSteps != null && additionalData?.transactionViolations != null && additionalData?.betas != null;
836+
if (shouldUpdateNextSteps) {
837+
const {reportNextSteps, transactionViolations, betas} = additionalData;
838+
const resolvedTransactionViolations: OnyxCollection<TransactionViolations> = transactionViolations ?? {};
839+
const resolvedReportNextSteps: NonNullable<OnyxCollection<ReportNextStepDeprecated>> = reportNextSteps ?? {};
840+
const resolvedBetas: Beta[] = betas ?? [];
841+
const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, resolvedBetas);
842+
const affectedReports = ReportUtils.getAllPolicyReports(policyID).filter(
843+
(report) => !!report && ReportUtils.isExpenseReport(report) && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED,
844+
);
815845

816-
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
846+
for (const report of affectedReports) {
847+
const reportID = report?.reportID;
848+
849+
if (!reportID) {
850+
continue;
851+
}
852+
853+
const nextStepKey: `${typeof ONYXKEYS.COLLECTION.NEXT_STEP}${string}` = `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`;
854+
const currentNextStep: OnyxEntry<ReportNextStepDeprecated> | null = resolvedReportNextSteps[nextStepKey] ?? null;
855+
const hasViolations = ReportUtils.hasViolations(reportID, resolvedTransactionViolations, currentUserAccountID, currentUserEmail, undefined, undefined, report, updatedPolicy);
856+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- Next step generation uses deprecated "ReportNextStepDeprecated" types (still in active use).
857+
const optimisticNextStep = buildNextStepNew({
858+
report,
859+
policy: updatedPolicy,
860+
currentUserAccountIDParam: currentUserAccountID,
861+
currentUserEmailParam: currentUserEmail,
862+
hasViolations,
863+
isASAPSubmitBetaEnabled,
864+
predictedNextStatus: report?.statusNum ?? CONST.REPORT.STATUS_NUM.SUBMITTED,
865+
});
866+
867+
nextStepOptimisticData.push({
868+
onyxMethod: Onyx.METHOD.MERGE,
869+
key: nextStepKey,
870+
value: optimisticNextStep,
871+
});
872+
873+
nextStepFailureData.push({
874+
onyxMethod: Onyx.METHOD.MERGE,
875+
key: nextStepKey,
876+
value: currentNextStep ?? null,
877+
});
878+
}
879+
}
880+
881+
const optimisticData: OnyxUpdate[] = [
817882
{
818883
onyxMethod: Onyx.METHOD.MERGE,
819884
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
@@ -823,8 +888,11 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo
823888
},
824889
},
825890
];
891+
if (nextStepOptimisticData.length > 0) {
892+
optimisticData.push(...nextStepOptimisticData);
893+
}
826894

827-
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
895+
const failureData: OnyxUpdate[] = [
828896
{
829897
onyxMethod: Onyx.METHOD.MERGE,
830898
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
@@ -837,6 +905,9 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo
837905
},
838906
},
839907
];
908+
if (nextStepFailureData.length > 0) {
909+
failureData.push(...nextStepFailureData);
910+
}
840911

841912
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
842913
{

src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
7979
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
8080
const [cardFeeds] = useCardFeeds(policy?.id);
8181
const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: false});
82+
const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP, {canBeMissing: true});
83+
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
84+
const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
8285
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
8386
const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, {canBeMissing: true});
8487
const workspaceCards = getAllCardsForWorkspace(workspaceAccountID, cardList, cardFeeds);
@@ -146,8 +149,12 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
146149

147150
const confirmDisableApprovals = useCallback(() => {
148151
setIsDisableApprovalsConfirmModalOpen(false);
149-
setWorkspaceApprovalMode(route.params.policyID, policy?.owner ?? '', CONST.POLICY.APPROVAL_MODE.OPTIONAL);
150-
}, [route.params.policyID, policy?.owner]);
152+
setWorkspaceApprovalMode(route.params.policyID, policy?.owner ?? '', CONST.POLICY.APPROVAL_MODE.OPTIONAL, {
153+
reportNextSteps: allReportNextSteps,
154+
transactionViolations,
155+
betas,
156+
});
157+
}, [allReportNextSteps, betas, policy?.owner, route.params.policyID, transactionViolations]);
151158

152159
// User should be allowed to add new Approval Workflow only if he's upgraded to Control Plan, otherwise redirected to the Upgrade Page
153160
const addApprovalAction = useCallback(() => {
@@ -231,7 +238,11 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
231238
setIsDisableApprovalsConfirmModalOpen(true);
232239
return;
233240
}
234-
setWorkspaceApprovalMode(route.params.policyID, policy?.owner ?? '', isEnabled ? updateApprovalMode : CONST.POLICY.APPROVAL_MODE.OPTIONAL);
241+
setWorkspaceApprovalMode(route.params.policyID, policy?.owner ?? '', isEnabled ? updateApprovalMode : CONST.POLICY.APPROVAL_MODE.OPTIONAL, {
242+
reportNextSteps: allReportNextSteps,
243+
transactionViolations,
244+
betas,
245+
});
235246
},
236247
subMenuItems: (
237248
<>
@@ -416,6 +427,9 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
416427
displayNameForAuthorizedPayer,
417428
route.params.policyID,
418429
updateApprovalMode,
430+
allReportNextSteps,
431+
transactionViolations,
432+
betas,
419433
isAccountLocked,
420434
hasValidExistingAccounts,
421435
shouldShowContinueModal,

tests/actions/PolicyTest.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {getOnboardingMessages} from '@libs/actions/Welcome/OnboardingFlow';
55
import {WRITE_COMMANDS} from '@libs/API/types';
66
// eslint-disable-next-line no-restricted-syntax
77
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
8+
// eslint-disable-next-line no-restricted-syntax -- this is needed to allow mocking
9+
import * as ReportUtils from '@libs/ReportUtils';
810
import CONST from '@src/CONST';
911
import IntlStore from '@src/languages/IntlStore';
1012
import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
@@ -1078,6 +1080,113 @@ describe('actions/Policy', () => {
10781080
expect(policy?.employeeList).toEqual(employeeList);
10791081
expect(policy?.approvalMode).toBe(CONST.POLICY.APPROVAL_MODE.OPTIONAL);
10801082
});
1083+
1084+
it('should optimistically refresh next steps for submitted expense reports when changing approval mode', async () => {
1085+
await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});
1086+
await waitForBatchedUpdates();
1087+
1088+
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
1089+
const buildNextStepNewSpy = jest
1090+
.spyOn(require('@libs/NextStepUtils'), 'buildNextStepNew')
1091+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- This test covers legacy NextStep optimistic updates which still use the deprecated type.
1092+
.mockReturnValue({type: 'neutral', icon: CONST.NEXT_STEP.ICONS.CHECKMARK, message: [{text: 'Mock next step'}]} as never);
1093+
1094+
const getAllPolicyReportsSpy = jest.spyOn(ReportUtils, 'getAllPolicyReports');
1095+
const isExpenseReportSpy = jest.spyOn(ReportUtils, 'isExpenseReport');
1096+
const hasViolationsSpy = jest.spyOn(ReportUtils, 'hasViolations');
1097+
1098+
const policyID = Policy.generatePolicyID();
1099+
const fakePolicy: PolicyType = {
1100+
...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM),
1101+
id: policyID,
1102+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
1103+
approver: ESH_EMAIL,
1104+
owner: ESH_EMAIL,
1105+
};
1106+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
1107+
await waitForBatchedUpdates();
1108+
1109+
const submittedExpenseReport1 = {reportID: '100', policyID, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED} as unknown as Report;
1110+
const submittedExpenseReport2 = {reportID: '101', policyID, statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED} as unknown as Report;
1111+
getAllPolicyReportsSpy.mockReturnValue([submittedExpenseReport1, submittedExpenseReport2]);
1112+
isExpenseReportSpy.mockReturnValue(true);
1113+
hasViolationsSpy.mockReturnValue(false);
1114+
1115+
const nextStepKey1 = `${ONYXKEYS.COLLECTION.NEXT_STEP}${submittedExpenseReport1.reportID}` as const;
1116+
const nextStepKey2 = `${ONYXKEYS.COLLECTION.NEXT_STEP}${submittedExpenseReport2.reportID}` as const;
1117+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- We need a minimal ReportNextStepDeprecated shape to simulate rollback on failure.
1118+
const currentNextStep1 = {type: 'neutral', icon: CONST.NEXT_STEP.ICONS.CHECKMARK, message: [{text: 'Old next step 1'}]} as never;
1119+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- We need a minimal ReportNextStepDeprecated shape to simulate rollback on failure.
1120+
const currentNextStep2 = {type: 'neutral', icon: CONST.NEXT_STEP.ICONS.CHECKMARK, message: [{text: 'Old next step 2'}]} as never;
1121+
1122+
Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL, {
1123+
reportNextSteps: {
1124+
[nextStepKey1]: currentNextStep1,
1125+
[nextStepKey2]: currentNextStep2,
1126+
},
1127+
transactionViolations: {},
1128+
betas: [],
1129+
});
1130+
await waitForBatchedUpdates();
1131+
1132+
expect(apiWriteSpy).toHaveBeenCalledWith(
1133+
WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE,
1134+
expect.anything(),
1135+
expect.objectContaining({
1136+
optimisticData: expect.arrayContaining([
1137+
expect.objectContaining({onyxMethod: Onyx.METHOD.MERGE, key: nextStepKey1}),
1138+
expect.objectContaining({onyxMethod: Onyx.METHOD.MERGE, key: nextStepKey2}),
1139+
]),
1140+
failureData: expect.arrayContaining([
1141+
expect.objectContaining({onyxMethod: Onyx.METHOD.MERGE, key: nextStepKey1, value: currentNextStep1}),
1142+
expect.objectContaining({onyxMethod: Onyx.METHOD.MERGE, key: nextStepKey2, value: currentNextStep2}),
1143+
]),
1144+
}),
1145+
);
1146+
1147+
expect(buildNextStepNewSpy).toHaveBeenCalledTimes(2);
1148+
1149+
apiWriteSpy.mockRestore();
1150+
buildNextStepNewSpy.mockRestore();
1151+
getAllPolicyReportsSpy.mockRestore();
1152+
isExpenseReportSpy.mockRestore();
1153+
hasViolationsSpy.mockRestore();
1154+
});
1155+
1156+
it('should not update next steps when additionalData is not provided', async () => {
1157+
await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});
1158+
await waitForBatchedUpdates();
1159+
1160+
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
1161+
const buildNextStepNewSpy = jest.spyOn(require('@libs/NextStepUtils'), 'buildNextStepNew');
1162+
const getAllPolicyReportsSpy = jest.spyOn(ReportUtils, 'getAllPolicyReports');
1163+
1164+
const policyID = Policy.generatePolicyID();
1165+
const fakePolicy: PolicyType = {
1166+
...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM),
1167+
id: policyID,
1168+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
1169+
approver: ESH_EMAIL,
1170+
owner: ESH_EMAIL,
1171+
};
1172+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
1173+
await waitForBatchedUpdates();
1174+
1175+
Policy.setWorkspaceApprovalMode(policyID, ESH_EMAIL, CONST.POLICY.APPROVAL_MODE.OPTIONAL);
1176+
await waitForBatchedUpdates();
1177+
1178+
expect(getAllPolicyReportsSpy).not.toHaveBeenCalled();
1179+
expect(buildNextStepNewSpy).not.toHaveBeenCalled();
1180+
1181+
const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as {optimisticData?: Array<{key?: string}>; failureData?: Array<{key?: string}>} | undefined;
1182+
expect(writeOptions).toBeTruthy();
1183+
expect((writeOptions?.optimisticData ?? []).some((u) => (u?.key ?? '').startsWith(ONYXKEYS.COLLECTION.NEXT_STEP))).toBe(false);
1184+
expect((writeOptions?.failureData ?? []).some((u) => (u?.key ?? '').startsWith(ONYXKEYS.COLLECTION.NEXT_STEP))).toBe(false);
1185+
1186+
apiWriteSpy.mockRestore();
1187+
buildNextStepNewSpy.mockRestore();
1188+
getAllPolicyReportsSpy.mockRestore();
1189+
});
10811190
});
10821191

10831192
describe('deleteWorkspace', () => {

0 commit comments

Comments
 (0)