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
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) {
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
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
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
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