Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion src/hooks/useDeleteTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
},
Expand All @@ -157,6 +163,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
quickAction,
iouReportNextStep,
betas,
policyTags,
personalDetails,
});
}
Expand Down Expand Up @@ -209,6 +216,7 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
reportActions,
transactionViolations,
betas,
allPolicyTags,
personalDetails,
],
);
Expand Down
20 changes: 5 additions & 15 deletions src/libs/actions/IOU/Split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -1044,18 +1034,16 @@ 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;

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 ?? [];

Expand Down Expand Up @@ -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);
Expand Down
265 changes: 264 additions & 1 deletion tests/actions/IOUTest/SplitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,6 +24,7 @@ import {
startSplitBill,
updateSplitExpenseAmountField,
updateSplitExpenseField,
updateSplitTransactions,
updateSplitTransactionsFromSplitExpensesFlow,
} from '@userActions/IOU/Split';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -129,6 +130,22 @@ const currentUserPersonalDetails: CurrentUserPersonalDetails = {
avatar: 'https://example.com/avatar.jpg',
};

const getPolicyTags = async (reportID: string) => {
let allPolicyTags: OnyxCollection<PolicyTagLists>;
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<string, PolicyTagLists> = {};
await getOnyxData({
Expand Down Expand Up @@ -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<Report>;
let chatReport: OnyxEntry<Report>;
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<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
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<Transaction>;
let allReports: OnyxCollection<Report>;
let allReportNameValuePairs: OnyxCollection<ReportNameValuePairs>;
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<Report>;
let chatReport: OnyxEntry<Report>;
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<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
isMoneyRequestAction(reportAction),
);
originalTransactionID = isMoneyRequestAction(iouActions?.at(0)) ? getOriginalMessage(iouActions?.at(0))?.IOUTransactionID : undefined;
},
});

const originalTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`);

let allTransactions: OnyxCollection<Transaction>;
let allReports: OnyxCollection<Report>;
let allReportNameValuePairs: OnyxCollection<ReportNameValuePairs>;
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 = {
Expand Down
Loading