From 4a7a3594f8c1e5014795f43b32857b07353ef3a1 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Wed, 25 Mar 2026 15:14:09 +0100 Subject: [PATCH 1/8] remove onyx.connect from getSendInvoiceInformation remove onyx.connect from getSendInvoiceInformation --- src/libs/actions/IOU/SendInvoice.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 29a2c298297c..87174e4606be 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -1,5 +1,5 @@ import {InteractionManager} from 'react-native'; -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {SendInvoiceParams} from '@libs/API/parameters'; @@ -596,7 +596,8 @@ function getSendInvoiceInformation({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, -}: SendInvoiceOptions): SendInvoiceInformation { + allPolicyTags, +}: SendInvoiceOptions & {allPolicyTags: OnyxCollection}): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID; @@ -652,8 +653,8 @@ function getSendInvoiceInformation({ const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80048 - // eslint-disable-next-line @typescript-eslint/no-deprecated - policyTags: getPolicyTagsData(optimisticInvoiceReport.policyID), + // eslint-disable-next-line + policyTags: allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${optimisticInvoiceReport.policyID}`] ?? {}, policyRecentlyUsedTags, transactionTags: tag, }); @@ -742,6 +743,7 @@ function sendInvoice({ policyRecentlyUsedTags, isFromGlobalCreate, }: SendInvoiceOptions) { + const allPolicyTags = getPolicyTags(); const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); if (transaction?.comment) { // eslint-disable-next-line no-param-reassign @@ -774,6 +776,7 @@ function sendInvoice({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, + allPolicyTags, }); const parameters: SendInvoiceParams = { From a1d6b8c8d39be5a1373faa2ebddd7b9b7a37d6f3 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Wed, 25 Mar 2026 16:20:58 +0100 Subject: [PATCH 2/8] remove onyx.connect from getSendInvoiceInformation part2 --- src/libs/actions/IOU/SendInvoice.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 87174e4606be..986c289820cb 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -1,5 +1,5 @@ import {InteractionManager} from 'react-native'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {SendInvoiceParams} from '@libs/API/parameters'; @@ -596,8 +596,8 @@ function getSendInvoiceInformation({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, - allPolicyTags, -}: SendInvoiceOptions & {allPolicyTags: OnyxCollection}): SendInvoiceInformation { + policyTags, +}: SendInvoiceOptions & {policyTags: OnyxTypes.PolicyTagLists}): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID; @@ -654,7 +654,7 @@ function getSendInvoiceInformation({ const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80048 // eslint-disable-next-line - policyTags: allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${optimisticInvoiceReport.policyID}`] ?? {}, + policyTags: policyTags ?? {}, policyRecentlyUsedTags, transactionTags: tag, }); @@ -743,7 +743,9 @@ function sendInvoice({ policyRecentlyUsedTags, isFromGlobalCreate, }: SendInvoiceOptions) { - const allPolicyTags = getPolicyTags(); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const policyTags = getPolicyTagsData(transaction?.participants?.find(p => p?.isSender)?.policyID); + const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); if (transaction?.comment) { // eslint-disable-next-line no-param-reassign @@ -776,7 +778,7 @@ function sendInvoice({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, - allPolicyTags, + policyTags, }); const parameters: SendInvoiceParams = { From 0da27b61c744b87e20fb13d3d351a11990178c60 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Wed, 25 Mar 2026 17:13:28 +0100 Subject: [PATCH 3/8] add tests to getSendInvoiceInformation() --- src/libs/actions/IOU/SendInvoice.ts | 14 ++- tests/actions/IOUTest/SendInvoiceTest.ts | 109 +++++++++++++++++++++++ 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 986c289820cb..33788cf61c25 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -596,8 +596,8 @@ function getSendInvoiceInformation({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, - policyTags, -}: SendInvoiceOptions & {policyTags: OnyxTypes.PolicyTagLists}): SendInvoiceInformation { + participantsPolicyTags, +}: SendInvoiceOptions & {participantsPolicyTags: OnyxTypes.PolicyTagLists}): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID; @@ -652,9 +652,7 @@ function getSendInvoiceInformation({ const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80048 - // eslint-disable-next-line - policyTags: policyTags ?? {}, + policyTags: participantsPolicyTags, policyRecentlyUsedTags, transactionTags: tag, }); @@ -744,8 +742,8 @@ function sendInvoice({ isFromGlobalCreate, }: SendInvoiceOptions) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const policyTags = getPolicyTagsData(transaction?.participants?.find(p => p?.isSender)?.policyID); - + const participantsPolicyTags = getPolicyTagsData(transaction?.participants?.find((p) => p?.isSender)?.policyID) ?? {}; + const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); if (transaction?.comment) { // eslint-disable-next-line no-param-reassign @@ -778,7 +776,7 @@ function sendInvoice({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, - policyTags, + participantsPolicyTags, }); const parameters: SendInvoiceParams = { diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index 291157a0e872..a56506f2cfff 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -164,6 +164,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: existingRecentlyUsedCategories, + participantsPolicyTags: {}, }); // Then: Verify optimistic data is generated when policyRecentlyUsedCategories are provided @@ -211,6 +212,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: undefined, + participantsPolicyTags: {}, }); expect(result.onyxData.optimisticData).toBeDefined(); @@ -289,6 +291,7 @@ describe('actions/SendInvoice', () => { companyName: 'Test Company Inc.', companyWebsite: 'https://testcompany.com', policyRecentlyUsedCategories: ['Services', 'Consulting'], + participantsPolicyTags: mockPolicyTagList as PolicyTagLists, }); // Then: Verify the result structure and key values @@ -390,6 +393,7 @@ describe('actions/SendInvoice', () => { companyName: 'Client Company Ltd.', companyWebsite: 'https://clientcompany.com', policyRecentlyUsedCategories: [], + participantsPolicyTags: {}, }); // Then: Verify the result uses existing chat report @@ -452,6 +456,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + participantsPolicyTags: {}, }); // Then: Verify receipt handling @@ -500,6 +505,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + participantsPolicyTags: {}, }); // Then: Verify function handles missing data gracefully @@ -551,6 +557,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + participantsPolicyTags: {}, }); expect(result.invoiceRoom).toBeDefined(); @@ -622,12 +629,114 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + participantsPolicyTags: {}, }); expect(result.invoiceRoom).toBeDefined(); expect(result.invoiceRoom.reportID).toBe(existingReportID); expect(result.invoiceRoom.reportID).not.toBe(preGeneratedReportID); }); + + it('should build optimistic recently used tags from participantsPolicyTags', async () => { + // Given: A transaction with a tag and policy tags seeded in Onyx + const policyID = 'workspace_tags_test'; + const tagListName = 'Department'; + const transactionTag = 'Engineering'; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, { + [tagListName]: { + name: tagListName, + orderWeight: 0, + required: false, + tags: { + Engineering: {name: 'Engineering', enabled: true}, + Marketing: {name: 'Marketing', enabled: true}, + }, + }, + }); + await waitForBatchedUpdates(); + + const participantsPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + + const policyRecentlyUsedTags: RecentlyUsedTags = { + [tagListName]: ['Marketing'], + }; + + const mockTransaction = { + transactionID: 'transaction_tags_test', + reportID: 'report_tags_test', + amount: 100, + currency: 'USD', + created: '2024-02-01', + merchant: 'Tags Test', + tag: transactionTag, + participants: [ + {accountID: 123, isSender: true, policyID}, + {accountID: 456, isSender: false}, + ], + }; + + // When: Call getSendInvoiceInformation with participantsPolicyTags read from Onyx + const result = getSendInvoiceInformation({ + transaction: mockTransaction as OnyxEntry, + currentUserAccountID: 123, + policyRecentlyUsedCurrencies: [], + policyRecentlyUsedTags, + participantsPolicyTags: participantsPolicyTags ?? {}, + }); + + // Then: optimisticData should contain a POLICY_RECENTLY_USED_TAGS update with the transaction tag prepended + const recentlyUsedTagsUpdate = result.onyxData.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`); + + expect(recentlyUsedTagsUpdate).toBeDefined(); + expect(recentlyUsedTagsUpdate?.value).toMatchObject({ + [tagListName]: [transactionTag, 'Marketing'], + }); + }); + + it('should not include recently used tags update when transaction has no tag', async () => { + // Given: A transaction with no tag and policy tags seeded in Onyx + const policyID = 'workspace_notags'; + const tagListName = 'Department'; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, { + [tagListName]: { + name: tagListName, + orderWeight: 0, + required: false, + tags: {Engineering: {name: 'Engineering', enabled: true}}, + }, + }); + await waitForBatchedUpdates(); + + const participantsPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + + const mockTransaction = { + transactionID: 'transaction_no_tags', + reportID: 'report_no_tags', + amount: 100, + currency: 'USD', + created: '2024-02-01', + merchant: 'No Tags Test', + participants: [ + {accountID: 123, isSender: true, policyID}, + {accountID: 456, isSender: false}, + ], + }; + + // When: Call getSendInvoiceInformation without a tag on the transaction + const result = getSendInvoiceInformation({ + transaction: mockTransaction as OnyxEntry, + currentUserAccountID: 123, + policyRecentlyUsedCurrencies: [], + participantsPolicyTags: participantsPolicyTags ?? {}, + }); + + // Then: No POLICY_RECENTLY_USED_TAGS update should be in optimisticData + const recentlyUsedTagsUpdate = result.onyxData.optimisticData?.find((update) => String(update.key).startsWith(ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS)); + + expect(recentlyUsedTagsUpdate).toBeUndefined(); + }); }); describe('sendInvoice', () => { it('creates a new invoice chat when one has been converted from individual to business', async () => { From 8f7767c6c4683efc69ec7a1f0f29e76cbe9bb8df Mon Sep 17 00:00:00 2001 From: jakubstec Date: Thu, 26 Mar 2026 10:50:39 +0100 Subject: [PATCH 4/8] refactor getSendInvoiceInformation() tests --- tests/actions/IOUTest/SendInvoiceTest.ts | 226 ++++------------------- 1 file changed, 38 insertions(+), 188 deletions(-) diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index a56506f2cfff..3ceb26eee458 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -22,6 +22,7 @@ import getOnyxValue from '../../utils/getOnyxValue'; import type {MockFetch} from '../../utils/TestHelper'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; +import {getPolicyTags} from '@libs/actions/IOU'; const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ @@ -121,39 +122,33 @@ describe('actions/SendInvoice', () => { expect(result).toBe(CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL); }); }); + + const baseParticipants = [ + {accountID: 123, isSender: true, policyID: 'workspace_test'}, + {accountID: 456, isSender: false}, + ]; + + const baseSenderPolicyID = baseParticipants.find((p) => p.isSender)?.policyID; + const baseParticipantsPolicyTags = getPolicyTags()?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${baseSenderPolicyID}`] ?? {}; + + const baseTransaction = { + transactionID: 'transaction_base', + reportID: 'report_base', + amount: 100, + currency: 'USD', + created: '2024-02-01', + merchant: 'Test Merchant', + participants: baseParticipants, + }; + describe('getSendInvoiceInformation', () => { it('should merge policyRecentlyUsedCategories when provided', () => { - // Given: Transaction with a category and existing recently used categories - const mockTransaction = { - transactionID: 'transaction_categories', - reportID: 'report_categories', - amount: 200, - currency: 'USD', - created: '2024-02-01', - merchant: 'Category Test', - category: 'Meals', - comment: { - comment: 'Invoice with categories', - }, - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_categories', - }, - { - accountID: 456, - isSender: false, - }, - ], - }; - const currentUserAccountID = 123; const existingRecentlyUsedCategories: OnyxEntry = []; // When: Call getSendInvoiceInformation with policyRecentlyUsedCategories const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -164,7 +159,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: existingRecentlyUsedCategories, - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); // Then: Verify optimistic data is generated when policyRecentlyUsedCategories are provided @@ -174,34 +169,11 @@ describe('actions/SendInvoice', () => { it('should merge policyRecentlyUsedCurrencies when currency is provided in transaction', () => { const testCurrency = CONST.CURRENCY.EUR; const initialCurrencies = [CONST.CURRENCY.USD, CONST.CURRENCY.GBP]; - const mockTransaction = { - transactionID: 'transaction_currency', - reportID: 'report_currency', - amount: 200, - currency: testCurrency, - created: '2024-02-01', - merchant: 'Currency Test', - category: 'Meals', - comment: { - comment: 'Invoice with currency', - }, - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_currency', - }, - { - accountID: 456, - isSender: false, - }, - ], - }; const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: {...baseTransaction, currency: CONST.CURRENCY.EUR} as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: initialCurrencies, invoiceChatReport: undefined, @@ -212,7 +184,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: undefined, - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); expect(result.onyxData.optimisticData).toBeDefined(); @@ -226,34 +198,6 @@ describe('actions/SendInvoice', () => { it('should return correct invoice information with new chat report', () => { // Given: Mock transaction data - const mockTransaction = { - transactionID: 'transaction_123', - reportID: 'report_123', - amount: 500, - currency: 'USD', - created: '2024-01-15', - merchant: 'Test Company', - category: 'Services', - tag: 'Project B', - taxCode: 'TAX001', - taxAmount: 50, - billable: true, - comment: { - comment: 'Invoice for consulting services', - }, - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_123', - }, - { - accountID: 456, - isSender: false, - }, - ], - }; - const currentUserAccountID = 123; const mockPolicy = createRandomPolicy(1); @@ -280,7 +224,7 @@ describe('actions/SendInvoice', () => { // When: Call getSendInvoiceInformation const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -296,7 +240,7 @@ describe('actions/SendInvoice', () => { // Then: Verify the result structure and key values expect(result).toMatchObject({ - senderWorkspaceID: 'workspace_123', + senderWorkspaceID: 'workspace_test', invoiceReportID: expect.any(String), transactionID: expect.any(String), transactionThreadReportID: expect.any(String), @@ -350,39 +294,14 @@ describe('actions/SendInvoice', () => { }, }; - const mockTransaction = { - transactionID: 'transaction_456', - reportID: 'report_456', - amount: 750, - currency: 'EUR', - created: '2024-01-20', - merchant: 'Client Company', - category: 'Development', - tag: 'Project C', - taxCode: 'TAX002', - taxAmount: 75, - billable: true, - comment: { - comment: 'Invoice for development work', - }, - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_456', - }, - { - accountID: 456, - isSender: false, - }, - ], - }; - const currentUserAccountID = 123; // When: Call getSendInvoiceInformation with existing chat report const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: { + ...baseTransaction, + participants: [{...baseParticipants.at(0), policyID: 'workspace_456'}, baseParticipants.at(1)], + } as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: existingInvoiceChatReport as OnyxEntry, @@ -393,7 +312,7 @@ describe('actions/SendInvoice', () => { companyName: 'Client Company Ltd.', companyWebsite: 'https://clientcompany.com', policyRecentlyUsedCategories: [], - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); // Then: Verify the result uses existing chat report @@ -407,33 +326,6 @@ describe('actions/SendInvoice', () => { it('should handle receipt attachment correctly', () => { // Given: Transaction with receipt - const mockTransaction = { - transactionID: 'transaction_789', - reportID: 'report_789', - amount: 300, - currency: 'USD', - created: '2024-01-25', - merchant: 'Receipt Company', - category: 'Equipment', - tag: 'Hardware', - taxCode: 'TAX003', - taxAmount: 30, - billable: true, - comment: { - comment: 'Invoice with receipt', - }, - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_789', - }, - { - accountID: 456, - isSender: false, - }, - ], - }; const mockReceipt = { source: 'receipt_source_123', @@ -445,7 +337,7 @@ describe('actions/SendInvoice', () => { // When: Call getSendInvoiceInformation with receipt const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -456,7 +348,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); // Then: Verify receipt handling @@ -505,7 +397,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); // Then: Verify function handles missing data gracefully @@ -519,33 +411,11 @@ describe('actions/SendInvoice', () => { it('should use provided invoiceChatReportID when creating new invoice chat', () => { const preGeneratedReportID = 'pre_generated_invoice_chat_123'; - const mockTransaction = { - transactionID: 'transaction_with_report_id', - reportID: 'report_with_id', - amount: 500, - currency: 'USD', - created: '2024-02-01', - merchant: 'Test Merchant', - comment: { - comment: 'Invoice with pre-generated report ID', - }, - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_test', - }, - { - accountID: 456, - isSender: false, - }, - ], - }; const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -557,7 +427,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); expect(result.invoiceRoom).toBeDefined(); @@ -594,30 +464,10 @@ describe('actions/SendInvoice', () => { }, }; - const mockTransaction = { - transactionID: 'transaction_existing_chat', - reportID: 'report_existing', - amount: 300, - currency: 'USD', - created: '2024-02-01', - merchant: 'Existing Chat Test', - participants: [ - { - accountID: 123, - isSender: true, - policyID: 'workspace_existing', - }, - { - accountID: receiverAccountID, - isSender: false, - }, - ], - }; - const currentUserAccountID = 123; const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: existingInvoiceChatReport as OnyxEntry, @@ -629,7 +479,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: {}, + participantsPolicyTags: baseParticipantsPolicyTags, }); expect(result.invoiceRoom).toBeDefined(); From c0c90ec964cb6739d790f38488c2f1c6bdf46700 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Mon, 30 Mar 2026 17:11:51 +0200 Subject: [PATCH 5/8] apply changes based on the review --- tests/actions/IOUTest/SendInvoiceTest.ts | 42 +++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index 3ceb26eee458..b5072b40ae6e 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import {getPolicyTags} from '@libs/actions/IOU'; import {getReceiverType, getSendInvoiceInformation, sendInvoice} from '@libs/actions/IOU/SendInvoice'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -22,7 +23,6 @@ import getOnyxValue from '../../utils/getOnyxValue'; import type {MockFetch} from '../../utils/TestHelper'; import {getGlobalFetchMock} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -import {getPolicyTags} from '@libs/actions/IOU'; const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ @@ -123,25 +123,29 @@ describe('actions/SendInvoice', () => { }); }); - const baseParticipants = [ - {accountID: 123, isSender: true, policyID: 'workspace_test'}, - {accountID: 456, isSender: false}, - ]; - - const baseSenderPolicyID = baseParticipants.find((p) => p.isSender)?.policyID; - const baseParticipantsPolicyTags = getPolicyTags()?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${baseSenderPolicyID}`] ?? {}; - - const baseTransaction = { - transactionID: 'transaction_base', - reportID: 'report_base', - amount: 100, - currency: 'USD', - created: '2024-02-01', - merchant: 'Test Merchant', - participants: baseParticipants, - }; - describe('getSendInvoiceInformation', () => { + const baseParticipants = [ + {accountID: 123, isSender: true, policyID: 'workspace_test'}, + {accountID: 456, isSender: false}, + ]; + + const baseSenderPolicyID = baseParticipants.find((p) => p.isSender)?.policyID; + + let baseParticipantsPolicyTags: PolicyTagLists; + beforeEach(async () => { + baseParticipantsPolicyTags = (await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${baseSenderPolicyID}`)) ?? {}; + }); + + const baseTransaction = { + transactionID: 'transaction_base', + reportID: 'report_base', + amount: 100, + currency: 'USD', + created: '2024-02-01', + merchant: 'Test Merchant', + participants: baseParticipants, + }; + it('should merge policyRecentlyUsedCategories when provided', () => { const currentUserAccountID = 123; const existingRecentlyUsedCategories: OnyxEntry = []; From 6a8855dc167de7ecaac4eb684ef863ce544a0b18 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Mon, 30 Mar 2026 17:46:46 +0200 Subject: [PATCH 6/8] fix eslint, typecheck, spelling errors --- src/libs/actions/IOU/SendInvoice.ts | 1 - tests/actions/IOUTest/SendInvoiceTest.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 91f842302ce7..0d52faae3032 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -647,7 +647,6 @@ function getSendInvoiceInformation({ tag, taxCode, taxAmount, - taxValue, billable, reimbursable: true, }, diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index b5072b40ae6e..c570205b615e 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import {getPolicyTags} from '@libs/actions/IOU'; import {getReceiverType, getSendInvoiceInformation, sendInvoice} from '@libs/actions/IOU/SendInvoice'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -550,7 +549,7 @@ describe('actions/SendInvoice', () => { it('should not include recently used tags update when transaction has no tag', async () => { // Given: A transaction with no tag and policy tags seeded in Onyx - const policyID = 'workspace_notags'; + const policyID = 'workspace_no_tags'; const tagListName = 'Department'; await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, { From 4944f31d04a3382185d48b1c6a3dc25f394e8bd7 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Tue, 31 Mar 2026 11:37:27 +0200 Subject: [PATCH 7/8] add review changes --- src/libs/actions/IOU/SendInvoice.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 0d52faae3032..35277d9d23c2 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -598,9 +598,9 @@ function getSendInvoiceInformation({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, - participantsPolicyTags, -}: SendInvoiceOptions & {participantsPolicyTags: OnyxTypes.PolicyTagLists}): SendInvoiceInformation { - const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; + senderPolicyTags, +}: SendInvoiceOptions & {senderPolicyTags: OnyxTypes.PolicyTagLists}): SendInvoiceInformation { + const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, taxValue, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID; const receiverParticipant: Participant | InvoiceReceiver | undefined = @@ -647,6 +647,7 @@ function getSendInvoiceInformation({ tag, taxCode, taxAmount, + taxValue, billable, reimbursable: true, }, @@ -654,7 +655,7 @@ function getSendInvoiceInformation({ const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - policyTags: participantsPolicyTags, + policyTags: senderPolicyTags, policyRecentlyUsedTags, transactionTags: tag, }); @@ -743,8 +744,9 @@ function sendInvoice({ policyRecentlyUsedTags, isFromGlobalCreate, }: SendInvoiceOptions) { + // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80048 // eslint-disable-next-line @typescript-eslint/no-deprecated - const participantsPolicyTags = getPolicyTagsData(transaction?.participants?.find((p) => p?.isSender)?.policyID) ?? {}; + const senderPolicyTags = getPolicyTagsData(transaction?.participants?.find((p) => p?.isSender)?.policyID) ?? {}; const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); if (transaction?.comment) { @@ -778,7 +780,7 @@ function sendInvoice({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, - participantsPolicyTags, + senderPolicyTags, }); const parameters: SendInvoiceParams = { From 0f546e3c58650be93c86930956c50391887e7122 Mon Sep 17 00:00:00 2001 From: jakubstec Date: Tue, 31 Mar 2026 12:42:12 +0200 Subject: [PATCH 8/8] replace name participantsPolicyTags with senderPolicyTags in SendInvoiceTest.ts --- tests/actions/IOUTest/SendInvoiceTest.ts | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index c570205b615e..dce9ac4c947f 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -130,9 +130,9 @@ describe('actions/SendInvoice', () => { const baseSenderPolicyID = baseParticipants.find((p) => p.isSender)?.policyID; - let baseParticipantsPolicyTags: PolicyTagLists; + let baseSenderPolicyTags: PolicyTagLists; beforeEach(async () => { - baseParticipantsPolicyTags = (await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${baseSenderPolicyID}`)) ?? {}; + baseSenderPolicyTags = (await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${baseSenderPolicyID}`)) ?? {}; }); const baseTransaction = { @@ -162,7 +162,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: existingRecentlyUsedCategories, - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify optimistic data is generated when policyRecentlyUsedCategories are provided @@ -187,7 +187,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: undefined, - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); expect(result.onyxData.optimisticData).toBeDefined(); @@ -238,7 +238,7 @@ describe('actions/SendInvoice', () => { companyName: 'Test Company Inc.', companyWebsite: 'https://testcompany.com', policyRecentlyUsedCategories: ['Services', 'Consulting'], - participantsPolicyTags: mockPolicyTagList as PolicyTagLists, + senderPolicyTags: mockPolicyTagList as PolicyTagLists, }); // Then: Verify the result structure and key values @@ -315,7 +315,7 @@ describe('actions/SendInvoice', () => { companyName: 'Client Company Ltd.', companyWebsite: 'https://clientcompany.com', policyRecentlyUsedCategories: [], - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify the result uses existing chat report @@ -351,7 +351,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify receipt handling @@ -400,7 +400,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify function handles missing data gracefully @@ -430,7 +430,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); expect(result.invoiceRoom).toBeDefined(); @@ -482,7 +482,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], - participantsPolicyTags: baseParticipantsPolicyTags, + senderPolicyTags: baseSenderPolicyTags, }); expect(result.invoiceRoom).toBeDefined(); @@ -490,7 +490,7 @@ describe('actions/SendInvoice', () => { expect(result.invoiceRoom.reportID).not.toBe(preGeneratedReportID); }); - it('should build optimistic recently used tags from participantsPolicyTags', async () => { + it('should build optimistic recently used tags from senderPolicyTags', async () => { // Given: A transaction with a tag and policy tags seeded in Onyx const policyID = 'workspace_tags_test'; const tagListName = 'Department'; @@ -509,7 +509,7 @@ describe('actions/SendInvoice', () => { }); await waitForBatchedUpdates(); - const participantsPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const senderPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const policyRecentlyUsedTags: RecentlyUsedTags = { [tagListName]: ['Marketing'], @@ -529,13 +529,13 @@ describe('actions/SendInvoice', () => { ], }; - // When: Call getSendInvoiceInformation with participantsPolicyTags read from Onyx + // When: Call getSendInvoiceInformation with senderPolicyTags read from Onyx const result = getSendInvoiceInformation({ transaction: mockTransaction as OnyxEntry, currentUserAccountID: 123, policyRecentlyUsedCurrencies: [], policyRecentlyUsedTags, - participantsPolicyTags: participantsPolicyTags ?? {}, + senderPolicyTags: senderPolicyTags ?? {}, }); // Then: optimisticData should contain a POLICY_RECENTLY_USED_TAGS update with the transaction tag prepended @@ -562,7 +562,7 @@ describe('actions/SendInvoice', () => { }); await waitForBatchedUpdates(); - const participantsPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const senderPolicyTags = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const mockTransaction = { transactionID: 'transaction_no_tags', @@ -582,7 +582,7 @@ describe('actions/SendInvoice', () => { transaction: mockTransaction as OnyxEntry, currentUserAccountID: 123, policyRecentlyUsedCurrencies: [], - participantsPolicyTags: participantsPolicyTags ?? {}, + senderPolicyTags: senderPolicyTags ?? {}, }); // Then: No POLICY_RECENTLY_USED_TAGS update should be in optimisticData