diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 082376430ed8..1b91e2119bd8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -411,6 +411,7 @@ type BasePolicyParams = { policy?: OnyxEntry; policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; + policyRecentlyUsedCategories?: OnyxEntry; }; type RecentlyUsedParams = { @@ -3172,6 +3173,7 @@ function getSendInvoiceInformation( policyCategories?: OnyxEntry, companyName?: string, companyWebsite?: string, + policyRecentlyUsedCategories?: OnyxEntry, ): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); @@ -3236,7 +3238,7 @@ function getSendInvoiceInformation( }, }); - const optimisticPolicyRecentlyUsedCategories = buildOptimisticPolicyRecentlyUsedCategories(optimisticInvoiceReport.policyID, category); + const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); const optimisticRecentlyUsedCurrencies = buildOptimisticRecentlyUsedCurrencies(currency); @@ -3606,6 +3608,10 @@ function computeDefaultPerDiemExpenseComment(customUnit: TransactionCustomUnit, return subRateComments.join(', '); } +function mergePolicyRecentlyUsedCategories(category: string | undefined, policyRecentlyUsedCategories: OnyxEntry) { + return category ? Array.from(new Set([category, ...(Array.isArray(policyRecentlyUsedCategories) ? policyRecentlyUsedCategories : [])])) : (policyRecentlyUsedCategories ?? []); +} + /** * Gathers all the data needed to submit a per diem expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead @@ -3613,7 +3619,7 @@ function computeDefaultPerDiemExpenseComment(customUnit: TransactionCustomUnit, function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseInformationParams): MoneyRequestInformation { const {parentChatReport, transactionParams, participantParams, policyParams = {}, recentlyUsedParams = {}, moneyRequestReportID = ''} = perDiemExpenseInformation; const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams; - const {policy, policyCategories, policyTagList} = policyParams; + const {policy, policyCategories, policyTagList, policyRecentlyUsedCategories} = policyParams; const {destinations: recentlyUsedDestinations} = recentlyUsedParams; const {comment = '', currency, created, category, tag, customUnit, billable, attendees, reimbursable} = transactionParams; @@ -3701,8 +3707,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI // This is to differentiate between a normal expense and a per diem expense optimisticTransaction.iouRequestType = CONST.IOU.REQUEST_TYPE.PER_DIEM; optimisticTransaction.hasEReceipt = true; - - const optimisticPolicyRecentlyUsedCategories = buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); + const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); const optimisticPolicyRecentlyUsedCurrencies = buildOptimisticRecentlyUsedCurrencies(currency); const optimisticPolicyRecentlyUsedDestinations = customUnit.customUnitRateID ? [...new Set([customUnit.customUnitRateID, ...(recentlyUsedDestinations ?? [])])] : []; @@ -5829,6 +5834,7 @@ function sendInvoice( policyCategories?: OnyxEntry, companyName?: string, companyWebsite?: string, + policyRecentlyUsedCategories?: OnyxEntry, ) { const parsedComment = getParsedComment(transaction?.comment?.comment?.trim() ?? ''); if (transaction?.comment) { @@ -5848,7 +5854,18 @@ function sendInvoice( createdReportActionIDForThread, reportActionID, onyxData, - } = getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories, companyName, companyWebsite); + } = getSendInvoiceInformation( + transaction, + currentUserAccountID, + invoiceChatReport, + receiptFile, + policy, + policyTagList, + policyCategories, + companyName, + companyWebsite, + policyRecentlyUsedCategories, + ); const parameters: SendInvoiceParams = { createdIOUReportActionID, @@ -12466,5 +12483,16 @@ export { reopenReport, retractReport, startDistanceRequest, + getPerDiemExpenseInformation, + getSendInvoiceInformation, +}; +export type { + GPSPoint as GpsPoint, + IOURequestType, + StartSplitBilActionParams, + CreateTrackExpenseParams, + RequestMoneyInformation, + ReplaceReceipt, + RequestMoneyParticipantParams, + PerDiemExpenseTransactionParams, }; -export type {GPSPoint as GpsPoint, IOURequestType, StartSplitBilActionParams, CreateTrackExpenseParams, RequestMoneyInformation, ReplaceReceipt}; diff --git a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx index d81cad8f87e0..acdf67809306 100644 --- a/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx +++ b/src/pages/iou/request/step/IOURequestStepCompanyInfo.tsx @@ -41,9 +41,11 @@ function IOURequestStepCompanyInfo({route, report, transaction}: IOURequestStepC const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const defaultWebsiteExample = useMemo(() => getDefaultCompanyWebsite(session, account), [session, account]); - const policy = usePolicy(getIOURequestPolicyID(transaction, report)); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getIOURequestPolicyID(transaction, report)}`, {canBeMissing: true}); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getIOURequestPolicyID(transaction, report)}`, {canBeMissing: true}); + const policyID = getIOURequestPolicyID(transaction, report); + const policy = usePolicy(policyID); + const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const formattedAmount = convertToDisplayString(Math.abs(transaction?.amount ?? 0), transaction?.currency); @@ -72,7 +74,18 @@ function IOURequestStepCompanyInfo({route, report, transaction}: IOURequestStepC const submit = (values: FormOnyxValues) => { const companyWebsite = Str.sanitizeURL(values.companyWebsite, CONST.COMPANY_WEBSITE_DEFAULT_SCHEME); - sendInvoice(currentUserPersonalDetails.accountID, transaction, report, undefined, policy, policyTags, policyCategories, values.companyName, companyWebsite); + sendInvoice( + currentUserPersonalDetails.accountID, + transaction, + report, + undefined, + policy, + policyTags, + policyCategories, + values.companyName, + companyWebsite, + policyRecentlyUsedCategories, + ); }; return ( diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 6812d310c097..4f5780c806c9 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -66,6 +66,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import type {RecentlyUsedCategories} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type Transaction from '@src/types/onyx/Transaction'; @@ -133,6 +134,7 @@ function IOURequestStepConfirmation({ const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION, {canBeMissing: true}); const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); const [recentlyUsedDestinations] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS}${realPolicyID}`, {canBeMissing: true}); + const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${realPolicyID}`, {canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); /* @@ -526,7 +528,7 @@ function IOURequestStepConfirmation({ ); const submitPerDiemExpense = useCallback( - (selectedParticipants: Participant[], trimmedComment: string) => { + (selectedParticipants: Participant[], trimmedComment: string, policyRecentlyUsedCategoriesParam?: RecentlyUsedCategories) => { if (!transaction) { return; } @@ -546,6 +548,7 @@ function IOURequestStepConfirmation({ policy, policyTagList: policyTags, policyCategories, + policyRecentlyUsedCategories: policyRecentlyUsedCategoriesParam, }, recentlyUsedParams: { destinations: recentlyUsedDestinations, @@ -797,7 +800,18 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.INVOICE) { - sendInvoice(currentUserPersonalDetails.accountID, transaction, report, currentTransactionReceiptFile, policy, policyTags, policyCategories); + sendInvoice( + currentUserPersonalDetails.accountID, + transaction, + report, + currentTransactionReceiptFile, + policy, + policyTags, + policyCategories, + undefined, + undefined, + policyRecentlyUsedCategories, + ); return; } @@ -842,7 +856,7 @@ function IOURequestStepConfirmation({ } if (isPerDiemRequest) { - submitPerDiemExpense(selectedParticipants, trimmedComment); + submitPerDiemExpense(selectedParticipants, trimmedComment, policyRecentlyUsedCategories); return; } @@ -909,9 +923,10 @@ function IOURequestStepConfirmation({ policy, policyTags, policyCategories, + policyRecentlyUsedCategories, trackExpense, - submitPerDiemExpense, userLocation, + submitPerDiemExpense, ], ); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 0dd4d3e813e0..5160013905e5 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import {renderHook} from '@testing-library/react-native'; import {format} from 'date-fns'; import {deepEqual} from 'fast-equals'; @@ -5,6 +8,7 @@ import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx' import Onyx from 'react-native-onyx'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; +import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { addSplitExpenseField, calculateDiffAmount, @@ -17,6 +21,8 @@ import { createDistanceRequest, deleteMoneyRequest, getIOUReportActionToApproveOrPay, + getPerDiemExpenseInformation, + getSendInvoiceInformation, initMoneyRequest, initSplitExpense, mergeDuplicates, @@ -72,7 +78,7 @@ import * as API from '@src/libs/API'; import DateUtils from '@src/libs/DateUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {OriginalMessageIOU, PersonalDetailsList, Policy, Report, ReportNameValuePairs, SearchResults} from '@src/types/onyx'; +import type {OriginalMessageIOU, PersonalDetailsList, Policy, PolicyTagLists, Report, ReportNameValuePairs, SearchResults} from '@src/types/onyx'; import type {Accountant} from '@src/types/onyx/IOU'; import type {Participant, ReportCollectionDataSet} from '@src/types/onyx/Report'; import type {ReportActions, ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; @@ -86,6 +92,7 @@ import * as InvoiceData from '../data/Invoice'; import type {InvoiceTestData} from '../data/Invoice'; import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies'; import createRandomPolicyCategories from '../utils/collections/policyCategory'; +import createRandomPolicyTags from '../utils/collections/policyTags'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; import getOnyxValue from '../utils/getOnyxValue'; @@ -7520,4 +7527,565 @@ describe('actions/IOU', () => { expect(writeSpy).toHaveBeenCalledWith(WRITE_COMMANDS.RESOLVE_DUPLICATES, expect.objectContaining({}), expect.objectContaining({})); }); }); + + describe('getPerDiemExpenseInformation', () => { + it('should return correct per diem expense information with new chat report', () => { + // Given: Mock data for per diem expense + const mockCustomUnit = { + customUnitID: 'per_diem_123', + customUnitRateID: 'rate_456', + name: CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL, + attributes: { + dates: { + start: '2024-01-15', + end: '2024-01-15', + }, + }, + subRates: [ + { + id: 'breakfast_1', + name: 'Breakfast', + rate: 25, + quantity: 1, + }, + { + id: 'lunch_1', + name: 'Lunch', + rate: 35, + quantity: 1, + }, + ], + quantity: 1, + }; + + const mockParticipant = { + accountID: 123, + login: 'test@example.com', + displayName: 'Test User', + isPolicyExpenseChat: false, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.MEMBER, + }; + + const mockTransactionParams = { + currency: 'USD', + created: '2024-01-15', + category: 'Travel', + tag: 'Project A', + customUnit: mockCustomUnit, + billable: true, + attendees: [], + reimbursable: true, + comment: 'Business trip per diem', + }; + + const mockParticipantParams = { + payeeAccountID: 456, + payeeEmail: 'payee@example.com', + participant: mockParticipant, + }; + + const mockPolicyParams = { + policy: createRandomPolicy(1), + policyCategories: createRandomPolicyCategories(3), + policyTagList: createRandomPolicyTags('tagList', 2), + }; + + // When: Call getPerDiemExpenseInformation + const result = getPerDiemExpenseInformation({ + parentChatReport: {} as OnyxEntry, + transactionParams: mockTransactionParams, + participantParams: mockParticipantParams, + policyParams: mockPolicyParams, + recentlyUsedParams: {}, + moneyRequestReportID: '1', + }); + + // Then: Verify the result structure and key values + expect(result).toMatchObject({ + payerAccountID: 123, + payerEmail: 'test@example.com', + billable: true, + reimbursable: true, + }); + + // Verify chat report was created + expect(result.chatReport).toBeDefined(); + expect(result.chatReport.reportID).toBeDefined(); + + // Verify IOU report was created + expect(result.iouReport).toBeDefined(); + expect(result.iouReport.reportID).toBeDefined(); + expect(result.iouReport.type).toBe(CONST.REPORT.TYPE.IOU); + + // Verify transaction was created with correct per diem data + expect(result.transaction).toBeDefined(); + expect(result.transaction.transactionID).toBeDefined(); + expect(result.transaction.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.PER_DIEM); + expect(result.transaction.hasEReceipt).toBe(true); + expect(result.transaction.currency).toBe('USD'); + expect(result.transaction.category).toBe('Travel'); + expect(result.transaction.tag).toBe('Project A'); + expect(result.transaction.comment?.comment).toBe('Business trip per diem'); + + // Verify IOU action was created + expect(result.iouAction).toBeDefined(); + expect(result.iouAction.reportActionID).toBeDefined(); + expect(result.iouAction.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.IOU); + + // Verify report preview action + expect(result.reportPreviewAction).toBeDefined(); + expect(result.reportPreviewAction.reportActionID).toBeDefined(); + + // Verify Onyx data structure + expect(result.onyxData).toBeDefined(); + expect(result.onyxData.optimisticData).toBeDefined(); + expect(result.onyxData.successData).toBeDefined(); + expect(result.onyxData.failureData).toBeDefined(); + + // Verify created action IDs for new reports + expect(result.createdChatReportActionID).toBeDefined(); + expect(result.createdIOUReportActionID).toBeDefined(); + }); + + it('should return correct per diem expense information with existing chat report', () => { + // Given: Existing chat report + const existingChatReport = { + reportID: 'chat_123', + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + participants: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '123': { + accountID: 123, + role: CONST.REPORT.ROLE.MEMBER, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '456': { + accountID: 456, + role: CONST.REPORT.ROLE.MEMBER, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + }, + iouReportID: 'iou_456', + type: CONST.REPORT.TYPE.CHAT, + }; + + const mockCustomUnit = { + customUnitID: 'per_diem_789', + customUnitRateID: 'rate_101', + name: CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL, + attributes: { + dates: { + start: '2024-01-20', + end: '2024-01-20', + }, + }, + subRates: [ + { + id: 'dinner_1', + name: 'Dinner', + rate: 45, + quantity: 1, + }, + ], + quantity: 2, + }; + + const mockParticipant = { + accountID: 123, + login: 'existing@example.com', + displayName: 'Existing User', + isPolicyExpenseChat: false, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.MEMBER, + }; + + const mockTransactionParams = { + comment: 'Conference per diem', + currency: 'USD', + created: '2024-01-20', + category: 'Meals', + tag: 'Conference', + customUnit: mockCustomUnit, + billable: false, + attendees: [], + reimbursable: true, + }; + + const mockParticipantParams = { + payeeAccountID: 456, + payeeEmail: 'payee@example.com', + participant: mockParticipant, + }; + + // When: Call getPerDiemExpenseInformation with existing chat report + const result = getPerDiemExpenseInformation({ + parentChatReport: existingChatReport as OnyxEntry, + transactionParams: mockTransactionParams as PerDiemExpenseTransactionParams, + participantParams: mockParticipantParams as RequestMoneyParticipantParams, + recentlyUsedParams: {}, + }); + + // Then: Verify the result uses existing chat report + expect(result.chatReport.reportID).toBe('chat_123'); + expect(result.chatReport.chatType).toBe(CONST.REPORT.CHAT_TYPE.GROUP); + + // Verify transaction has correct per diem data + expect(result.transaction.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.PER_DIEM); + expect(result.transaction.hasEReceipt).toBe(true); + expect(result.transaction.currency).toBe('USD'); + expect(result.transaction.category).toBe('Meals'); + expect(result.transaction.tag).toBe('Conference'); + expect(result.transaction.comment?.comment).toBe('Conference per diem'); + + // Verify no new chat report action ID since using existing + expect(result.createdChatReportActionID).toBeUndefined(); + }); + + it('should handle policy expense chat correctly', () => { + // Given: Policy expense chat participant + const mockParticipant = { + accountID: 123, + login: 'policy@example.com', + displayName: 'Policy User', + isPolicyExpenseChat: true, + reportID: 'policy_chat_123', + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + role: CONST.REPORT.ROLE.MEMBER, + }; + + const mockCustomUnit = { + customUnitID: 'per_diem_policy', + customUnitRateID: 'rate_policy', + name: CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL, + attributes: { + dates: { + start: '2024-01-25', + end: '2024-01-25', + }, + }, + subRates: [ + { + id: 'lodging_1', + name: 'Lodging', + rate: 150, + quantity: 1, + }, + ], + quantity: 1, + }; + + const mockTransactionParams = { + comment: 'Policy per diem', + currency: 'USD', + created: '2024-01-25', + category: 'Lodging', + tag: 'Policy', + customUnit: mockCustomUnit, + billable: true, + attendees: [], + reimbursable: true, + }; + + const mockParticipantParams = { + payeeAccountID: 456, + payeeEmail: 'payee@example.com', + participant: mockParticipant, + }; + + const mockPolicyParams = { + policy: createRandomPolicy(2), + }; + + // When: Call getPerDiemExpenseInformation for policy expense chat + const result = getPerDiemExpenseInformation({ + parentChatReport: {} as OnyxEntry, + transactionParams: mockTransactionParams as PerDiemExpenseTransactionParams, + participantParams: mockParticipantParams as RequestMoneyParticipantParams, + policyParams: mockPolicyParams, + recentlyUsedParams: {}, + }); + + // Then: Verify policy expense chat handling + expect(result.payerAccountID).toBe(123); + expect(result.payerEmail).toBe('policy@example.com'); + expect(result.transaction.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.PER_DIEM); + expect(result.transaction.hasEReceipt).toBe(true); + expect(result.billable).toBe(true); + expect(result.reimbursable).toBe(true); + }); + }); + + describe('getSendInvoiceInformation', () => { + 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); + + const mockPolicyCategories = { + Services: { + name: 'Services', + enabled: true, + }, + }; + + const mockPolicyTagList = { + tagList: { + name: 'tagList', + orderWeight: 0, + required: false, + tags: { + projectB: { + name: 'Project B', + enabled: true, + }, + }, + }, + }; + + // When: Call getSendInvoiceInformation + const result = getSendInvoiceInformation( + mockTransaction as OnyxEntry, + currentUserAccountID, + undefined, // invoiceChatReport + undefined, // receipt + mockPolicy, + mockPolicyTagList as OnyxEntry, + mockPolicyCategories, + 'Test Company Inc.', + 'https://testcompany.com', + ['Services', 'Consulting'], + ); + + // Then: Verify the result structure and key values + expect(result).toMatchObject({ + senderWorkspaceID: 'workspace_123', + invoiceReportID: expect.any(String), + transactionID: expect.any(String), + transactionThreadReportID: expect.any(String), + createdIOUReportActionID: expect.any(String), + reportActionID: expect.any(String), + createdChatReportActionID: expect.any(String), + reportPreviewReportActionID: expect.any(String), + }); + + // Verify receiver information + expect(result.receiver).toBeDefined(); + expect(result.receiver.accountID).toBe(123); + + // Verify invoice room (chat report) + expect(result.invoiceRoom).toBeDefined(); + expect(result.invoiceRoom.reportID).toBeDefined(); + expect(result.invoiceRoom.chatType).toBe(CONST.REPORT.CHAT_TYPE.INVOICE); + + // Verify Onyx data structure + expect(result.onyxData).toBeDefined(); + expect(result.onyxData.optimisticData).toBeDefined(); + expect(result.onyxData.successData).toBeDefined(); + expect(result.onyxData.failureData).toBeDefined(); + }); + + it('should return correct invoice information with existing chat report', () => { + // Given: Existing invoice chat report + const existingInvoiceChatReport = { + reportID: 'invoice_chat_123', + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + type: CONST.REPORT.TYPE.CHAT, + participants: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '123': { + accountID: 123, + role: CONST.REPORT.ROLE.MEMBER, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '456': { + accountID: 456, + role: CONST.REPORT.ROLE.MEMBER, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, + }, + }, + invoiceReceiver: { + type: 'individual', + accountID: 456, + displayName: 'Client Company', + login: 'client@example.com', + }, + }; + + 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( + mockTransaction, + currentUserAccountID, + existingInvoiceChatReport as OnyxEntry, + undefined, // receipt + undefined, // policy + undefined, // policyTagList + undefined, // policyCategories + 'Client Company Ltd.', + 'https://clientcompany.com', + ); + + // Then: Verify the result uses existing chat report + expect(result.invoiceRoom.reportID).toBe('invoice_chat_123'); + expect(result.invoiceRoom.chatType).toBe(CONST.REPORT.CHAT_TYPE.INVOICE); + + // Verify transaction data + expect(result.transactionID).toBeDefined(); + expect(result.senderWorkspaceID).toBe('workspace_456'); + }); + + 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', + name: 'receipt.pdf', + state: CONST.IOU.RECEIPT_STATE.SCAN_READY, + }; + + const currentUserAccountID = 123; + + // When: Call getSendInvoiceInformation with receipt + const result = getSendInvoiceInformation( + mockTransaction, + currentUserAccountID, + undefined, // invoiceChatReport + mockReceipt, + undefined, // policy + undefined, // policyTagList + undefined, // policyCategories + ); + + // Then: Verify receipt handling + expect(result.transactionID).toBeDefined(); + expect(result.invoiceRoom).toBeDefined(); + expect(result.invoiceRoom.chatType).toBe(CONST.REPORT.CHAT_TYPE.INVOICE); + + // Verify Onyx data includes receipt information + expect(result.onyxData).toBeDefined(); + expect(result.onyxData.optimisticData).toBeDefined(); + }); + + it('should handle missing transaction data gracefully', () => { + // Given: Minimal transaction data + const mockTransaction = { + transactionID: 'transaction_minimal', + reportID: 'report_minimal', + amount: 100, + currency: 'USD', + created: '2024-01-30', + merchant: 'Minimal Company', + participants: [ + { + accountID: 123, + isSender: true, + }, + { + accountID: 456, + isSender: false, + }, + ], + }; + + const currentUserAccountID = 123; + + // When: Call getSendInvoiceInformation with minimal data + const result = getSendInvoiceInformation(mockTransaction, currentUserAccountID); + + // Then: Verify function handles missing data gracefully + expect(result).toBeDefined(); + expect(result.transactionID).toBeDefined(); + expect(result.invoiceRoom).toBeDefined(); + expect(result.invoiceRoom.chatType).toBe(CONST.REPORT.CHAT_TYPE.INVOICE); + expect(result.receiver).toBeDefined(); + expect(result.onyxData).toBeDefined(); + }); + }); });