diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index b244c1acf5afb..74ad40428c30f 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -11560,7 +11560,12 @@ function payInvoice({ API.write(WRITE_COMMANDS.PAY_INVOICE, params, onyxData); } -function detachReceipt(transactionID: string | undefined, transactionPolicy: OnyxEntry, transactionPolicyCategories?: OnyxEntry) { +function detachReceipt( + transactionID: string | undefined, + transactionPolicy: OnyxEntry, + transactionPolicyTagList: OnyxEntry, + transactionPolicyCategories?: OnyxEntry, +) { if (!transactionID) { return; } @@ -11616,17 +11621,14 @@ function detachReceipt(transactionID: string | undefined, transactionPolicy: Ony ]; if (transactionPolicy && isPaidGroupPolicy(transactionPolicy) && newTransaction) { - // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData (https://github.com/Expensify/App/issues/71491) with useOnyx hook - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policyTagList = getPolicyTagsData(transactionPolicy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( newTransaction, currentTransactionViolations, transactionPolicy, - policyTagList ?? {}, + transactionPolicyTagList ?? {}, transactionPolicyCategories ?? {}, - hasDependentTags(transactionPolicy, policyTagList ?? {}), + hasDependentTags(transactionPolicy, transactionPolicyTagList ?? {}), isInvoiceReportReportUtils(expenseReport), ); optimisticData.push(violationsOnyxData); diff --git a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx index 4b96afbce989a..1759ee10c2545 100644 --- a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx +++ b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx @@ -74,6 +74,8 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre // If we have a merge transaction, we need to use the receipt from the merge transaction const [mergeTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${getNonEmptyStringOnyxID(mergeTransactionID)}`); + const [policyTagList] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`); + const isDraftTransaction = !!action; const draftTransactionID = isDraftTransaction ? transactionID : undefined; @@ -255,9 +257,9 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre * Detach the receipt and close the modal. */ const deleteReceiptAndClose = useCallback(() => { - detachReceipt(transaction?.transactionID, policy, policyCategories); + detachReceipt(transaction?.transactionID, policy, policyTagList, policyCategories); navigation.goBack(); - }, [navigation, transaction?.transactionID, policy, policyCategories]); + }, [navigation, transaction?.transactionID, policy, policyCategories, policyTagList]); /** * Remove odometer image and close the modal. diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index c0c5ac5f222ca..aaf205536f176 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -23,6 +23,7 @@ import { convertBulkTrackedExpensesToIOU, createDistanceRequest, deleteMoneyRequest, + detachReceipt, getDeleteTrackExpenseInformation, getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, @@ -130,6 +131,7 @@ import currencyList from '../unit/currencyList.json'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies'; import createRandomPolicyCategories from '../utils/collections/policyCategory'; +import createRandomPolicyTags from '../utils/collections/policyTags'; import createRandomReportAction from '../utils/collections/reportActions'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; @@ -17599,4 +17601,117 @@ describe('actions/IOU', () => { expect(updatedTransaction?.comment?.odometerEndImage).toBeUndefined(); }); }); + + describe('detachReceipt', () => { + const transactionID = '1'; + const reportID = '2'; + const policyID = '3'; + const tagListName = 'Department'; + + const policy = { + ...createRandomPolicy(1, CONST.POLICY.TYPE.TEAM), + id: policyID, + }; + + const policyTagList = createRandomPolicyTags(tagListName, 3); + + const transaction = { + ...createRandomTransaction(1), + transactionID, + reportID, + receipt: {source: 'receipt-url.jpg'}, + merchant: 'Test Merchant', + }; + + const report = { + ...createRandomReport(1, undefined), + reportID, + policyID, + type: CONST.REPORT.TYPE.EXPENSE, + lastVisibleActionCreated: '2024-01-01 00:00:00', + lastReadTime: '2024-01-01 00:00:00', + }; + + const seedOnyx = async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, policyTagList); + await waitForBatchedUpdates(); + }; + + it('should do nothing when transactionID is undefined', async () => { + const transactionsBefore = await getOnyxValue(ONYXKEYS.COLLECTION.TRANSACTION); + + detachReceipt(undefined, undefined, undefined, undefined); + await waitForBatchedUpdates(); + + const transactionsAfter = await getOnyxValue(ONYXKEYS.COLLECTION.TRANSACTION); + expect(transactionsAfter).toEqual(transactionsBefore); + }); + + it('should optimistically null the receipt and set pending field', async () => { + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + await seedOnyx(); + + try { + detachReceipt(transactionID, undefined, undefined, undefined); + await waitForBatchedUpdates(); + + const onyxData = writeSpy.mock.calls.at(0)?.at(2) as {optimisticData?: Array<{key: string; value: unknown}>}; + const transactionOptimistic = onyxData?.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + expect(transactionOptimistic?.value).toEqual( + expect.objectContaining({ + receipt: null, + pendingFields: {receipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }), + ); + } finally { + writeSpy.mockRestore(); + } + }); + + it('should create an optimistic report action and update report timestamps', async () => { + await seedOnyx(); + + detachReceipt(transactionID, undefined, undefined, undefined); + await waitForBatchedUpdates(); + + // Then a new report action should be created on the report + const reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + const actions = Object.values(reportActions ?? {}); + expect(actions.length).toBeGreaterThan(0); + + // And the report timestamps should be updated + const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + expect(updatedReport?.lastVisibleActionCreated).not.toBe('2024-01-01 00:00:00'); + }); + + it('should call API.write with DETACH_RECEIPT command and correct params', async () => { + // eslint-disable-next-line rulesdir/no-multiple-api-calls + const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + await seedOnyx(); + + try { + detachReceipt(transactionID, undefined, undefined, undefined); + await waitForBatchedUpdates(); + + expect(writeSpy).toHaveBeenCalledWith(WRITE_COMMANDS.DETACH_RECEIPT, expect.objectContaining({transactionID}), expect.anything(), expect.anything()); + } finally { + writeSpy.mockRestore(); + } + }); + + it('should compute violations when policy is paid group', async () => { + await seedOnyx(); + + detachReceipt(transactionID, policy, policyTagList, undefined); + await waitForBatchedUpdates(); + + const violations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + expect(violations).toBeDefined(); + expect(Array.isArray(violations)).toBe(true); + }); + }); });