Skip to content
102 changes: 99 additions & 3 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@
};

let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({

Check warning on line 756 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -849,7 +849,7 @@
};

let allTransactions: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 852 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -863,7 +863,7 @@
});

let allTransactionDrafts: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 866 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -872,7 +872,7 @@
});

let allTransactionViolations: NonNullable<OnyxCollection<OnyxTypes.TransactionViolations>> = {};
Onyx.connect({

Check warning on line 875 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -886,7 +886,7 @@
});

let allNextSteps: NonNullable<OnyxCollection<OnyxTypes.ReportNextStepDeprecated>> = {};
Onyx.connect({

Check warning on line 889 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.NEXT_STEP,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -898,14 +898,14 @@
// `allRecentlyUsedTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor.
// All uses of this variable should be replaced with `useOnyx`.
let allRecentlyUsedTags: OnyxCollection<RecentlyUsedTags> = {};
Onyx.connect({

Check warning on line 901 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS,
waitForCollectionCallback: true,
callback: (val) => (allRecentlyUsedTags = val),
});

let allReports: OnyxCollection<OnyxTypes.Report>;
Onyx.connect({

Check warning on line 908 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -914,7 +914,7 @@
});

let allReportNameValuePairs: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
Onyx.connect({

Check warning on line 917 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -924,7 +924,7 @@

let userAccountID = -1;
let currentUserEmail = '';
Onyx.connect({

Check warning on line 927 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserEmail = value?.email ?? '';
Expand All @@ -933,7 +933,7 @@
});

let deprecatedCurrentUserPersonalDetails: OnyxEntry<OnyxTypes.PersonalDetails>;
Onyx.connect({

Check warning on line 936 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
deprecatedCurrentUserPersonalDetails = value?.[userAccountID] ?? undefined;
Expand Down Expand Up @@ -14761,17 +14761,113 @@
splitApiParams[`splits[${i}][${key}]`] = value !== null && typeof value === 'object' ? JSON.stringify(value) : value;
}
}
const parameters: SplitTransactionParams = {

if (isCreationOfSplits) {
const isTransactionOnHold = isOnHold(originalTransaction);

if (isTransactionOnHold) {
const holdReportActionIDs: string[] = [];
const holdReportActionCommentIDs: string[] = [];
const transactionReportActions = getAllReportActions(firstIOU?.childReportID);
const holdReportAction = getReportAction(firstIOU?.childReportID, `${originalTransaction?.comment?.hold ?? ''}`);

const holdReportActionComment = holdReportAction
? Object.values(transactionReportActions ?? {}).find(
(action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT && action?.timestamp === holdReportAction.timestamp,
)
: undefined;

if (holdReportAction && holdReportActionComment) {
// Loop through all split expenses and add optimistic hold report actions for each split
for (const [index, splitExpense] of splits.entries()) {
const splitReportID = splitExpense?.transactionThreadReportID;
if (!splitReportID) {
continue;
}

// Generate new IDs and timestamps for each split
const newHoldReportActionID = NumberUtils.rand64();
const newHoldReportActionCommentID = NumberUtils.rand64();
const timestamp = DateUtils.getDBTime();
const reportActionTimestamp = DateUtils.addMillisecondsFromDateTime(timestamp, 1);

// Store IDs for API parameters
holdReportActionIDs[index] = newHoldReportActionID;
holdReportActionCommentIDs[index] = newHoldReportActionCommentID;

// Create new optimistic hold report action with new ID and timestamp, keeping other information
const newHoldReportAction = {
...holdReportAction,
reportActionID: newHoldReportActionID,
created: timestamp,
Comment thread
paulnjs marked this conversation as resolved.
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
};

// Create new optimistic hold report action comment with new ID and timestamp, keeping other information
const newHoldReportActionComment = {
...holdReportActionComment,
reportActionID: newHoldReportActionCommentID,
created: reportActionTimestamp,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
};

// Add to optimisticData for this split's reportActions
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitReportID}`,
value: {
[newHoldReportActionID]: newHoldReportAction,
[newHoldReportActionCommentID]: newHoldReportActionComment,
},
});

// Add successData to clear pendingAction after API call succeeds
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitReportID}`,
value: {
[newHoldReportActionID]: {pendingAction: null},
[newHoldReportActionCommentID]: {pendingAction: null},
},
});

// Add failureData to remove optimistic hold report actions if the request fails
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitReportID}`,
value: {
[newHoldReportActionID]: null,
[newHoldReportActionCommentID]: null,
},
});
}
Comment thread
paulnjs marked this conversation as resolved.

// Add hold report action IDs to API parameters
for (const [i, holdReportActionID] of holdReportActionIDs.entries()) {
if (holdReportActionID) {
splitApiParams[`splits[${i}][holdReportActionID]`] = holdReportActionID;
}
}
for (const [i, holdReportActionCommentID] of holdReportActionCommentIDs.entries()) {
if (holdReportActionCommentID) {
splitApiParams[`splits[${i}][holdReportActionCommentID]`] = holdReportActionCommentID;
}
}
}
}
}

const splitParameters: SplitTransactionParams = {
...splitApiParams,
transactionID: originalTransactionID,
};

if (isCreationOfSplits) {
// eslint-disable-next-line rulesdir/no-multiple-api-calls
API.write(WRITE_COMMANDS.SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData});
API.write(WRITE_COMMANDS.SPLIT_TRANSACTION, splitParameters, {optimisticData, successData, failureData});
} else {
// eslint-disable-next-line rulesdir/no-multiple-api-calls
API.write(WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData});
API.write(WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, splitParameters, {optimisticData, successData, failureData});
}
}
// eslint-disable-next-line @typescript-eslint/no-deprecated
Expand Down
239 changes: 239 additions & 0 deletions tests/actions/IOUTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8967,6 +8967,245 @@ describe('actions/IOU', () => {
expect(split1).toBeDefined();
expect(split2).toBeDefined();
});

it('should create hold report actions for split transactions when original transaction is on hold', async () => {
// Given an expense that is on hold
const amount = 10000;
let expenseReport: OnyxEntry<Report>;
let chatReport: OnyxEntry<Report>;
let originalTransactionID: string | undefined;
let transactionThreadReportID: string | undefined;

const policyID = generatePolicyID();
createWorkspace({
policyOwnerEmail: CARLOS_EMAIL,
makeMeAdmin: true,
policyName: "Carlos's Workspace for Hold Test",
policyID,
});

// Change the approval mode for the policy since default is Submit and Close
setWorkspaceApprovalMode(policyID, CARLOS_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC);
await waitForBatchedUpdates();

await getOnyxData({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (allReports) => {
chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT);
},
});

// Create the initial expense
requestMoney({
report: chatReport,
participantParams: {
payeeEmail: RORY_EMAIL,
payeeAccountID: RORY_ACCOUNT_ID,
participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport?.reportID},
},
transactionParams: {
amount,
attendees: [],
currency: CONST.CURRENCY.USD,
created: '',
merchant: 'Test Merchant',
comment: 'Original expense',
},
shouldGenerateTransactionThreadReport: true,
isASAPSubmitBetaEnabled: false,
currentUserAccountIDParam: RORY_ACCOUNT_ID,
currentUserEmailParam: RORY_EMAIL,
transactionViolations: {},
});
await waitForBatchedUpdates();

await getOnyxData({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (allReports) => {
expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE);
},
});

// Get the original transaction ID and transaction thread report ID
await getOnyxData({
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
waitForCollectionCallback: false,
callback: (allReportsAction) => {
const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
isMoneyRequestAction(reportAction),
);
const iouAction = iouActions?.at(0);
const originalMessage = isMoneyRequestAction(iouAction) ? getOriginalMessage(iouAction) : undefined;
originalTransactionID = originalMessage?.IOUTransactionID;
transactionThreadReportID = iouAction?.childReportID;
},
});

// Put the expense on hold
if (originalTransactionID && transactionThreadReportID) {
putOnHold(originalTransactionID, 'Test hold reason', transactionThreadReportID);
}
await waitForBatchedUpdates();

// Verify the transaction is on hold
const originalTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`);
expect(originalTransaction?.comment?.hold).toBeDefined();

// Get the first IOU action for the split flow
let firstIOU: ReportAction | undefined;
await getOnyxData({
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
waitForCollectionCallback: false,
callback: (allReportsAction) => {
const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
isMoneyRequestAction(reportAction),
);
firstIOU = iouActions?.at(0);
},
});

// Create the draft transaction with split expenses
const draftTransaction: Transaction = {
reportID: originalTransaction?.reportID ?? '456',
transactionID: originalTransaction?.transactionID ?? '234',
amount,
created: originalTransaction?.created ?? DateUtils.getDBTime(),
currency: CONST.CURRENCY.USD,
merchant: originalTransaction?.merchant ?? '',
comment: {
originalTransactionID,
comment: originalTransaction?.comment?.comment ?? '',
hold: originalTransaction?.comment?.hold,
splitExpenses: [
{
transactionID: 'split-held-tx-1',
amount: amount / 2,
description: 'Split 1',
created: DateUtils.getDBTime(),
},
{
transactionID: 'split-held-tx-2',
amount: amount / 2,
description: 'Split 2',
created: DateUtils.getDBTime(),
},
],
attendees: [],
type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT,
},
};

let allTransactions: OnyxCollection<Transaction>;
let allReports: OnyxCollection<Report>;
let allReportNameValuePairs: OnyxCollection<ReportNameValuePairs>;

await getOnyxData({
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
allTransactions = value;
},
});
await getOnyxData({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
allReports = value;
},
});
await getOnyxData({
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
allReportNameValuePairs = value;
},
});

// When splitting the held expense
updateSplitTransactionsFromSplitExpensesFlow({
allTransactionsList: allTransactions,
allReportsList: allReports,
allReportNameValuePairsList: allReportNameValuePairs,
transactionData: {
reportID: draftTransaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID),
originalTransactionID: draftTransaction?.comment?.originalTransactionID ?? String(CONST.DEFAULT_NUMBER_ID),
splitExpenses: draftTransaction?.comment?.splitExpenses ?? [],
splitExpensesTotal: draftTransaction?.comment?.splitExpensesTotal,
},
searchContext: {
currentSearchHash: -2,
},
policyCategories: undefined,
policy: undefined,
policyRecentlyUsedCategories: [],
iouReport: expenseReport,
firstIOU,
isASAPSubmitBetaEnabled: false,
currentUserPersonalDetails,
transactionViolations: {},
});

