diff --git a/package.json b/package.json index c1c5374338f5..e7b516e14789 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=301 --cache --cache-location=node_modules/.cache/eslint", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=299 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 0be7a32f096f..a930609520f5 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,9 +1,14 @@ import {deepEqual} from 'fast-equals'; import React, {useMemo, useRef} from 'react'; import useCurrentReportID from '@hooks/useCurrentReportID'; +import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; +import useOnyx from '@hooks/useOnyx'; +import {getSortedReportActions, shouldReportActionBeVisibleAsLastAction} from '@libs/ReportActionsUtils'; +import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -39,6 +44,25 @@ function OptionRowLHNData({ const isReportFocused = isOptionFocused && currentReportIDValue?.currentReportID === reportID; const optionItemRef = useRef(undefined); + + const [reportActionsData] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canBeMissing: true}); + + const lastAction = useMemo(() => { + if (!reportActionsData || !fullReport) { + return undefined; + } + + const canUserPerformWriteAction = canUserPerformWriteActionUtil(fullReport); + const actionsArray = getSortedReportActions(Object.values(reportActionsData)); + + const reportActionsForDisplay = actionsArray.filter( + (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + + return reportActionsForDisplay.at(-1); + }, [reportActionsData, fullReport]); + + const card = useGetExpensifyCardFromReportAction({reportAction: lastAction, policyID: fullReport?.policyID}); const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! const item = SidebarUtils.getOptionData({ @@ -51,6 +75,7 @@ function OptionRowLHNData({ parentReportAction, lastMessageTextFromReport, invoiceReceiverPolicy, + card, localeCompare, }); // eslint-disable-next-line react-compiler/react-compiler @@ -84,6 +109,7 @@ function OptionRowLHNData({ invoiceReceiverPolicy, lastMessageTextFromReport, reportAttributes, + card, localeCompare, ]); diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx index 3a68dc4900d8..1c76d2260fd2 100644 --- a/src/components/ReportActionItem/IssueCardMessage.tsx +++ b/src/components/ReportActionItem/IssueCardMessage.tsx @@ -3,10 +3,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import {useSession} from '@components/OnyxListItemProvider'; import RenderHTML from '@components/RenderHTML'; +import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getExpensifyCardFromReportAction} from '@libs/CardMessageUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getCardIssuedMessage, getOriginalMessage, shouldShowAddMissingDetails} from '@libs/ReportActionsUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -25,7 +25,7 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) { const styles = useThemeStyles(); const session = useSession(); const assigneeAccountID = (getOriginalMessage(action) as IssueNewCardOriginalMessage)?.assigneeAccountID; - const expensifyCard = getExpensifyCardFromReportAction({reportAction: action, policyID}); + const expensifyCard = useGetExpensifyCardFromReportAction({reportAction: action, policyID}); const isAssigneeCurrentUser = !isEmptyObject(session) && session.accountID === assigneeAccountID; const shouldShowAddMissingDetailsButton = isAssigneeCurrentUser && shouldShowAddMissingDetails(action?.actionName, expensifyCard); const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); diff --git a/src/hooks/useGetExpensifyCardFromReportAction.ts b/src/hooks/useGetExpensifyCardFromReportAction.ts new file mode 100644 index 000000000000..653a772044c9 --- /dev/null +++ b/src/hooks/useGetExpensifyCardFromReportAction.ts @@ -0,0 +1,25 @@ +import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isCardIssuedAction} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Card, ReportAction} from '@src/types/onyx'; +import useOnyx from './useOnyx'; + +function useGetExpensifyCardFromReportAction({reportAction, policyID}: {reportAction?: ReportAction; policyID?: string}): Card | undefined { + const [allUserCards] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); + const [allExpensifyCards] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, { + selector: (val) => { + const workspaceAccountID = getWorkspaceAccountID(policyID); + return val?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; + }, + canBeMissing: true, + }); + const cardIssuedActionOriginalMessage = isCardIssuedAction(reportAction) ? getOriginalMessage(reportAction) : undefined; + + const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID; + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line deprecation/deprecation + return isPolicyAdmin(getPolicy(policyID)) ? allExpensifyCards?.[cardID] : allUserCards?.[cardID]; +} + +export default useGetExpensifyCardFromReportAction; diff --git a/src/libs/CardMessageUtils.ts b/src/libs/CardMessageUtils.ts deleted file mode 100644 index d1931062ba00..000000000000 --- a/src/libs/CardMessageUtils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import CONST from '@src/CONST'; -import type {OnyxValues} from '@src/ONYXKEYS'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, ReportAction, WorkspaceCardsList} from '@src/types/onyx'; -import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from './PolicyUtils'; -import {getOriginalMessage, isCardIssuedAction} from './ReportActionsUtils'; - -let allUserCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {}; -Onyx.connect({ - key: ONYXKEYS.CARD_LIST, - callback: (val) => { - if (!val || Object.keys(val).length === 0) { - return; - } - - allUserCards = val; - }, -}); - -let allWorkspaceCards: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, - waitForCollectionCallback: true, - callback: (value) => { - allWorkspaceCards = value; - }, -}); - -function getExpensifyCardFromReportAction({reportAction, policyID}: {reportAction?: ReportAction; policyID?: string}): Card | undefined { - const cardIssuedActionOriginalMessage = isCardIssuedAction(reportAction) ? getOriginalMessage(reportAction) : undefined; - - const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID; - const workspaceAccountID = getWorkspaceAccountID(policyID); - const allExpensifyCards = allWorkspaceCards?.[`cards_${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`] ?? {}; - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation - return isPolicyAdmin(getPolicy(policyID)) ? allExpensifyCards?.[cardID] : allUserCards[cardID]; -} - -export { - // eslint-disable-next-line import/prefer-default-export - getExpensifyCardFromReportAction, -}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index e0a3acd068f2..430eb50faf72 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -6,7 +6,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Card, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -14,7 +14,6 @@ import type Policy from '@src/types/onyx/Policy'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import {getExpensifyCardFromReportAction} from './CardMessageUtils'; import {extractCollectionItemID} from './CollectionUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; import {translateLocal} from './Localize'; @@ -510,6 +509,7 @@ function getOptionData({ parentReportAction, lastMessageTextFromReport: lastMessageTextFromReportProp, invoiceReceiverPolicy, + card, localeCompare, }: { report: OnyxEntry; @@ -521,6 +521,7 @@ function getOptionData({ lastMessageTextFromReport?: string; invoiceReceiverPolicy?: OnyxEntry; reportAttributes: OnyxEntry; + card: Card | undefined; localeCompare: LocaleContextProps['localeCompare']; }): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for @@ -746,7 +747,6 @@ function getOptionData({ } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_POLICY) { result.alternateText = getPolicyChangeLogEmployeeLeftMessage(lastAction, true); } else if (isCardIssuedAction(lastAction)) { - const card = getExpensifyCardFromReportAction({reportAction: lastAction, policyID: report.policyID}); result.alternateText = getCardIssuedMessage({reportAction: lastAction, expensifyCard: card}); } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { result.alternateText = formatReportLastMessageText(Parser.htmlToText(`${lastActorDisplayName}: ${lastMessageText}`)); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index a3c49fddf1e9..408c42874c3b 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -11,6 +11,7 @@ import ContextMenuItem from '@components/ContextMenuItem'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useEnvironment from '@hooks/useEnvironment'; +import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -20,7 +21,6 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; import useStyleUtils from '@hooks/useStyleUtils'; -import {getExpensifyCardFromReportAction} from '@libs/CardMessageUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getLinkedTransactionID, getOneTransactionThreadReportID, getOriginalMessage, getReportAction} from '@libs/ReportActionsUtils'; import { @@ -321,7 +321,7 @@ function BaseReportActionContextMenu({ }; // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const card = getExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); + const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); return ( (isVisible || shouldKeepOpen || !isMini) && ( diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index a88866c95fce..a6633fc1c3a1 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -84,6 +84,7 @@ describe('SidebarUtils', () => { policy, parentReportAction, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }), ); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 26275a34b5d8..94aae78241df 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -328,6 +328,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); const optionDataUnpinned = SidebarUtils.getOptionData({ @@ -338,6 +339,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -832,6 +834,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -890,6 +893,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -931,6 +935,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -965,6 +970,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -996,6 +1002,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -1116,6 +1123,7 @@ describe('SidebarUtils', () => { policy, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(iouReport); @@ -1158,6 +1166,7 @@ describe('SidebarUtils', () => { parentReportAction: undefined, lastMessageTextFromReport: 'test message', oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -1223,6 +1232,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -1265,6 +1275,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -1329,6 +1340,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); @@ -1440,6 +1452,7 @@ describe('SidebarUtils', () => { policy: undefined, parentReportAction: undefined, oneTransactionThreadReport: undefined, + card: undefined, localeCompare, }); diff --git a/tests/unit/useGetExpensifyCardFromReportActionTest.ts b/tests/unit/useGetExpensifyCardFromReportActionTest.ts new file mode 100644 index 000000000000..1aa4496b939a --- /dev/null +++ b/tests/unit/useGetExpensifyCardFromReportActionTest.ts @@ -0,0 +1,252 @@ +import {renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import {getPolicy, getWorkspaceAccountID, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isCardIssuedAction} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import useGetExpensifyCardFromReportAction from '@src/hooks/useGetExpensifyCardFromReportAction'; +import type {OnyxKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Card, ReportAction} from '@src/types/onyx'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +// Mock the dependencies +jest.mock('@libs/PolicyUtils'); +jest.mock('@libs/ReportActionsUtils'); + +// This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 +// eslint-disable-next-line deprecation/deprecation +const mockGetPolicy = getPolicy as jest.MockedFunction; +const mockGetWorkspaceAccountID = getWorkspaceAccountID as jest.MockedFunction; +const mockIsPolicyAdmin = isPolicyAdmin as jest.MockedFunction; +const mockGetOriginalMessage = getOriginalMessage as jest.MockedFunction; +const mockIsCardIssuedAction = isCardIssuedAction as jest.MockedFunction; + +describe('useGetExpensifyCardFromReportAction', () => { + const mockCard: Card = { + cardID: 123, + cardName: 'Test Card', + cardNumber: '1234567890123456', + domainName: 'test.com', + lastUpdated: '2023-01-01T00:00:00.000Z', + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE, + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + bank: 'Test Bank', + }; + + const createMockReportAction = (cardID = 123): ReportAction => ({ + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD, + originalMessage: {cardID, assigneeAccountID: 1}, + created: '2023-01-01T00:00:00.000Z', + actorAccountID: 1, + person: [], + shouldShow: true, + isAttachmentOnly: false, + isFirstItem: false, + pendingAction: null, + errors: undefined, + message: [], + reportID: '1', + }); + + const mockPolicy = { + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.USER, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }; + + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(async () => { + await Onyx.clear(); + jest.clearAllMocks(); + + mockGetPolicy.mockReturnValue(mockPolicy); + mockGetWorkspaceAccountID.mockReturnValue(123); + mockIsPolicyAdmin.mockReturnValue(false); + mockGetOriginalMessage.mockReturnValue({cardID: 123, assigneeAccountID: 1}); + mockIsCardIssuedAction.mockReturnValue(true); + }); + + describe('when reportAction is not a card issued action', () => { + it('returns undefined', async () => { + mockIsCardIssuedAction.mockReturnValue(false); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('when reportAction is a card issued action', () => { + beforeEach(() => { + mockIsCardIssuedAction.mockReturnValue(true); + mockGetOriginalMessage.mockReturnValue({cardID: 123, assigneeAccountID: 1}); + }); + + describe('when user is not a policy admin', () => { + beforeEach(() => { + mockIsPolicyAdmin.mockReturnValue(false); + }); + + it('returns card from allUserCards when card exists', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + + it('returns undefined when card does not exist in allUserCards', async () => { + Onyx.set(ONYXKEYS.CARD_LIST, {}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('when user is a policy admin', () => { + beforeEach(() => { + mockIsPolicyAdmin.mockReturnValue(true); + mockGetWorkspaceAccountID.mockReturnValue(123); + // Override the default policy for admin tests + mockGetPolicy.mockReturnValue({ + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }); + }); + + it('returns card from allExpensifyCards when card exists', async () => { + const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(workspaceCardsKey, {123: mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + + it('returns undefined when card does not exist in allExpensifyCards', async () => { + const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {[workspaceCardsKey]: {}}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + }); + }); + }); + + describe('reactivity to Onyx changes', () => { + it('updates when allUserCards changes', async () => { + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + + it('updates when allExpensifyCards changes for policy admin', async () => { + mockIsPolicyAdmin.mockReturnValue(true); + mockGetPolicy.mockReturnValue({ + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }); + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toBeUndefined(); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const workspaceCardsKey = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}123_${CONST.EXPENSIFY_CARD.BANK}` as OnyxKey; + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(workspaceCardsKey, {123: mockCard}); + await waitForBatchedUpdatesWithAct(); + + expect(result.current).toEqual(mockCard); + }); + }); + + describe('workspace account ID generation', () => { + it('calls getWorkspaceAccountID with correct policyID', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'test-policy-123'})); + await waitForBatchedUpdatesWithAct(); + + expect(mockGetWorkspaceAccountID).toHaveBeenCalledWith('test-policy-123'); + expect(result.current).toEqual(mockCard); + }); + }); + + describe('policy admin check', () => { + it('calls isPolicyAdmin with correct policy', async () => { + const testPolicy = { + id: 'policy123', + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + isAttendeeTrackingEnabled: false, + owner: '1', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: false, + workspaceAccountID: 123, + }; + mockGetPolicy.mockReturnValue(testPolicy); + + // eslint-disable-next-line @typescript-eslint/naming-convention + Onyx.set(ONYXKEYS.CARD_LIST, {'123': mockCard}); + await waitForBatchedUpdatesWithAct(); + + const {result} = renderHook(() => useGetExpensifyCardFromReportAction({reportAction: createMockReportAction(), policyID: 'policy123'})); + await waitForBatchedUpdatesWithAct(); + + expect(mockGetPolicy).toHaveBeenCalledWith('policy123'); + expect(mockIsPolicyAdmin).toHaveBeenCalledWith(testPolicy); + expect(result.current).toEqual(mockCard); + }); + }); +});