diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 6aa047052e14..0a740e65747f 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -51,25 +51,19 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {MoneyRequestPreviewProps, PendingMessageProps} from './types'; function MoneyRequestPreviewContent({ - iouReport, isBillSplit, - session, action, - personalDetails, - chatReport, - transaction, contextMenuAnchor, chatReportID, reportID, onPreviewPressed, containerStyles, - walletTerms, checkIfContextMenuActive = () => {}, shouldShowPendingConversionMessage = false, isHovered = false, isWhisper = false, - transactionViolations, shouldDisplayContextMenu = true, + iouReportID, }: MoneyRequestPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -78,6 +72,16 @@ function MoneyRequestPreviewContent({ const {windowWidth} = useWindowDimensions(); const route = useRoute>(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`); + + const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action); + const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : '-1'; + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? -1; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx index c01206f83f55..f902948b2cb5 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx @@ -1,44 +1,18 @@ import lodashIsEmpty from 'lodash/isEmpty'; import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import {useOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import MoneyRequestPreviewContent from './MoneyRequestPreviewContent'; -import type {MoneyRequestPreviewOnyxProps, MoneyRequestPreviewProps} from './types'; +import type {MoneyRequestPreviewProps} from './types'; function MoneyRequestPreview(props: MoneyRequestPreviewProps) { + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${props.iouReportID || '-1'}`); // We should not render the component if there is no iouReport and it's not a split or track expense. // Moved outside of the component scope to allow for easier use of hooks in the main component. // eslint-disable-next-line react/jsx-props-no-spreading - return lodashIsEmpty(props.iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ; + return lodashIsEmpty(iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ; } MoneyRequestPreview.displayName = 'MoneyRequestPreview'; -export default withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - }, - session: { - key: ONYXKEYS.SESSION, - }, - transaction: { - key: ({action}) => { - const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action); - const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : 0; - return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; - }, - }, - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, -})(MoneyRequestPreview); +export default MoneyRequestPreview; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index 021ae5d188d9..c40b45c6d2bd 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -1,33 +1,9 @@ import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import type * as OnyxTypes from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -type MoneyRequestPreviewOnyxProps = { - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; - - /** Chat report associated with iouReport */ - chatReport: OnyxEntry; - - /** IOU report data object */ - iouReport: OnyxEntry; - - /** Session info for the currently logged in user. */ - session: OnyxEntry; - - /** The transaction attached to the action.message.iouTransactionID */ - transaction: OnyxEntry; - - /** The transaction violations attached to the action.message.iouTransactionID */ - transactionViolations: OnyxCollection; - - /** Information about the user accepting the terms for payments */ - walletTerms: OnyxEntry; -}; - -type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { +type MoneyRequestPreviewProps = { /** The active IOUReport, used for Onyx subscription */ // The iouReportID is used inside withOnyx HOC // eslint-disable-next-line react/no-unused-prop-types @@ -90,4 +66,4 @@ type PendingProps = { type PendingMessageProps = PendingProps | NoPendingProps; -export type {MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps, PendingMessageProps}; +export type {MoneyRequestPreviewProps, PendingMessageProps}; diff --git a/src/libs/API/parameters/ResolveDuplicatesParams.ts b/src/libs/API/parameters/ResolveDuplicatesParams.ts new file mode 100644 index 000000000000..d225f227c0d7 --- /dev/null +++ b/src/libs/API/parameters/ResolveDuplicatesParams.ts @@ -0,0 +1,24 @@ +type ResolveDuplicatesParams = { + /** The ID of the transaction that we want to keep */ + transactionID: string; + + /** The list of other duplicated transactions */ + transactionIDList: string[]; + created: string; + merchant: string; + amount: number; + currency: string; + category: string; + comment: string; + billable: boolean; + reimbursable: boolean; + tag: string; + + /** The reportActionID of the dismissed violation action in the kept transaction thread report */ + dismissedViolationReportActionID: string; + + /** The ID list of the hold report actions corresponding to the transactionIDList */ + reportActionIDList: string[]; +}; + +export default ResolveDuplicatesParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e5cde1b77be7..9f51cab3f360 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -238,6 +238,7 @@ export type {default as SendInvoiceParams} from './SendInvoiceParams'; export type {default as PayInvoiceParams} from './PayInvoiceParams'; export type {default as MarkAsCashParams} from './MarkAsCashParams'; export type {default as TransactionMergeParams} from './TransactionMergeParams'; +export type {default as ResolveDuplicatesParams} from './ResolveDuplicatesParams'; export type {default as UpdateSubscriptionTypeParams} from './UpdateSubscriptionTypeParams'; export type {default as SignUpUserParams} from './SignUpUserParams'; export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscriptionAutoRenewParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 88372382d3c8..cfbb1558f465 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -284,6 +284,7 @@ const WRITE_COMMANDS = { PAY_INVOICE: 'PayInvoice', MARK_AS_CASH: 'MarkAsCash', TRANSACTION_MERGE: 'Transaction_Merge', + RESOLVE_DUPLICATES: 'ResolveDuplicates', UPDATE_SUBSCRIPTION_TYPE: 'UpdateSubscriptionType', SIGN_UP_USER: 'SignUpUser', UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', @@ -701,6 +702,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams; [WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams; [WRITE_COMMANDS.TRANSACTION_MERGE]: Parameters.TransactionMergeParams; + [WRITE_COMMANDS.RESOLVE_DUPLICATES]: Parameters.ResolveDuplicatesParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_TYPE]: Parameters.UpdateSubscriptionTypeParams; [WRITE_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4221c9db2897..675fe6f211da 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -17,6 +17,7 @@ import type { PayMoneyRequestParams, ReplaceReceiptParams, RequestMoneyParams, + ResolveDuplicatesParams, SendInvoiceParams, SendMoneyParams, SetNameValuePairParams, @@ -8011,6 +8012,21 @@ function getIOURequestPolicyID(transaction: OnyxEntry, re return workspaceSender?.policyID ?? report?.policyID ?? '-1'; } +function getIOUActionForTransactions(transactionIDList: string[], iouReportID: string): Array> { + return Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`] ?? {})?.filter( + (reportAction): reportAction is ReportAction => { + if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) { + return false; + } + const message = ReportActionsUtils.getOriginalMessage(reportAction); + if (!message?.IOUTransactionID) { + return false; + } + return transactionIDList.includes(message.IOUTransactionID); + }, + ); +} + /** Merge several transactions into one by updating the fields of the one we want to keep and deleting the rest */ function mergeDuplicates(params: TransactionMergeParams) { const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`]; @@ -8095,18 +8111,7 @@ function mergeDuplicates(params: TransactionMergeParams) { }, }; - const iouActionsToDelete = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${params.reportID}`] ?? {})?.filter( - (reportAction): reportAction is ReportAction => { - if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) { - return false; - } - const message = ReportActionsUtils.getOriginalMessage(reportAction); - if (!message?.IOUTransactionID) { - return false; - } - return params.transactionIDList.includes(message.IOUTransactionID); - }, - ); + const iouActionsToDelete = getIOUActionForTransactions(params.transactionIDList, params.reportID); const deletedTime = DateUtils.getDBTime(); const expenseReportActionsOptimisticData: OnyxUpdate = { @@ -8167,6 +8172,125 @@ function mergeDuplicates(params: TransactionMergeParams) { API.write(WRITE_COMMANDS.TRANSACTION_MERGE, params, {optimisticData, failureData}); } +/** Instead of merging the duplicates, it updates the transaction we want to keep and puts the others on hold without deleting them */ +function resolveDuplicates(params: TransactionMergeParams) { + const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`]; + + const optimisticTransactionData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`, + value: { + ...originalSelectedTransaction, + billable: params.billable, + comment: { + comment: params.comment, + }, + category: params.category, + created: params.created, + currency: params.currency, + modifiedMerchant: params.merchant, + reimbursable: params.reimbursable, + tag: params.tag, + }, + }; + + const failureTransactionData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + value: originalSelectedTransaction as OnyxTypes.Transaction, + }; + + const optimisticTransactionViolations: OnyxUpdate[] = [...params.transactionIDList, params.transactionID].map((id) => { + const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []; + const newViolation = {name: CONST.VIOLATIONS.HOLD, type: CONST.VIOLATION_TYPES.VIOLATION}; + const updatedViolations = id === params.transactionID ? violations : [...violations, newViolation]; + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`, + value: updatedViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION), + }; + }); + + const failureTransactionViolations: OnyxUpdate[] = [...params.transactionIDList, params.transactionID].map((id) => { + const violations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []; + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`, + value: violations, + }; + }); + + const iouActionList = getIOUActionForTransactions(params.transactionIDList, params.reportID); + const transactionThreadReportIDList = iouActionList.map((action) => action?.childReportID); + const orderedTransactionIDList = iouActionList.map((action) => { + const message = ReportActionsUtils.getOriginalMessage(action); + return message?.IOUTransactionID ?? ''; + }); + + const optimisticHoldActions: OnyxUpdate[] = []; + const failureHoldActions: OnyxUpdate[] = []; + const reportActionIDList: string[] = []; + transactionThreadReportIDList.forEach((transactionThreadReportID) => { + const createdReportAction = ReportUtils.buildOptimisticHoldReportAction(); + reportActionIDList.push(createdReportAction.reportActionID); + optimisticHoldActions.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: { + [createdReportAction.reportActionID]: createdReportAction, + }, + }); + failureHoldActions.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: { + [createdReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericHoldExpenseFailureMessage'), + }, + }, + }); + }); + + const transactionThreadReportID = getIOUActionForTransactions([params.transactionID], params.reportID)?.[0]?.childReportID; + const optimisticReportAction = ReportUtils.buildOptimisticDismissedViolationReportAction({ + reason: 'manual', + violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }); + + const optimisticReportActionData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: { + [optimisticReportAction.reportActionID]: optimisticReportAction, + }, + }; + + const failureReportActionData: OnyxUpdate = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + value: { + [optimisticReportAction.reportActionID]: null, + }, + }; + + const optimisticData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + optimisticData.push(optimisticTransactionData, ...optimisticTransactionViolations, ...optimisticHoldActions, optimisticReportActionData); + failureData.push(failureTransactionData, ...failureTransactionViolations, ...failureHoldActions, failureReportActionData); + const {reportID, transactionIDList, receiptID, ...otherParams} = params; + + const parameters: ResolveDuplicatesParams = { + ...otherParams, + reportActionIDList, + transactionIDList: orderedTransactionIDList, + dismissedViolationReportActionID: optimisticReportAction.reportActionID, + }; + + API.write(WRITE_COMMANDS.RESOLVE_DUPLICATES, parameters, {optimisticData, failureData}); +} + export { adjustRemainingSplitShares, approveMoneyRequest, @@ -8237,5 +8361,7 @@ export { updateMoneyRequestTaxAmount, updateMoneyRequestTaxRate, mergeDuplicates, + resolveDuplicates, + prepareToCleanUpMoneyRequest, }; export type {GPSPoint as GpsPoint, IOURequestType}; diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx index 15217e215ad4..b9a4adb729e2 100644 --- a/src/pages/TransactionDuplicate/Confirmation.tsx +++ b/src/pages/TransactionDuplicate/Confirmation.tsx @@ -14,6 +14,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useReviewDuplicatesNavigation from '@hooks/useReviewDuplicatesNavigation'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -34,6 +35,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; function Confirmation() { const styles = useThemeStyles(); const {translate} = useLocalize(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const route = useRoute>(); const [reviewDuplicates, reviewDuplicatesResult] = useOnyx(ONYXKEYS.REVIEW_DUPLICATES); const transaction = useMemo(() => TransactionUtils.buildNewTransactionAfterReviewingDuplicates(reviewDuplicates), [reviewDuplicates]); @@ -41,17 +43,26 @@ function Confirmation() { const compareResult = TransactionUtils.compareDuplicateTransactionFields(transactionID); const {goBack} = useReviewDuplicatesNavigation(Object.keys(compareResult.change ?? {}), 'confirmation', route.params.threadReportID, route.params.backTo); const [report, reportResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transaction?.reportID}`); const reportAction = Object.values(reportActions ?? {}).find( (action) => ReportActionsUtils.isMoneyRequestAction(action) && ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID === reviewDuplicates?.transactionID, ); + const isReportOwner = iouReport?.ownerAccountID === currentUserPersonalDetails?.accountID; + const transactionsMergeParams = useMemo(() => TransactionUtils.buildTransactionsMergeParams(reviewDuplicates, transaction), [reviewDuplicates, transaction]); + const mergeDuplicates = useCallback(() => { IOU.mergeDuplicates(transactionsMergeParams); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportAction?.childReportID ?? '-1')); }, [reportAction?.childReportID, transactionsMergeParams]); + const resolveDuplicates = useCallback(() => { + IOU.resolveDuplicates(transactionsMergeParams); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportAction?.childReportID ?? '-1')); + }, [transactionsMergeParams, reportAction?.childReportID]); + const contextValue = useMemo( () => ({ transactionThreadReport: report, @@ -116,7 +127,13 @@ function Confirmation() {