Skip to content

Commit a9b3913

Browse files
authored
Merge pull request Expensify#66604 from DylanDylann/dylan/fix/65243
Update next step
2 parents 8dcc9d3 + fd06fc2 commit a9b3913

5 files changed

Lines changed: 216 additions & 15 deletions

File tree

src/libs/actions/Transaction.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {ChangeTransactionsReportParams, DismissViolationParams, GetRoutePar
99
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
1010
import * as CollectionUtils from '@libs/CollectionUtils';
1111
import DateUtils from '@libs/DateUtils';
12+
import {buildNextStep} from '@libs/NextStepUtils';
1213
import * as NumberUtils from '@libs/NumberUtils';
1314
import {rand64} from '@libs/NumberUtils';
1415
import {hasDependentTags, isPaidGroupPolicy} from '@libs/PolicyUtils';
@@ -21,12 +22,24 @@ import {
2122
buildOptimisticUnreportedTransactionAction,
2223
buildTransactionThread,
2324
findSelfDMReportID,
25+
getReportTransactions,
2426
} from '@libs/ReportUtils';
2527
import {getAmount, waypointHasValidAddress} from '@libs/TransactionUtils';
2628
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
2729
import CONST from '@src/CONST';
2830
import ONYXKEYS from '@src/ONYXKEYS';
29-
import type {PersonalDetails, Policy, RecentWaypoint, Report, ReportAction, ReviewDuplicates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
31+
import type {
32+
PersonalDetails,
33+
Policy,
34+
RecentWaypoint,
35+
Report,
36+
ReportAction,
37+
ReportNextStep,
38+
ReviewDuplicates,
39+
Transaction,
40+
TransactionViolation,
41+
TransactionViolations,
42+
} from '@src/types/onyx';
3043
import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage';
3144
import type {OnyxData} from '@src/types/onyx/Request';
3245
import type {WaypointCollection} from '@src/types/onyx/Transaction';
@@ -100,6 +113,11 @@ Onyx.connect({
100113
callback: (val) => (allTransactionViolations = val ?? []),
101114
});
102115

116+
// Helper to safely check for a string 'name' property
117+
function isViolationWithName(violation: unknown): violation is {name: string} {
118+
return !!(violation && typeof violation === 'object' && typeof (violation as {name?: unknown}).name === 'string');
119+
}
120+
103121
function saveWaypoint(transactionID: string, index: string, waypoint: RecentWaypoint | null, isDraft = false) {
104122
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
105123
comment: {
@@ -595,7 +613,7 @@ function setTransactionReport(transactionID: string, transaction: Partial<Transa
595613
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction);
596614
}
597615

598-
function changeTransactionsReport(transactionIDs: string[], reportID: string, policy?: OnyxEntry<Policy>) {
616+
function changeTransactionsReport(transactionIDs: string[], reportID: string, policy?: OnyxEntry<Policy>, reportNextStep?: OnyxEntry<ReportNextStep>) {
599617
const newReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
600618

601619
const transactions = transactionIDs.map((id) => allTransactions?.[id]).filter((t): t is NonNullable<typeof t> => t !== undefined);
@@ -694,6 +712,11 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string, po
694712
}
695713

696714
let transactionsMoved = false;
715+
let shouldFixViolations = false;
716+
717+
const policyTagList = getPolicyTagsData(policy?.id);
718+
const policyCategories = getPolicyCategoriesData(policy?.id);
719+
const policyHasDependentTags = hasDependentTags(policy, policyTagList);
697720

698721
transactions.forEach((transaction) => {
699722
const isUnreportedExpense = !transaction.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
@@ -769,22 +792,25 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string, po
769792

770793
// 2. Calculate transaction violations if moving transaction to a workspace
771794
if (isPaidGroupPolicy(policy) && policy?.id) {
772-
const policyTagList = getPolicyTagsData(policy.id);
773-
const violationData = ViolationsUtils.getViolationsOnyxData(
774-
transaction,
775-
allTransactionViolations,
776-
policy,
777-
policyTagList,
778-
getPolicyCategoriesData(policy.id),
779-
hasDependentTags(policy, policyTagList),
780-
false,
781-
);
795+
const violationData = ViolationsUtils.getViolationsOnyxData(transaction, allTransactionViolations, policy, policyTagList, policyCategories, policyHasDependentTags, false);
782796
optimisticData.push(violationData);
783797
failureData.push({
784798
onyxMethod: Onyx.METHOD.MERGE,
785799
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
786800
value: allTransactionViolation?.[transaction.transactionID],
787801
});
802+
const transactionHasViolations = Array.isArray(violationData.value) && violationData.value.length > 0;
803+
const hasOtherViolationsBesideDuplicates =
804+
Array.isArray(violationData.value) &&
805+
!violationData.value.every((violation) => {
806+
if (!isViolationWithName(violation)) {
807+
return false;
808+
}
809+
return violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION;
810+
});
811+
if (transactionHasViolations && hasOtherViolationsBesideDuplicates) {
812+
shouldFixViolations = true;
813+
}
788814
}
789815

790816
// 3. Keep track of the new report totals
@@ -1029,6 +1055,39 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string, po
10291055
});
10301056
});
10311057

1058+
const reportTransactions = getReportTransactions(reportID);
1059+
reportTransactions.forEach((transaction) => {
1060+
if (!isPaidGroupPolicy(policy) || !policy?.id) {
1061+
return;
1062+
}
1063+
const violationData = ViolationsUtils.getViolationsOnyxData(transaction, allTransactionViolations, policy, policyTagList, policyCategories, policyHasDependentTags, false);
1064+
const hasOtherViolationsBesideDuplicates =
1065+
Array.isArray(violationData.value) &&
1066+
!violationData.value.every((violation) => {
1067+
if (!isViolationWithName(violation)) {
1068+
return false;
1069+
}
1070+
return violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION;
1071+
});
1072+
if (Array.isArray(violationData.value) && violationData.value.length > 0 && hasOtherViolationsBesideDuplicates) {
1073+
shouldFixViolations = true;
1074+
}
1075+
});
1076+
1077+
// 9. Update next step for report
1078+
const nextStepReport = {...newReport, total: updatedReportTotals[reportID] ?? newReport?.total, reportID: newReport?.reportID ?? reportID};
1079+
const optimisticNextStep = buildNextStep(nextStepReport, nextStepReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, shouldFixViolations);
1080+
optimisticData.push({
1081+
onyxMethod: Onyx.METHOD.MERGE,
1082+
key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`,
1083+
value: optimisticNextStep,
1084+
});
1085+
failureData.push({
1086+
onyxMethod: Onyx.METHOD.MERGE,
1087+
key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`,
1088+
value: reportNextStep,
1089+
});
1090+
10321091
const parameters: ChangeTransactionsReportParams = {
10331092
transactionList: transactionIDs.join(','),
10341093
reportID,

src/pages/AddUnreportedExpense.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) {
4444

4545
const {reportID, backToReport} = route.params;
4646
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true});
47+
const [reportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`, {canBeMissing: true});
4748
const policy = usePolicy(report?.policyID);
4849
const [hasMoreUnreportedTransactionsResults] = useOnyx(ONYXKEYS.HAS_MORE_UNREPORTED_TRANSACTIONS_RESULTS, {canBeMissing: true});
4950
const [isLoadingUnreportedTransactions] = useOnyx(ONYXKEYS.IS_LOADING_UNREPORTED_TRANSACTIONS, {canBeMissing: true});
@@ -188,7 +189,7 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) {
188189
if (report && isIOUReport(report)) {
189190
convertBulkTrackedExpensesToIOU([...selectedIds], report.reportID);
190191
} else {
191-
changeTransactionsReport([...selectedIds], report?.reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID, policy);
192+
changeTransactionsReport([...selectedIds], report?.reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID, policy, reportNextStep);
192193
}
193194
});
194195
setErrorMessage('');

src/pages/Search/SearchTransactionsChangeReport.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function SearchTransactionsChangeReport() {
1919
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]);
2020

2121
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false});
22+
const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP, {canBeMissing: true});
2223
const transactionsReports = useMemo(() => {
2324
const reports = Object.values(selectedTransactions).reduce((acc, transaction) => {
2425
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
@@ -35,7 +36,8 @@ function SearchTransactionsChangeReport() {
3536
return;
3637
}
3738

38-
changeTransactionsReport(selectedTransactionsKeys, item.value);
39+
const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${item.value}`];
40+
changeTransactionsReport(selectedTransactionsKeys, item.value, undefined, reportNextStep);
3941
clearSelectedTransactions();
4042

4143
Navigation.goBack();

src/pages/iou/request/step/IOURequestEditReport.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
2525
const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext();
2626

2727
const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false});
28+
const [reportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`, {canBeMissing: true});
2829

2930
const selectReport = (item: TransactionGroupListItem) => {
3031
if (selectedTransactionIDs.length === 0 || item.value === reportID) {
3132
Navigation.dismissModal();
3233
return;
3334
}
3435

35-
changeTransactionsReport(selectedTransactionIDs, item.value);
36+
changeTransactionsReport(selectedTransactionIDs, item.value, undefined, reportNextStep);
3637
turnOffMobileSelectionMode();
3738
clearSelectedTransactions(true);
3839
Navigation.dismissModal();

tests/unit/TransactionTest.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,143 @@ describe('Transaction', () => {
141141

142142
expect(getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID)).toBeDefined();
143143
});
144+
145+
it('correctly handles reportNextStep parameter when moving transactions between reports', async () => {
146+
const mockAPIWrite = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
147+
148+
const transaction = generateTransaction({
149+
reportID: FAKE_OLD_REPORT_ID,
150+
});
151+
const oldIOUAction: OnyxEntry<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>> = {
152+
reportActionID: rand64(),
153+
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
154+
actorAccountID: CURRENT_USER_ID,
155+
created: DateUtils.getDBTime(),
156+
originalMessage: {
157+
IOUReportID: FAKE_OLD_REPORT_ID,
158+
IOUTransactionID: transaction.transactionID,
159+
amount: transaction.amount,
160+
currency: transaction.currency,
161+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
162+
},
163+
};
164+
165+
const mockReportNextStep = {
166+
type: 'neutral' as const,
167+
icon: CONST.NEXT_STEP.ICONS.HOURGLASS,
168+
message: [
169+
{
170+
text: 'Test next step message',
171+
},
172+
],
173+
};
174+
175+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
176+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction});
177+
178+
changeTransactionsReport([transaction.transactionID], FAKE_NEW_REPORT_ID, undefined, mockReportNextStep);
179+
await waitForBatchedUpdates();
180+
181+
expect(mockAPIWrite).toHaveBeenCalled();
182+
183+
const apiWriteCall = mockAPIWrite.mock.calls.at(0);
184+
const failureData = (apiWriteCall?.[2] as {failureData?: Array<{key: string; value: unknown}>})?.failureData;
185+
186+
const nextStepFailureData = failureData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.NEXT_STEP}${FAKE_NEW_REPORT_ID}`);
187+
188+
expect(nextStepFailureData).toBeDefined();
189+
expect(nextStepFailureData?.value).toEqual(mockReportNextStep);
190+
191+
mockAPIWrite.mockRestore();
192+
});
193+
194+
it('correctly handles reportNextStep parameter when moving transactions to unreported report', async () => {
195+
const mockAPIWrite = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
196+
197+
const transaction = generateTransaction({
198+
reportID: FAKE_OLD_REPORT_ID,
199+
});
200+
const oldIOUAction: OnyxEntry<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>> = {
201+
reportActionID: rand64(),
202+
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
203+
actorAccountID: CURRENT_USER_ID,
204+
created: DateUtils.getDBTime(),
205+
originalMessage: {
206+
IOUReportID: FAKE_OLD_REPORT_ID,
207+
IOUTransactionID: transaction.transactionID,
208+
amount: transaction.amount,
209+
currency: transaction.currency,
210+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
211+
},
212+
};
213+
214+
const mockReportNextStep = {
215+
type: 'alert' as const,
216+
icon: CONST.NEXT_STEP.ICONS.CHECKMARK,
217+
message: [
218+
{
219+
text: 'Alert next step message',
220+
},
221+
],
222+
requiresUserAction: true,
223+
};
224+
225+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
226+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction});
227+
228+
changeTransactionsReport([transaction.transactionID], CONST.REPORT.UNREPORTED_REPORT_ID, undefined, mockReportNextStep);
229+
await waitForBatchedUpdates();
230+
231+
expect(mockAPIWrite).toHaveBeenCalled();
232+
233+
const apiWriteCall = mockAPIWrite.mock.calls.at(0);
234+
const failureData = (apiWriteCall?.[2] as {failureData?: Array<{key: string; value: unknown}>})?.failureData;
235+
236+
const nextStepFailureData = failureData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.NEXT_STEP}${CONST.REPORT.UNREPORTED_REPORT_ID}`);
237+
238+
expect(nextStepFailureData).toBeDefined();
239+
expect(nextStepFailureData?.value).toEqual(mockReportNextStep);
240+
241+
mockAPIWrite.mockRestore();
242+
});
243+
244+
it('correctly handles undefined reportNextStep parameter', async () => {
245+
const mockAPIWrite = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
246+
247+
const transaction = generateTransaction({
248+
reportID: FAKE_OLD_REPORT_ID,
249+
});
250+
const oldIOUAction: OnyxEntry<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>> = {
251+
reportActionID: rand64(),
252+
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
253+
actorAccountID: CURRENT_USER_ID,
254+
created: DateUtils.getDBTime(),
255+
originalMessage: {
256+
IOUReportID: FAKE_OLD_REPORT_ID,
257+
IOUTransactionID: transaction.transactionID,
258+
amount: transaction.amount,
259+
currency: transaction.currency,
260+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
261+
},
262+
};
263+
264+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
265+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction});
266+
267+
changeTransactionsReport([transaction.transactionID], FAKE_NEW_REPORT_ID, undefined, undefined);
268+
await waitForBatchedUpdates();
269+
270+
expect(mockAPIWrite).toHaveBeenCalled();
271+
272+
const apiWriteCall = mockAPIWrite.mock.calls.at(0);
273+
const failureData = (apiWriteCall?.[2] as {failureData?: Array<{key: string; value: unknown}>})?.failureData;
274+
275+
const nextStepFailureData = failureData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.NEXT_STEP}${FAKE_NEW_REPORT_ID}`);
276+
277+
expect(nextStepFailureData).toBeDefined();
278+
expect(nextStepFailureData?.value).toBeUndefined();
279+
280+
mockAPIWrite.mockRestore();
281+
});
144282
});
145283
});

0 commit comments

Comments
 (0)