Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f831f4f
implementing bulk hold
Tony-MK May 19, 2025
44ba96a
refactoring
Tony-MK May 19, 2025
59eb659
Merge branch 'Expensify:main' into dev/BulkHold
Tony-MK May 19, 2025
7b08271
Merge pull request #60 from Tony-MK/dev/BulkHold
Tony-MK May 19, 2025
be271d3
creating BulkHoldRequestParams
Tony-MK May 20, 2025
7aed8d0
Merge branch 'Expensify:main' into BulkHold
Tony-MK May 21, 2025
78cd253
applying suggestions
Tony-MK May 22, 2025
b70fd35
Merge branch 'main' of https://github.com/Expensify/App into BulkHold
Tony-MK May 26, 2025
84bdbae
Merge branch 'main' of https://github.com/Expensify/App into BulkHold
Tony-MK May 27, 2025
a3dcab5
Merge branch 'Expensify:main' into BulkHold
Tony-MK May 28, 2025
9e09614
Merge branch 'main' into BulkHold
Tony-MK May 30, 2025
6de4c11
removing getIOUActionForReportID import
Tony-MK May 30, 2025
81c18c2
Merge branch 'main' into BulkHold
Tony-MK Jun 3, 2025
4240b31
tsc
Tony-MK Jun 3, 2025
bc1554e
Merge branch 'BulkHold' of https://github.com/Tony-MK/Expensify-App i…
Tony-MK Jun 3, 2025
b8fbf06
Merge branch 'Expensify:main' into BulkHold
Tony-MK Jun 4, 2025
c6c7d7f
Merge branch 'main' of https://github.com/Expensify/App into BulkHold
Tony-MK Jun 4, 2025
1354307
Delete src/pages/Search/SearchMoneyRequestReportHoldReasonPage.tsx
Tony-MK Jun 4, 2025
51620a0
lint
Tony-MK Jun 4, 2025
85ced92
Merge branch 'main' of https://github.com/Expensify/App into BulkHold
Tony-MK Jun 4, 2025
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
6 changes: 6 additions & 0 deletions src/libs/API/parameters/BulkHoldRequestParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type BulkHoldRequestParams = {
holdData: string; // This is a json object with shape Record<string, HoldDataEntry>;
comment: string;
};

export default BulkHoldRequestParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export type {default as SubmitReportParams} from './SubmitReportParams';
export type {default as DetachReceiptParams} from './DetachReceiptParams';
export type {default as PayMoneyRequestParams} from './PayMoneyRequestParams';
export type {default as HoldMoneyRequestParams} from './HoldMoneyRequestParams';
export type {default as BulkHoldRequestParams} from './BulkHoldRequestParams';
export type {default as UnHoldMoneyRequestParams} from './UnHoldMoneyRequestParams';
export type {default as CancelPaymentParams} from './CancelPaymentParams';
export type {default as AcceptACHContractForBankAccount} from './AcceptACHContractForBankAccount';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const WRITE_COMMANDS = {
UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription',
UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY: 'UpdateMoneyRequestAmountAndCurrency',
HOLD_MONEY_REQUEST: 'HoldRequest',
BULK_HOLD_MONEY_REQUEST: 'BulkHoldRequest',
UPDATE_BILLING_CARD_CURRENCY: 'UpdateBillingCardCurrency',
UNHOLD_MONEY_REQUEST: 'UnHoldRequest',
REQUEST_MONEY: 'RequestMoney',
Expand Down Expand Up @@ -674,6 +675,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.HOLD_MONEY_REQUEST]: Parameters.HoldMoneyRequestParams;
[WRITE_COMMANDS.BULK_HOLD_MONEY_REQUEST]: Parameters.BulkHoldRequestParams;
[WRITE_COMMANDS.UNHOLD_MONEY_REQUEST]: Parameters.UnHoldMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.REQUEST_MONEY]: Parameters.RequestMoneyParams;
Expand Down
256 changes: 250 additions & 6 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
import {
getAllReportActions,
getIOUActionForReportID,
getIOUActionForTransactionID,
getIOUReportIDFromReportActionPreview,
getLastVisibleAction,
getLastVisibleMessage,
Expand Down Expand Up @@ -360,6 +361,13 @@ type PayMoneyRequestData = {
failureData: OnyxUpdate[];
};

type HoldDataEntry = {
transactionThreadReportID?: string;
transactionThreadCreatedReportActionID?: string;
holdReportActionID: string;
commentReportActionID: string;
};

type SendMoneyParamsData = {
params: SendMoneyParams;
optimisticData: OnyxUpdate[];
Expand Down Expand Up @@ -10832,14 +10840,250 @@ function putOnHold(transactionID: string, comment: string, initialReportID: stri
Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID));
}

