From ec1f5e6399d87c68dc27072a3519ebda0afe9996 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Sat, 9 May 2026 11:52:38 +0700 Subject: [PATCH] refactor payMoneyRequest to use conciergeReportID from useOnyx --- .../MoneyReportHeaderSecondaryActions.tsx | 2 + .../MoneyReportHeaderSelectionDropdown.tsx | 2 + .../PayPrimaryAction.tsx | 2 + .../PayActionButton.tsx | 2 + src/hooks/useHoldMenuSubmit.ts | 2 + src/hooks/useSelectionModeReportActions.ts | 2 + src/libs/actions/IOU/Hold.ts | 4 + src/libs/actions/IOU/PayMoneyRequest.ts | 8 +- tests/actions/IOUTest/PayMoneyRequestTest.ts | 104 ++++++++++++++++++ 9 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index cc7f922ecc54..6e912d9beccc 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -104,6 +104,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`, {}, ); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {login: currentUserLogin, accountID, email} = currentUserPersonalDetails; @@ -195,6 +196,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo amountOwed, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + conciergeReportID, onPaid: () => { startAnimation(); }, diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx index f68593fcaeab..08f84422b03e 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx @@ -118,6 +118,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const activePolicy = usePolicy(activePolicyID); const chatReportPolicy = usePolicy(chatReport?.policyID); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); @@ -293,6 +294,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn amountOwed, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + conciergeReportID, onPaid: () => { startAnimation(); }, diff --git a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx index e2b70728236c..4a37a81292a8 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx @@ -58,6 +58,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${moneyRequestReport?.reportID}`); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const activePolicy = usePolicy(activePolicyID); const chatReportPolicy = usePolicy(chatReport?.policyID); @@ -164,6 +165,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { amountOwed, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + conciergeReportID, onPaid: startAnimation, }); if (currentSearchQueryJSON && !isOffline) { diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx index 2b584c0cfcd6..4dc75a1f1cd3 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx @@ -89,6 +89,7 @@ function PayActionButton({ const [betas] = useOnyx(ONYXKEYS.BETAS); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const reportTransactionsCollection = useReportTransactionsCollection(iouReportID); const transactions = Object.values(reportTransactionsCollection ?? {}).filter( @@ -205,6 +206,7 @@ function PayActionButton({ userBillingGracePeriodEnds, amountOwed, ownerBillingGracePeriodEnd, + conciergeReportID, onPaid: startAnimation, }); } diff --git a/src/hooks/useHoldMenuSubmit.ts b/src/hooks/useHoldMenuSubmit.ts index f5082038cfdd..4e17be6c9ed0 100644 --- a/src/hooks/useHoldMenuSubmit.ts +++ b/src/hooks/useHoldMenuSubmit.ts @@ -40,6 +40,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [moneyRequestReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const {isBetaEnabled} = usePermissions(); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); @@ -96,6 +97,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment amountOwed, ownerBillingGracePeriodEnd, methodID, + conciergeReportID, onPaid: animationCallback, }); } diff --git a/src/hooks/useSelectionModeReportActions.ts b/src/hooks/useSelectionModeReportActions.ts index 9b5817b4f3a9..166d06d44fb5 100644 --- a/src/hooks/useSelectionModeReportActions.ts +++ b/src/hooks/useSelectionModeReportActions.ts @@ -114,6 +114,7 @@ function useSelectionModeReportActions({ const {isOffline} = useNetwork(); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const activePolicy = usePolicy(activePolicyID); const chatReportPolicy = usePolicy(chatReport?.policyID); const [invoiceReceiverPolicy] = useOnyx( @@ -396,6 +397,7 @@ function useSelectionModeReportActions({ amountOwed, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + conciergeReportID, }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/libs/actions/IOU/Hold.ts b/src/libs/actions/IOU/Hold.ts index 475f6affa825..8fcc3248472f 100644 --- a/src/libs/actions/IOU/Hold.ts +++ b/src/libs/actions/IOU/Hold.ts @@ -685,6 +685,7 @@ function getReportFromHoldRequestsOnyxData({ createdTimestamp, betas, isApprovalFlow = false, + conciergeReportID, }: { chatReport: OnyxTypes.Report; iouReport: OnyxEntry; @@ -693,6 +694,8 @@ function getReportFromHoldRequestsOnyxData({ createdTimestamp?: string; betas: OnyxEntry; isApprovalFlow?: boolean; + // TODO: This will be required eventually. Ref: https://github.com/Expensify/App/issues/66411 + conciergeReportID?: string; }): { optimisticHoldReportID: string; optimisticHoldActionID: string; @@ -752,6 +755,7 @@ function getReportFromHoldRequestsOnyxData({ firstHoldTransaction, optimisticExpenseReport.reportID, newParentReportActionID, + conciergeReportID, ); let optimisticCreatedReportForUnapprovedAction: OnyxTypes.ReportAction | null = null; diff --git a/src/libs/actions/IOU/PayMoneyRequest.ts b/src/libs/actions/IOU/PayMoneyRequest.ts index cd52d58753ff..5a90f18928ed 100644 --- a/src/libs/actions/IOU/PayMoneyRequest.ts +++ b/src/libs/actions/IOU/PayMoneyRequest.ts @@ -93,6 +93,7 @@ type PayMoneyRequestFunctionParams = { amountOwed: OnyxEntry; ownerBillingGracePeriodEnd?: OnyxEntry; methodID?: number; + conciergeReportID: string | undefined; onPaid?: () => void; }; @@ -116,6 +117,7 @@ function getPayMoneyRequestParams({ betas, isSelfTourViewed, defaultWorkspaceName, + conciergeReportID, }: { initialChatReport: OnyxTypes.Report; iouReport: OnyxEntry; @@ -136,6 +138,8 @@ function getPayMoneyRequestParams({ betas: OnyxEntry; isSelfTourViewed: boolean | undefined; defaultWorkspaceName?: string; + // TODO: This will be required eventually. Ref: https://github.com/Expensify/App/issues/66411 + conciergeReportID?: string; }): PayMoneyRequestData { const allTransactionViolations = getAllTransactionViolations(); @@ -441,7 +445,7 @@ function getPayMoneyRequestParams({ let optimisticHoldActionID; let optimisticHoldReportExpenseActionIDs; if (!full) { - const holdReportOnyxData = getReportFromHoldRequestsOnyxData({chatReport, iouReport, recipient, policy: reportPolicy, betas}); + const holdReportOnyxData = getReportFromHoldRequestsOnyxData({chatReport, iouReport, recipient, policy: reportPolicy, betas, conciergeReportID}); onyxData.optimisticData?.push(...holdReportOnyxData.optimisticData); onyxData.successData?.push(...holdReportOnyxData.successData); @@ -746,6 +750,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { amountOwed, ownerBillingGracePeriodEnd, methodID, + conciergeReportID, onPaid, } = params; const policyForBillingRestriction = chatReportPolicy ?? (policy?.id === chatReport.policyID ? policy : undefined); @@ -777,6 +782,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { betas, isSelfTourViewed, bankAccountID: paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + conciergeReportID, }); // For now, we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with diff --git a/tests/actions/IOUTest/PayMoneyRequestTest.ts b/tests/actions/IOUTest/PayMoneyRequestTest.ts index cd0952300996..2b179176fec3 100644 --- a/tests/actions/IOUTest/PayMoneyRequestTest.ts +++ b/tests/actions/IOUTest/PayMoneyRequestTest.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxInputValue} from 'react-native-onyx'; +import * as Hold from '@libs/actions/IOU/Hold'; import {putOnHold} from '@libs/actions/IOU/Hold'; import {cancelPayment, completePaymentOnboarding, payMoneyRequest} from '@libs/actions/IOU/PayMoneyRequest'; import {requestMoney} from '@libs/actions/IOU/TrackExpense'; @@ -9,6 +10,7 @@ import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy'; import {notifyNewAction} from '@libs/actions/Report'; import type * as PolicyUtils from '@libs/PolicyUtils'; import {getOriginalMessage, getReportActionHtml, getReportActionText, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import {buildOptimisticIOUReport, buildOptimisticIOUReportAction} from '@libs/ReportUtils'; import {buildOptimisticTransaction} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -252,6 +254,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + conciergeReportID: undefined, }); return waitForBatchedUpdates(); }) @@ -464,6 +467,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + conciergeReportID: undefined, }); return waitForBatchedUpdates(); }) @@ -630,6 +634,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + conciergeReportID: undefined, }); return waitForBatchedUpdates(); }) @@ -684,6 +689,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -801,6 +807,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(partialPayChatReport), + conciergeReportID: undefined, }); return waitForBatchedUpdates(); }) @@ -898,6 +905,7 @@ describe('actions/IOU/PayMoneyRequest', () => { isSelfTourViewed: false, userBillingGracePeriodEnds: undefined, amountOwed: 0, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); const newExpenseReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${newExpenseReportID}`); @@ -935,6 +943,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyTrueTour, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -985,6 +994,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFalseTour, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -1056,6 +1066,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: pastDate, policy, chatReportPolicy: policy, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -1122,6 +1133,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: pastDate, policy: expensePolicy, chatReportPolicy: workspacePolicy, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -1178,6 +1190,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: futureGraceEnd, chatReportPolicy: workspacePolicy, policy: workspacePolicy, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -1214,6 +1227,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyAmountZero, + conciergeReportID: undefined, }); await waitForBatchedUpdates(); @@ -1233,6 +1247,94 @@ describe('actions/IOU/PayMoneyRequest', () => { mockFetch?.resume?.(); }); + + it('forwards the provided conciergeReportID to buildOptimisticReportPreview when partially paying', async () => { + // Given an iou report with two transactions, one of which is held (so partial pay is possible) + const iouReport = buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); + const transaction1 = buildOptimisticTransaction({ + transactionParams: {amount: 100, currency: 'USD', reportID: iouReport.reportID}, + }); + const transaction2 = buildOptimisticTransaction({ + transactionParams: {amount: 100, currency: 'USD', reportID: iouReport.reportID}, + }); + const concierge42TransactionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, + }; + await Onyx.multiSet(concierge42TransactionDataSet); + putOnHold(transaction1.transactionID, 'comment', iouReport.reportID, false, RORY_EMAIL, RORY_ACCOUNT_ID); + await waitForBatchedUpdates(); + + const buildOptimisticReportPreviewSpy = jest.spyOn(ReportUtils, 'buildOptimisticReportPreview'); + const partialPayChatReport = {reportID: topMostReportID, policyID: CONST.POLICY.ID_FAKE}; + const conciergeReportID = 'concierge_report_id_42'; + + // When partial paying with a specific conciergeReportID + payMoneyRequest({ + paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + chatReport: partialPayChatReport, + iouReport, + introSelected: undefined, + iouReportCurrentNextStepDeprecated: undefined, + currentUserAccountID: CARLOS_ACCOUNT_ID, + currentUserLogin: CARLOS_EMAIL, + full: false, + betas: [CONST.BETAS.ALL], + isSelfTourViewed: false, + userBillingGracePeriodEnds: undefined, + amountOwed: 0, + chatReportPolicy: chatReportPolicyFromChat(partialPayChatReport), + conciergeReportID, + }); + await waitForBatchedUpdates(); + + // Then buildOptimisticReportPreview should receive the same conciergeReportID + expect(buildOptimisticReportPreviewSpy).toHaveBeenCalled(); + const callsWithConciergeID = buildOptimisticReportPreviewSpy.mock.calls.filter((args) => args.at(6) === conciergeReportID); + expect(callsWithConciergeID.length).toBeGreaterThan(0); + + buildOptimisticReportPreviewSpy.mockRestore(); + }); + + it('does not invoke getReportFromHoldRequestsOnyxData when full paying, so conciergeReportID is not forwarded', async () => { + // Given an iou report and a non-partial pay (full=true is the default) + const chatReport = { + ...createRandomReport(0, undefined), + lastReadTime: DateUtils.getDBTime(), + lastVisibleActionCreated: DateUtils.getDBTime(), + }; + const iouReport = { + ...createRandomReport(1, undefined), + chatType: undefined, + type: CONST.REPORT.TYPE.IOU, + total: 10, + }; + const getReportFromHoldRequestsOnyxDataSpy = jest.spyOn(Hold, 'getReportFromHoldRequestsOnyxData'); + const conciergeReportID = 'concierge_report_id_should_not_propagate'; + + // When fully paying + payMoneyRequest({ + paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + chatReport, + iouReport, + introSelected: undefined, + iouReportCurrentNextStepDeprecated: undefined, + currentUserAccountID: CARLOS_ACCOUNT_ID, + currentUserLogin: CARLOS_EMAIL, + betas: [CONST.BETAS.ALL], + isSelfTourViewed: false, + userBillingGracePeriodEnds: undefined, + amountOwed: 0, + chatReportPolicy: chatReportPolicyFromChat(chatReport), + conciergeReportID, + }); + await waitForBatchedUpdates(); + + // Then getReportFromHoldRequestsOnyxData is not called at all because full pay skips the hold flow + expect(getReportFromHoldRequestsOnyxDataSpy).not.toHaveBeenCalled(); + + getReportFromHoldRequestsOnyxDataSpy.mockRestore(); + }); }); describe('a expense chat with a cancelled payment', () => { @@ -1338,6 +1440,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + conciergeReportID: undefined, }); return waitForBatchedUpdates(); }) @@ -1563,6 +1666,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + conciergeReportID: undefined, }); } await waitForBatchedUpdates();