diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index deffc971dfa7..35277d9d23c2 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -598,7 +598,8 @@ function getSendInvoiceInformation({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, -}: SendInvoiceOptions): SendInvoiceInformation { + 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; @@ -654,9 +655,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 @typescript-eslint/no-deprecated - policyTags: getPolicyTagsData(optimisticInvoiceReport.policyID), + policyTags: senderPolicyTags, policyRecentlyUsedTags, transactionTags: tag, }); @@ -745,6 +744,10 @@ 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 senderPolicyTags = 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 @@ -777,6 +780,7 @@ function sendInvoice({ companyWebsite, policyRecentlyUsedCategories, policyRecentlyUsedTags, + senderPolicyTags, }); const parameters: SendInvoiceParams = { diff --git a/tests/actions/IOUTest/SendInvoiceTest.ts b/tests/actions/IOUTest/SendInvoiceTest.ts index 291157a0e872..dce9ac4c947f 100644 --- a/tests/actions/IOUTest/SendInvoiceTest.ts +++ b/tests/actions/IOUTest/SendInvoiceTest.ts @@ -121,39 +121,37 @@ describe('actions/SendInvoice', () => { expect(result).toBe(CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL); }); }); + 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 baseParticipants = [ + {accountID: 123, isSender: true, policyID: 'workspace_test'}, + {accountID: 456, isSender: false}, + ]; + + const baseSenderPolicyID = baseParticipants.find((p) => p.isSender)?.policyID; + let baseSenderPolicyTags: PolicyTagLists; + beforeEach(async () => { + baseSenderPolicyTags = (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 = []; // When: Call getSendInvoiceInformation with policyRecentlyUsedCategories const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -164,6 +162,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: existingRecentlyUsedCategories, + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify optimistic data is generated when policyRecentlyUsedCategories are provided @@ -173,34 +172,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, @@ -211,6 +187,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: undefined, + senderPolicyTags: baseSenderPolicyTags, }); expect(result.onyxData.optimisticData).toBeDefined(); @@ -224,34 +201,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); @@ -278,7 +227,7 @@ describe('actions/SendInvoice', () => { // When: Call getSendInvoiceInformation const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -289,11 +238,12 @@ describe('actions/SendInvoice', () => { companyName: 'Test Company Inc.', companyWebsite: 'https://testcompany.com', policyRecentlyUsedCategories: ['Services', 'Consulting'], + senderPolicyTags: mockPolicyTagList as PolicyTagLists, }); // 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), @@ -347,39 +297,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, @@ -390,6 +315,7 @@ describe('actions/SendInvoice', () => { companyName: 'Client Company Ltd.', companyWebsite: 'https://clientcompany.com', policyRecentlyUsedCategories: [], + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify the result uses existing chat report @@ -403,33 +329,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', @@ -441,7 +340,7 @@ describe('actions/SendInvoice', () => { // When: Call getSendInvoiceInformation with receipt const result = getSendInvoiceInformation({ - transaction: mockTransaction as OnyxEntry, + transaction: baseTransaction as OnyxEntry, currentUserAccountID, policyRecentlyUsedCurrencies: [], invoiceChatReport: undefined, @@ -452,6 +351,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify receipt handling @@ -500,6 +400,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + senderPolicyTags: baseSenderPolicyTags, }); // Then: Verify function handles missing data gracefully @@ -513,33 +414,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, @@ -551,6 +430,7 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + senderPolicyTags: baseSenderPolicyTags, }); expect(result.invoiceRoom).toBeDefined(); @@ -587,30 +467,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, @@ -622,12 +482,114 @@ describe('actions/SendInvoice', () => { companyName: undefined, companyWebsite: undefined, policyRecentlyUsedCategories: [], + senderPolicyTags: baseSenderPolicyTags, }); 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 senderPolicyTags', 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 senderPolicyTags = 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 senderPolicyTags read from Onyx + const result = getSendInvoiceInformation({ + transaction: mockTransaction as OnyxEntry, + currentUserAccountID: 123, + policyRecentlyUsedCurrencies: [], + policyRecentlyUsedTags, + senderPolicyTags: senderPolicyTags ?? {}, + }); + + // 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_no_tags'; + 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 senderPolicyTags = 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: [], + senderPolicyTags: senderPolicyTags ?? {}, + }); + + // 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 () => {