await waitForBatchedUpdates();

// Then verify the split transactions were created
const split1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}split-held-tx-1`);
const split2 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}split-held-tx-2`);

expect(split1).toBeDefined();
expect(split2).toBeDefined();

// Find the transaction thread reports for each split by looking at the IOU actions
let split1ThreadReportID: string | undefined;
let split2ThreadReportID: string | undefined;

await getOnyxData({
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
waitForCollectionCallback: false,
callback: (allReportsAction) => {
const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
isMoneyRequestAction(reportAction),
);
for (const action of iouActions) {
const message = isMoneyRequestAction(action) ? getOriginalMessage(action) : undefined;
if (message?.IOUTransactionID === 'split-held-tx-1') {
split1ThreadReportID = action.childReportID;
} else if (message?.IOUTransactionID === 'split-held-tx-2') {
split2ThreadReportID = action.childReportID;
}
}
},
});

// Verify that split transaction thread IDs exist
expect(split1ThreadReportID).toBeDefined();
expect(split2ThreadReportID).toBeDefined();

// Verify each split transaction thread has hold report actions
// When splitting a held expense, new hold report actions should be created for each split
if (split1ThreadReportID) {
const split1ReportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${split1ThreadReportID}`);
const split1HoldActions = Object.values(split1ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD);
const split1CommentActions = Object.values(split1ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT);

// Should have at least one HOLD action and one ADD_COMMENT action (the hold comment)
// The hold actions are created optimistically with pendingAction: ADD, but this
// may be cleared to null after the API call succeeds
expect(split1HoldActions.length).toBeGreaterThanOrEqual(1);
expect(split1CommentActions.length).toBeGreaterThanOrEqual(1);
}

if (split2ThreadReportID) {
const split2ReportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${split2ThreadReportID}`);
const split2HoldActions = Object.values(split2ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD);
const split2CommentActions = Object.values(split2ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT);

// Should have at least one HOLD action and one ADD_COMMENT action (the hold comment)
expect(split2HoldActions.length).toBeGreaterThanOrEqual(1);
expect(split2CommentActions.length).toBeGreaterThanOrEqual(1);
}
});
});
});

Expand Down
Loading