diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts index 9c2783177777..e841fdfa16b4 100644 --- a/src/hooks/useDeleteTransactions.ts +++ b/src/hooks/useDeleteTransactions.ts @@ -38,6 +38,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const [iouReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(report?.reportID)}`); + const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const [betas] = useOnyx(ONYXKEYS.BETAS); const {isBetaEnabled} = usePermissions(); const archivedReportsIdSet = useArchivedReportsIdSet(); @@ -133,12 +134,17 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac return initSplitExpenseItemData(childTransaction, transactionReport); }); + const reportID = report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); + const parentTransactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; + const expenseReport = report?.type === CONST.REPORT.TYPE.EXPENSE ? report : parentTransactionReport; + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${expenseReport?.policyID}`] ?? {}; + updateSplitTransactions({ allTransactionsList: allTransactions, allReportsList: allReports, allReportNameValuePairsList: allReportNameValuePairs, transactionData: { - reportID: report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID), + reportID, originalTransactionID: transactionID, splitExpenses: remainingSplitExpenses, }, @@ -157,6 +163,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac quickAction, iouReportNextStep, betas, + policyTags, personalDetails, }); } @@ -209,6 +216,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac reportActions, transactionViolations, betas, + allPolicyTags, personalDetails, ], ); diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index 57cd3a527e57..32a4edc9bfd8 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -330,16 +330,6 @@ function splitBillAndOpenReport({ notifyNewAction(splitData.chatReportID, undefined, true); } -/** - * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. - * TODO: remove `getPolicyTagsData` from this file [https://github.com/Expensify/App/issues/80401] - * All usages of this function should be replaced with useOnyx hook in React components. - */ -function getPolicyTagsData(policyID: string | undefined) { - const allPolicyTags = getPolicyTags(); - return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - /** Used exclusively for starting a split expense request that contains a receipt, the split request will be completed once the receipt is scanned * or user enters details manually. * @@ -1044,8 +1034,9 @@ function updateSplitTransactions({ iouReportNextStep, isFromSplitExpensesFlow, betas, + policyTags, personalDetails, -}: UpdateSplitTransactionsParams) { +}: UpdateSplitTransactionsParams & {policyTags: OnyxTypes.PolicyTagLists}) { const transactionReport = getReportOrDraftReport(transactionData?.reportID); const parentTransactionReport = getReportOrDraftReport(transactionReport?.parentReportID); const expenseReport = transactionReport?.type === CONST.REPORT.TYPE.EXPENSE ? transactionReport : parentTransactionReport; @@ -1053,9 +1044,6 @@ function updateSplitTransactions({ const originalTransactionID = transactionData?.originalTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID; const originalTransaction = allTransactionsList?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; const originalTransactionDetails = getTransactionDetails(originalTransaction); - // TODO: remove `allPolicyTags` from this file [https://github.com/Expensify/App/issues/80401] - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport, currentUserPersonalDetails.accountID); const splitExpenses = transactionData?.splitExpenses ?? []; @@ -1688,10 +1676,12 @@ function updateSplitTransactions({ } function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransactionsParams) { - updateSplitTransactions({...params, isFromSplitExpensesFlow: true}); const transactionReport = getReportOrDraftReport(params.transactionData?.reportID); const parentTransactionReport = getReportOrDraftReport(transactionReport?.parentReportID); const expenseReport = transactionReport?.type === CONST.REPORT.TYPE.EXPENSE ? transactionReport : parentTransactionReport; + const policyTags = getPolicyTags()?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${expenseReport?.policyID}`] ?? {}; + + updateSplitTransactions({...params, isFromSplitExpensesFlow: true, policyTags}); const isSearchPageTopmostFullScreenRoute = isSearchTopmostFullScreenRoute(); const transactionThreadReportID = params.firstIOU?.childReportID; const transactionThreadReportScreen = Navigation.getReportRouteByID(transactionThreadReportID); diff --git a/tests/actions/IOUTest/SplitTest.ts b/tests/actions/IOUTest/SplitTest.ts index 0b4b5e4ceb71..5508bd3a133f 100644 --- a/tests/actions/IOUTest/SplitTest.ts +++ b/tests/actions/IOUTest/SplitTest.ts @@ -10,7 +10,7 @@ import {createWorkspace, generatePolicyID, setWorkspaceApprovalMode} from '@libs import initSplitExpense from '@libs/actions/SplitExpenses'; import {rand64} from '@libs/NumberUtils'; import {getOriginalMessage, isActionOfType, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {buildOptimisticIOUReportAction} from '@libs/ReportUtils'; +import {buildOptimisticIOUReportAction, getReportOrDraftReport} from '@libs/ReportUtils'; import { addSplitExpenseField, completeSplitBill, @@ -24,6 +24,7 @@ import { startSplitBill, updateSplitExpenseAmountField, updateSplitExpenseField, + updateSplitTransactions, updateSplitTransactionsFromSplitExpensesFlow, } from '@userActions/IOU/Split'; import CONST from '@src/CONST'; @@ -129,6 +130,22 @@ const currentUserPersonalDetails: CurrentUserPersonalDetails = { avatar: 'https://example.com/avatar.jpg', }; +const getPolicyTags = async (reportID: string) => { + let allPolicyTags: OnyxCollection; + await getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}`, + waitForCollectionCallback: true, + callback: (value) => { + allPolicyTags = value; + }, + }); + + const splitTransactionReport = getReportOrDraftReport(reportID); + const splitParentTransactionReport = getReportOrDraftReport(splitTransactionReport?.parentReportID); + const splitExpenseReport = splitTransactionReport?.type === CONST.REPORT.TYPE.EXPENSE ? splitTransactionReport : splitParentTransactionReport; + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${splitExpenseReport?.policyID}`] ?? {}; + return policyTags; +}; const getParticipantsPolicyTags = async (participants: IOUParticipant[]) => { let participantsPolicyTags: Record = {}; await getOnyxData({ @@ -2701,6 +2718,252 @@ describe('updateSplitTransactionsFromSplitExpensesFlow', () => { }); }); +describe('updateSplitTransactions', () => { + it('should create split transactions and move original to SPLIT_REPORT_ID', async () => { + const amount = 10000; + let expenseReport: OnyxEntry; + let chatReport: OnyxEntry; + let originalTransactionID: string | undefined; + let firstIOU: ReportAction | undefined; + + const policyID = generatePolicyID(); + createWorkspace({ + policyOwnerEmail: CARLOS_EMAIL, + makeMeAdmin: true, + policyName: "Carlos's Workspace", + policyID, + introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, + currentUserAccountIDParam: CARLOS_ACCOUNT_ID, + currentUserEmailParam: CARLOS_EMAIL, + isSelfTourViewed: false, + }); + 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); + }, + }); + + 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'}, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + currentUserAccountIDParam: RORY_ACCOUNT_ID, + currentUserEmailParam: RORY_EMAIL, + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + quickAction: undefined, + isSelfTourViewed: false, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + existingTransactionDraft: undefined, + draftTransactionIDs: [], + }); + await waitForBatchedUpdates(); + + await getOnyxData({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE); + }, + }); + await getOnyxData({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + waitForCollectionCallback: false, + callback: (allReportsAction) => { + const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + firstIOU = iouActions?.at(0); + originalTransactionID = isMoneyRequestAction(firstIOU) ? getOriginalMessage(firstIOU)?.IOUTransactionID : undefined; + }, + }); + + const originalTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`); + expect(originalTransaction?.reportID).not.toBe(CONST.REPORT.SPLIT_REPORT_ID); + + let allTransactions: OnyxCollection; + let allReports: OnyxCollection; + let allReportNameValuePairs: OnyxCollection; + await getOnyxData({key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, callback: (v) => (allTransactions = v)}); + await getOnyxData({key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (v) => (allReports = v)}); + await getOnyxData({key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, waitForCollectionCallback: true, callback: (v) => (allReportNameValuePairs = v)}); + + const reportID = originalTransaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); + const policyTags = await getPolicyTags(reportID); + + updateSplitTransactions({ + allTransactionsList: allTransactions, + allReportsList: allReports, + allReportNameValuePairsList: allReportNameValuePairs, + transactionData: { + reportID, + originalTransactionID: originalTransactionID ?? String(CONST.DEFAULT_NUMBER_ID), + splitExpenses: [ + {transactionID: 'split-1', amount: amount / 2, description: 'Split 1', created: DateUtils.getDBTime()}, + {transactionID: 'split-2', amount: amount / 2, description: 'Split 2', created: DateUtils.getDBTime()}, + ], + }, + searchContext: {currentSearchHash: -2}, + policyCategories: undefined, + policy: undefined, + policyRecentlyUsedCategories: [], + iouReport: expenseReport, + firstIOU, + isASAPSubmitBetaEnabled: false, + currentUserPersonalDetails, + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + quickAction: undefined, + iouReportNextStep: undefined, + betas: [CONST.BETAS.ALL], + policyTags, + personalDetails: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}}, + }); + await waitForBatchedUpdates(); + + const updatedOriginal = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`); + expect(updatedOriginal?.reportID).toBe(CONST.REPORT.SPLIT_REPORT_ID); + + const split1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}split-1`); + const split2 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}split-2`); + expect(split1).toBeDefined(); + expect(split2).toBeDefined(); + }); + + it('should preserve category and tag in split transactions', async () => { + const amount = 10000; + const testCategory = 'Travel'; + const testTag = 'business-trip'; + let expenseReport: OnyxEntry; + let chatReport: OnyxEntry; + let originalTransactionID: string | undefined; + + const policyID = generatePolicyID(); + createWorkspace({ + policyOwnerEmail: CARLOS_EMAIL, + makeMeAdmin: true, + policyName: "Carlos's Workspace", + policyID, + introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, + currentUserAccountIDParam: CARLOS_ACCOUNT_ID, + currentUserEmailParam: CARLOS_EMAIL, + isSelfTourViewed: false, + }); + 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); + }, + }); + + 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', category: testCategory, tag: testTag}, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + currentUserAccountIDParam: RORY_ACCOUNT_ID, + currentUserEmailParam: RORY_EMAIL, + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + quickAction: undefined, + isSelfTourViewed: false, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + existingTransactionDraft: undefined, + draftTransactionIDs: [], + }); + await waitForBatchedUpdates(); + + await getOnyxData({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE); + }, + }); + await getOnyxData({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + waitForCollectionCallback: false, + callback: (allReportsAction) => { + const iouActions = Object.values(allReportsAction ?? {}).filter((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + originalTransactionID = isMoneyRequestAction(iouActions?.at(0)) ? getOriginalMessage(iouActions?.at(0))?.IOUTransactionID : undefined; + }, + }); + + const originalTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`); + + let allTransactions: OnyxCollection; + let allReports: OnyxCollection; + let allReportNameValuePairs: OnyxCollection; + await getOnyxData({key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, callback: (v) => (allTransactions = v)}); + await getOnyxData({key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (v) => (allReports = v)}); + await getOnyxData({key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, waitForCollectionCallback: true, callback: (v) => (allReportNameValuePairs = v)}); + + const reportID = originalTransaction?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); + const policyTags = await getPolicyTags(reportID); + + updateSplitTransactions({ + allTransactionsList: allTransactions, + allReportsList: allReports, + allReportNameValuePairsList: allReportNameValuePairs, + transactionData: { + reportID, + originalTransactionID: originalTransactionID ?? String(CONST.DEFAULT_NUMBER_ID), + splitExpenses: [ + {transactionID: 'cat-tag-1', amount: amount / 2, description: 'Split 1', created: DateUtils.getDBTime(), category: testCategory, tags: [testTag]}, + {transactionID: 'cat-tag-2', amount: amount / 2, description: 'Split 2', created: DateUtils.getDBTime(), category: testCategory, tags: [testTag]}, + ], + }, + searchContext: {currentSearchHash: -2}, + policyCategories: undefined, + policy: undefined, + policyRecentlyUsedCategories: [], + iouReport: expenseReport, + firstIOU: undefined, + isASAPSubmitBetaEnabled: false, + currentUserPersonalDetails, + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + quickAction: undefined, + iouReportNextStep: undefined, + betas: [CONST.BETAS.ALL], + policyTags, + personalDetails: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}}, + }); + await waitForBatchedUpdates(); + + const split1 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}cat-tag-1`); + const split2 = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}cat-tag-2`); + expect(split1?.category).toBe(testCategory); + expect(split1?.tag).toBe(testTag); + expect(split2?.category).toBe(testCategory); + expect(split2?.tag).toBe(testTag); + }); +}); + describe('initSplitExpense', () => { it('should initialize split expense with correct transaction details', async () => { const transaction: Transaction = {