Skip to content

Commit d382b9a

Browse files
authored
Merge pull request #75980 from paulnjs/paulnjs-fix/74682
fix: No message that expense is on hold when split expense offline
2 parents e219999 + 76e0e13 commit d382b9a

File tree

2 files changed

+338
-3
lines changed

2 files changed

+338
-3
lines changed

src/libs/actions/IOU/index.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14129,17 +14129,113 @@ function updateSplitTransactions({
1412914129
splitApiParams[`splits[${i}][${key}]`] = value !== null && typeof value === 'object' ? JSON.stringify(value) : value;
1413014130
}
1413114131
}
14132-
const parameters: SplitTransactionParams = {
14132+
14133+
if (isCreationOfSplits) {
14134+
const isTransactionOnHold = isOnHold(originalTransaction);
14135+
14136+
if (isTransactionOnHold) {
14137+
const holdReportActionIDs: string[] = [];
14138+
const holdReportActionCommentIDs: string[] = [];
14139+
const transactionReportActions = getAllReportActions(firstIOU?.childReportID);
14140+
const holdReportAction = getReportAction(firstIOU?.childReportID, `${originalTransaction?.comment?.hold ?? ''}`);
14141+
14142+
const holdReportActionComment = holdReportAction
14143+
? Object.values(transactionReportActions ?? {}).find(
14144+
(action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT && action?.timestamp === holdReportAction.timestamp,
14145+
)
14146+
: undefined;
14147+
14148+
if (holdReportAction && holdReportActionComment) {
14149+
// Loop through all split expenses and add optimistic hold report actions for each split
14150+
for (const [index, splitExpense] of splits.entries()) {
14151+
const splitReportID = splitExpense?.transactionThreadReportID;
14152+
if (!splitReportID) {
14153+
continue;
14154+
}
14155+
14156+
// Generate new IDs and timestamps for each split
14157+
const newHoldReportActionID = NumberUtils.rand64();
14158+
const newHoldReportActionCommentID = NumberUtils.rand64();
14159+
const timestamp = DateUtils.getDBTime();
14160+
const reportActionTimestamp = DateUtils.addMillisecondsFromDateTime(timestamp, 1);
14161+
14162+
// Store IDs for API parameters
14163+
holdReportActionIDs[index] = newHoldReportActionID;
14164+
holdReportActionCommentIDs[index] = newHoldReportActionCommentID;
14165+
14166+
// Create new optimistic hold report action with new ID and timestamp, keeping other information
14167+
const newHoldReportAction = {
14168+
...holdReportAction,
14169+
reportActionID: newHoldReportActionID,
14170+
created: timestamp,
14171+
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
14172+
};
14173+
14174+
// Create new optimistic hold report action comment with new ID and timestamp, keeping other information
14175+
const newHoldReportActionComment = {
14176+
...holdReportActionComment,
14177+
reportActionID: newHoldReportActionCommentID,
14178+
created: reportActionTimestamp,
14179+
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
14180+
};
14181+
14182+
// Add to optimisticData for this split's reportActions
14183+
optimisticData.push({
14184+
onyxMethod: Onyx.METHOD.MERGE,
14185+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitReportID}`,
14186+
value: {
14187+
[newHoldReportActionID]: newHoldReportAction,
14188+
[newHoldReportActionCommentID]: newHoldReportActionComment,
14189+
},
14190+
});
14191+
14192+
// Add successData to clear pendingAction after API call succeeds
14193+
successData.push({
14194+
onyxMethod: Onyx.METHOD.MERGE,
14195+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitReportID}`,
14196+
value: {
14197+
[newHoldReportActionID]: {pendingAction: null},
14198+
[newHoldReportActionCommentID]: {pendingAction: null},
14199+
},
14200+
});
14201+
14202+
// Add failureData to remove optimistic hold report actions if the request fails
14203+
failureData.push({
14204+
onyxMethod: Onyx.METHOD.MERGE,
14205+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitReportID}`,
14206+
value: {
14207+
[newHoldReportActionID]: null,
14208+
[newHoldReportActionCommentID]: null,
14209+
},
14210+
});
14211+
}
14212+
14213+
// Add hold report action IDs to API parameters
14214+
for (const [i, holdReportActionID] of holdReportActionIDs.entries()) {
14215+
if (holdReportActionID) {
14216+
splitApiParams[`splits[${i}][holdReportActionID]`] = holdReportActionID;
14217+
}
14218+
}
14219+
for (const [i, holdReportActionCommentID] of holdReportActionCommentIDs.entries()) {
14220+
if (holdReportActionCommentID) {
14221+
splitApiParams[`splits[${i}][holdReportActionCommentID]`] = holdReportActionCommentID;
14222+
}
14223+
}
14224+
}
14225+
}
14226+
}
14227+
14228+
const splitParameters: SplitTransactionParams = {
1413314229
...splitApiParams,
1413414230
transactionID: originalTransactionID,
1413514231
};
1413614232

1413714233
if (isCreationOfSplits) {
1413814234
// eslint-disable-next-line rulesdir/no-multiple-api-calls
14139-
API.write(WRITE_COMMANDS.SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData});
14235+
API.write(WRITE_COMMANDS.SPLIT_TRANSACTION, splitParameters, {optimisticData, successData, failureData});
1414014236
} else {
1414114237
// eslint-disable-next-line rulesdir/no-multiple-api-calls
14142-
API.write(WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, parameters, {optimisticData, successData, failureData});
14238+
API.write(WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, splitParameters, {optimisticData, successData, failureData});
1414314239
}
1414414240
}
1414514241
// eslint-disable-next-line @typescript-eslint/no-deprecated

tests/actions/IOUTest.ts

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9437,6 +9437,245 @@ describe('actions/IOU', () => {
94379437
expect(split1).toBeDefined();
94389438
expect(split2).toBeDefined();
94399439
});
9440+
9441+
it('should create hold report actions for split transactions when original transaction is on hold', async () => {
9442+
// Given an expense that is on hold
9443+
const amount = 10000;
9444+
let expenseReport: OnyxEntry<Report>;
9445+
let chatReport: OnyxEntry<Report>;
9446+
let originalTransactionID: string | undefined;
9447+
let transactionThreadReportID: string | undefined;
9448+
9449+
const policyID = generatePolicyID();
9450+
createWorkspace({
9451+
policyOwnerEmail: CARLOS_EMAIL,
9452+
makeMeAdmin: true,
9453+
policyName: "Carlos's Workspace for Hold Test",
9454+
policyID,
9455+
});
9456+
9457+
// Change the approval mode for the policy since default is Submit and Close
9458+
setWorkspaceApprovalMode(policyID, CARLOS_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC);
9459+
await waitForBatchedUpdates();
9460+
9461+
await getOnyxData({
9462+
key: ONYXKEYS.COLLECTION.REPORT,
9463+
waitForCollectionCallback: true,
9464+
callback: (allReports) => {
9465+
chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT);
9466+
},
9467+
});
9468+
9469+
// Create the initial expense
9470+
requestMoney({
9471+
report: chatReport,
9472+
participantParams: {
9473+
payeeEmail: RORY_EMAIL,
9474+
payeeAccountID: RORY_ACCOUNT_ID,
9475+
participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport?.reportID},
9476+
},
9477+
transactionParams: {
9478+
amount,
9479+
attendees: [],
9480+
currency: CONST.CURRENCY.USD,
9481+
created: '',
9482+
merchant: 'Test Merchant',
9483+
comment: 'Original expense',
9484+
},
9485+
shouldGenerateTransactionThreadReport: true,
9486+
isASAPSubmitBetaEnabled: false,
9487+
currentUserAccountIDParam: RORY_ACCOUNT_ID,
9488+
currentUserEmailParam: RORY_EMAIL,
9489+
transactionViolations: {},
9490+
});
9491+
await waitForBatchedUpdates();
9492+
9493+
await getOnyxData({
9494+
key: ONYXKEYS.COLLECTION.REPORT,
9495+
waitForCollectionCallback: true,
9496+
callback: (allReports) => {
9497+
expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE);
9498+
},
9499+
});
9500+
9501+
// Get the original transaction ID and transaction thread report ID
9502+
await getOnyxData({
9503+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
9504+
waitForCollectionCallback: false,
9505+
callback: (allReportsAction) => {
9506+
const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
9507+
isMoneyRequestAction(reportAction),
9508+
);
9509+
const iouAction = iouActions?.at(0);
9510+
const originalMessage = isMoneyRequestAction(iouAction) ? getOriginalMessage(iouAction) : undefined;
9511+
originalTransactionID = originalMessage?.IOUTransactionID;
9512+
transactionThreadReportID = iouAction?.childReportID;
9513+
},
9514+
});
9515+
9516+
// Put the expense on hold
9517+
if (originalTransactionID && transactionThreadReportID) {
9518+
putOnHold(originalTransactionID, 'Test hold reason', transactionThreadReportID);
9519+
}
9520+
await waitForBatchedUpdates();
9521+
9522+
// Verify the transaction is on hold
9523+
const originalTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`);
9524+
expect(originalTransaction?.comment?.hold).toBeDefined();
9525+
9526+
// Get the first IOU action for the split flow
9527+
let firstIOU: ReportAction | undefined;
9528+
await getOnyxData({
9529+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
9530+
waitForCollectionCallback: false,
9531+
callback: (allReportsAction) => {
9532+
const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
9533+
isMoneyRequestAction(reportAction),
9534+
);
9535+
firstIOU = iouActions?.at(0);
9536+
},
9537+
});
9538+
9539+
// Create the draft transaction with split expenses
9540+
const draftTransaction: Transaction = {
9541+
reportID: originalTransaction?.reportID ?? '456',
9542+
transactionID: originalTransaction?.transactionID ?? '234',
9543+
amount,
9544+
created: originalTransaction?.created ?? DateUtils.getDBTime(),
9545+
currency: CONST.CURRENCY.USD,
9546+
merchant: originalTransaction?.merchant ?? '',
9547+
comment: {
9548+
originalTransactionID,
9549+
comment: originalTransaction?.comment?.comment ?? '',
9550+
hold: originalTransaction?.comment?.hold,
9551+
splitExpenses: [
9552+
{
9553+
transactionID: 'split-held-tx-1',
9554+
amount: amount / 2,
9555+
description: 'Split 1',
9556+
created: DateUtils.getDBTime(),
9557+
},
9558+
{
9559+
transactionID: 'split-held-tx-2',
9560+
amount: amount / 2,
9561+
description: 'Split 2',
9562+
created: DateUtils.getDBTime(),
9563+
},
9564+
],
9565+
attendees: [],
9566+
type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT,
9567+
},
9568+
};
9569+
9570+
let allTransactions: OnyxCollection<Transaction>;
9571+
let allReports: OnyxCollection<Report>;
9572+
let allReportNameValuePairs: OnyxCollection<ReportNameValuePairs>;
9573+
9574+
await getOnyxData({
9575+
key: ONYXKEYS.COLLECTION.TRANSACTION,
9576+
waitForCollectionCallback: true,
9577+
callback: (value) => {
9578+
allTransactions = value;
9579+
},
9580+
});
9581+
await getOnyxData({
9582+
key: ONYXKEYS.COLLECTION.REPORT,
9583+
waitForCollectionCallback: true,
9584+
callback: (value) => {
9585+
allReports = value;
9586+
},
9587+
});
9588+
await getOnyxData({
9589+
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
9590+
waitForCollectionCallback: true,
9591+
callback: (value) => {
9592+
allReportNameValuePairs = value;
9593+
},
9594+
});
9595+
9596+
// When splitting the held expense
9597+
updateSplitTransactionsFromSplitExpensesFlow({
9598+
allTransactionsList: allTransactions,
9599+
allReportsList: allReports,
9600+
allReportNameValuePairsList: allReportNameValuePairs,
9601+
transactionData: {
9602+
reportID: draftTransaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID),
9603+
originalTransactionID: draftTransaction?.comment?.originalTransactionID ?? String(CONST.DEFAULT_NUMBER_ID),
9604+
splitExpenses: draftTransaction?.comment?.splitExpenses ?? [],
9605+
splitExpensesTotal: draftTransaction?.comment?.splitExpensesTotal,
9606+
},
9607+
searchContext: {
9608+
currentSearchHash: -2,
9609+
},
9610+
policyCategories: undefined,
9611+
policy: undefined,
9612+
policyRecentlyUsedCategories: [],
9613+
iouReport: expenseReport,
9614+
firstIOU,
9615+
isASAPSubmitBetaEnabled: false,
9616+
currentUserPersonalDetails,
9617+
transactionViolations: {},
9618+
});
9619+
9620+
await waitForBatchedUpdates();
9621+
9622+
// Then verify the split transactions were created
9623+
const split1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}split-held-tx-1`);
9624+
const split2 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}split-held-tx-2`);
9625+
9626+
expect(split1).toBeDefined();
9627+
expect(split2).toBeDefined();
9628+
9629+
// Find the transaction thread reports for each split by looking at the IOU actions
9630+
let split1ThreadReportID: string | undefined;
9631+
let split2ThreadReportID: string | undefined;
9632+
9633+
await getOnyxData({
9634+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
9635+
waitForCollectionCallback: false,
9636+
callback: (allReportsAction) => {
9637+
const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
9638+
isMoneyRequestAction(reportAction),
9639+
);
9640+
for (const action of iouActions) {
9641+
const message = isMoneyRequestAction(action) ? getOriginalMessage(action) : undefined;
9642+
if (message?.IOUTransactionID === 'split-held-tx-1') {
9643+
split1ThreadReportID = action.childReportID;
9644+
} else if (message?.IOUTransactionID === 'split-held-tx-2') {
9645+
split2ThreadReportID = action.childReportID;
9646+
}
9647+
}
9648+
},
9649+
});
9650+
9651+
// Verify that split transaction thread IDs exist
9652+
expect(split1ThreadReportID).toBeDefined();
9653+
expect(split2ThreadReportID).toBeDefined();
9654+
9655+
// Verify each split transaction thread has hold report actions
9656+
// When splitting a held expense, new hold report actions should be created for each split
9657+
if (split1ThreadReportID) {
9658+
const split1ReportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${split1ThreadReportID}`);
9659+
const split1HoldActions = Object.values(split1ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD);
9660+
const split1CommentActions = Object.values(split1ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT);
9661+
9662+
// Should have at least one HOLD action and one ADD_COMMENT action (the hold comment)
9663+
// The hold actions are created optimistically with pendingAction: ADD, but this
9664+
// may be cleared to null after the API call succeeds
9665+
expect(split1HoldActions.length).toBeGreaterThanOrEqual(1);
9666+
expect(split1CommentActions.length).toBeGreaterThanOrEqual(1);
9667+
}
9668+
9669+
if (split2ThreadReportID) {
9670+
const split2ReportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${split2ThreadReportID}`);
9671+
const split2HoldActions = Object.values(split2ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD);
9672+
const split2CommentActions = Object.values(split2ReportActions ?? {}).filter((action) => action?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT);
9673+
9674+
// Should have at least one HOLD action and one ADD_COMMENT action (the hold comment)
9675+
expect(split2HoldActions.length).toBeGreaterThanOrEqual(1);
9676+
expect(split2CommentActions.length).toBeGreaterThanOrEqual(1);
9677+
}
9678+
});
94409679
});
94419680
});
94429681

0 commit comments

Comments
 (0)