function putTransactionsOnHold(transactionsID: string[], comment: string, reportID: string) {
transactionsID.forEach((transactionID) => {
const {childReportID} = getIOUActionForReportID(reportID, transactionID) ?? {};
if (!childReportID) {
/**
* Put expenses on HOLD in bulk
*/
function bulkHold(transactionIDs: string[], comment: string, reportID: string, searchHash?: number) {
Comment thread
Tony-MK marked this conversation as resolved.
Resolved
const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const iouReportActions = Object.values(getAllReportActions(reportID));
const iouReportOptimisticData: Partial<OnyxTypes.Report> = {};
const holdData: Record<string, HoldDataEntry> = {};
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
const failureData: OnyxUpdate[] = [];

transactionIDs.forEach((transactionID) => {
const iouAction = getIOUActionForTransactionID(iouReportActions, transactionID);

if (!iouAction) {
return;
}
putOnHold(transactionID, comment, childReportID);

const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];

if (iouReport && iouReport.currency === transaction?.currency) {
const isExpenseReportLocal = isExpenseReport(iouReport);
const coefficient = isExpenseReportLocal ? -1 : 1;
const transactionAmount = getAmount(transaction, isExpenseReportLocal) * coefficient;
iouReportOptimisticData.unheldTotal = (iouReportOptimisticData.unheldTotal ?? 0) - transactionAmount;
iouReportOptimisticData.unheldNonReimbursableTotal = !transaction?.reimbursable
? (iouReportOptimisticData.unheldNonReimbursableTotal ?? 0) - transactionAmount
: iouReportOptimisticData.unheldNonReimbursableTotal;
}

const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? [];
const newViolation = {name: CONST.VIOLATIONS.HOLD, type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true};
const updatedViolations = [...transactionViolations, newViolation];

const currentTime = DateUtils.getDBTime();
const createdReportAction = buildOptimisticHoldReportAction(currentTime);
const createdReportActionComment = buildOptimisticHoldReportActionComment(comment, DateUtils.addMillisecondsFromDateTime(currentTime, 1));

holdData[transactionID] = {
holdReportActionID: createdReportAction.reportActionID,
commentReportActionID: createdReportActionComment.reportActionID,
};

let transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouAction?.childReportID}`];
if (!transactionThreadReport) {
const optimisticTransactionThread = buildTransactionThread(iouAction, iouReport);
const optimisticCreatedActionForTransactionThread = buildOptimisticCreatedReportAction(currentUserEmail);

// If the transactionThread is optimistic, we need the transactionThreadReportID and transactionThreadCreatedReportActionID in the holdData.
holdData[transactionID].transactionThreadReportID = optimisticTransactionThread.reportID;
holdData[transactionID].transactionThreadCreatedReportActionID = optimisticCreatedActionForTransactionThread.reportActionID;

transactionThreadReport = optimisticTransactionThread;

optimisticData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
value: {...optimisticTransactionThread, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
value: {[optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[iouAction.reportActionID]: {childReportID: optimisticTransactionThread.reportID}},
},
);

successData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
value: {pendingAction: null},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
value: {[optimisticCreatedActionForTransactionThread.reportActionID]: {pendingAction: null}},
},
);

failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticTransactionThread.reportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`,
value: {[optimisticCreatedActionForTransactionThread.reportActionID]: null},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
value: {[iouAction.reportActionID]: {childReportID: null}},
},
);
}

const parentReportActionOptimistic = getOptimisticDataForParentReportAction(
transactionThreadReport.reportID,
createdReportActionComment.created,
CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
);

optimisticData.push(
...parentReportActionOptimistic.filter((parentActionData): parentActionData is OnyxUpdate => parentActionData !== null),
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
value: {
[createdReportAction.reportActionID]: createdReportAction as ReportAction,
[createdReportActionComment.reportActionID]: createdReportActionComment as ReportAction,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
value: {
lastVisibleActionCreated: createdReportActionComment.created,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
value: {
[createdReportAction.reportActionID]: null,
[createdReportActionComment.reportActionID]: null,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
comment: {
hold: createdReportAction.reportActionID,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
value: updatedViolations,
},
);

successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
pendingAction: null,
},
});

failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {
pendingAction: null,
comment: {
hold: null,
},
errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericHoldExpenseFailureMessage'),
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
value: {
lastVisibleActionCreated: transactionThreadReport?.lastVisibleActionCreated,
},
},

{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
value: transactionViolations,
},
);

// If we are holding from the search page, we optimistically update the snapshot data that search uses so that it is kept in sync
if (searchHash) {

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.

The searchHash is not falsey when we are NOT in the search, it is -1, this is causing this: #63614

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.

Doesn't seem to be as simple as this, even if I change this check to searchHash !== -1 there is still a problem because we can still get a searchHash (I'm getting 72317659) and I'm not holding from the search. I think the final solution would be to check that the transaction actually exists in onyx so we don't merge incomplete stuff or something like that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, let me look into it.

optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`,
value: {
data: {
[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {
canHold: false,
canUnhold: true,
},
},
} as Record<string, Record<string, Partial<SearchTransaction>>>,
});

failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchHash}`,
value: {
data: {
[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {
canHold: true,
canUnhold: false,
},
},
} as Record<string, Record<string, Partial<SearchTransaction>>>,
});
}
});

optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: iouReportOptimisticData,
});

failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
unheldTotal: iouReport?.unheldTotal,
unheldNonReimbursableTotal: iouReport?.unheldNonReimbursableTotal,
},
});

API.write(
WRITE_COMMANDS.BULK_HOLD_MONEY_REQUEST,
{
holdData: JSON.stringify(holdData),
comment,
},
{optimisticData, successData, failureData},
);

const currentReportID = getDisplayedReportID(reportID);
Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, userAccountID));
}

/**
Expand Down Expand Up @@ -11899,7 +12143,7 @@ export {
payInvoice,
payMoneyRequest,
putOnHold,
putTransactionsOnHold,
bulkHold,
replaceReceipt,
requestMoney,
resetSplitShares,
Expand Down
4 changes: 2 additions & 2 deletions src/pages/Search/SearchHoldReasonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig
import {getFieldRequiredErrors} from '@libs/ValidationUtils';
import type {SearchReportParamList} from '@navigation/types';
import HoldReasonFormView from '@pages/iou/HoldReasonFormView';
import {putTransactionsOnHold} from '@userActions/IOU';
import {bulkHold} from '@userActions/IOU';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm';
Expand All @@ -22,7 +22,7 @@ function SearchHoldReasonPage({route}: PlatformStackScreenProps<Omit<SearchRepor
const onSubmit = useCallback(
({comment}: FormOnyxValues<typeof ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM>) => {
if (route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT_HOLD_TRANSACTIONS) {
putTransactionsOnHold(context.selectedTransactionIDs, comment, reportID);
bulkHold(context.selectedTransactionIDs, comment, reportID, context.currentSearchHash);
context.clearSelectedTransactions(true);
} else {
holdMoneyRequestOnSearch(context.currentSearchHash, Object.keys(context.selectedTransactions), comment);
Expand Down