From 1162233dd0bdf1885e24e4d4859487835e585af2 Mon Sep 17 00:00:00 2001 From: eve-git Date: Mon, 23 Feb 2026 16:19:29 -0800 Subject: [PATCH 1/2] prevent duplicate payment for PPR registration --- ppr-ui/package.json | 2 +- ppr-ui/src/components/common/ButtonFooter.vue | 19 ++++++--- ppr-ui/tests/unit/ButtonFooter.spec.ts | 42 ++++++++++++++++++- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/ppr-ui/package.json b/ppr-ui/package.json index 8b60fdf0c..24c633fe2 100644 --- a/ppr-ui/package.json +++ b/ppr-ui/package.json @@ -1,6 +1,6 @@ { "name": "ppr-ui", - "version": "6.0.8", + "version": "6.0.9", "private": true, "appName": "Assets UI", "connectLayerName": "Core UI", diff --git a/ppr-ui/src/components/common/ButtonFooter.vue b/ppr-ui/src/components/common/ButtonFooter.vue index eeb18c262..eafce283f 100644 --- a/ppr-ui/src/components/common/ButtonFooter.vue +++ b/ppr-ui/src/components/common/ButtonFooter.vue @@ -73,7 +73,7 @@ @@ -291,12 +291,19 @@ export default defineComponent({ } if (checkValid()) { + // Prevents multiple submits (i.e. double click) + if (localState.submitting) { + return + } + localState.submitting = true + if (localState.isStaffReg) { localState.staffPaymentDialogDisplay = true } else { submitFinancingStatement() } } else { + localState.submitting = false // emit registration incomplete error const error: ErrorIF = { statusCode: 400, @@ -333,6 +340,8 @@ export default defineComponent({ if (apiResponse.error !== undefined) { // Emit error message. emit('error', apiResponse.error) + // set submitting to false to allow user to try submitting again after error + localState.submitting = false } else if (apiResponse.paymentPending) { goToPay(apiResponse.payment?.invoiceId, null, `pprReg-${apiResponse.documentId}`) } else { @@ -347,6 +356,7 @@ export default defineComponent({ await goToRoute(localState.buttonConfig.nextRouteName as RouteNames) } } else { + localState.submitting = false // emit registation incomplete error const error: ErrorIF = { statusCode: 400, @@ -356,16 +366,13 @@ export default defineComponent({ } } - const throttleSubmitStatement = throttle(async (stateModel: StateModelIF): Promise => { - // Prevents multiple submits (i.e. double click) - localState.submitting = true + const throttleSubmitStatement = async (stateModel: StateModelIF): Promise => { const statement = await saveFinancingStatement( stateModel, userSelectedPaymentMethod.value === ConnectPaymentMethod.DIRECT_PAY ) - localState.submitting = false return statement - }, 2000, { trailing: false }) + } const throttleSubmitStatementDraft = throttle(async (stateModel: StateModelIF): Promise => { // Prevents multiple submits (i.e. double click) diff --git a/ppr-ui/tests/unit/ButtonFooter.spec.ts b/ppr-ui/tests/unit/ButtonFooter.spec.ts index 1fafeef03..9ae69cf40 100644 --- a/ppr-ui/tests/unit/ButtonFooter.spec.ts +++ b/ppr-ui/tests/unit/ButtonFooter.spec.ts @@ -14,6 +14,9 @@ import { mockedManufacturerAuthRoles, mockedModelAmendmdmentAdd } from './test-d import type { ButtonConfigIF } from '@/interfaces' import { MhrRegistrationType } from '@/resources' import { createPinia, setActivePinia } from 'pinia' +import { vi } from 'vitest' +import * as navigationComposable from '@/composables/common/useNavigation' +import * as registrationUtils from '@/utils' // Input field selectors / buttons const cancelBtn: string = '#reg-cancel-btn' @@ -142,7 +145,7 @@ describe('New Financing Statement Registration Buttons Step 3', () => { wrapper = await createComponent(ButtonFooter, { currentStepName: RouteNames.ADD_COLLATERAL, navConfig: RegistrationButtonFooterConfig - }, RouteNames.ADD_COLLATERAL. null, [pinia]) + }, RouteNames.ADD_COLLATERAL, null, [pinia]) }) it('renders with step 3 values', async () => { @@ -322,6 +325,43 @@ describe('Button events', () => { await flushPromises() expect(getLastEvent(wrapper, 'registrationIncomplete')).not.toBeNull() }) + + it('calls goToPay once when ppr Review Confirm is triggered', async () => { + const goToPayMock = vi.fn() + const useNavigationSpy = vi.spyOn(navigationComposable, 'useNavigation').mockReturnValue({ + goToDash: vi.fn(), + goToRoute: vi.fn(), + goToPay: goToPayMock + } as any) + vi.spyOn(registrationUtils, 'saveFinancingStatement').mockResolvedValue({ + paymentPending: true, + payment: { invoiceId: '12345' }, + documentId: 'T1000001' + } as any) + + const multiClickWrapper = await createComponent(ButtonFooter, { + currentStepName: RouteNames.REVIEW_CONFIRM, + navConfig: RegistrationButtonFooterConfig, + certifyValid: true + }, RouteNames.REVIEW_CONFIRM, null, [pinia]) + + store.getStateModel.registration = mockedModelAmendmdmentAdd.registration + store.getStateModel.registration.lengthTrust.valid = true + store.getStateModel.registration.parties.valid = true + store.getStateModel.registration.collateral.valid = true + store.isRoleStaffReg = { value: false } + await flushPromises() + + vi.useFakeTimers() + await multiClickWrapper.vm.submitNext() + vi.advanceTimersByTime(3000) + await multiClickWrapper.vm.submitNext() + await flushPromises() + + expect(goToPayMock).toHaveBeenCalledTimes(1) + vi.useRealTimers() + useNavigationSpy.mockRestore() + }) }) describe('Mhr Manufacturer Registration step 1 - Your Home', () => { From 06958ae7224066e7828359a8d4e2510fe2a06f32 Mon Sep 17 00:00:00 2001 From: eve-git Date: Tue, 24 Feb 2026 08:19:53 -0800 Subject: [PATCH 2/2] review update --- ppr-ui/src/components/common/ButtonFooter.vue | 6 +++--- ppr-ui/tests/unit/ButtonFooter.spec.ts | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ppr-ui/src/components/common/ButtonFooter.vue b/ppr-ui/src/components/common/ButtonFooter.vue index eafce283f..8bbd20c99 100644 --- a/ppr-ui/src/components/common/ButtonFooter.vue +++ b/ppr-ui/src/components/common/ButtonFooter.vue @@ -237,7 +237,7 @@ export default defineComponent({ draft = await mhrDraftHandler() } else { const stateModel: StateModelIF = getStateModel.value - draft = await throttleSubmitStatementDraft(stateModel) + draft = await submitStatement(stateModel) prevDraftId = stateModel.registration?.draft?.financingStatement?.documentId || '' } @@ -336,7 +336,7 @@ export default defineComponent({ const stateModel: StateModelIF = getStateModel.value if (checkValid()) { // API call here - const apiResponse: FinancingStatementIF = await throttleSubmitStatement(stateModel) + const apiResponse: FinancingStatementIF = await submitStatement(stateModel) if (apiResponse.error !== undefined) { // Emit error message. emit('error', apiResponse.error) @@ -366,7 +366,7 @@ export default defineComponent({ } } - const throttleSubmitStatement = async (stateModel: StateModelIF): Promise => { + const submitStatement = async (stateModel: StateModelIF): Promise => { const statement = await saveFinancingStatement( stateModel, userSelectedPaymentMethod.value === ConnectPaymentMethod.DIRECT_PAY diff --git a/ppr-ui/tests/unit/ButtonFooter.spec.ts b/ppr-ui/tests/unit/ButtonFooter.spec.ts index 9ae69cf40..25b454ff7 100644 --- a/ppr-ui/tests/unit/ButtonFooter.spec.ts +++ b/ppr-ui/tests/unit/ButtonFooter.spec.ts @@ -354,6 +354,11 @@ describe('Button events', () => { vi.useFakeTimers() await multiClickWrapper.vm.submitNext() + + // Assert submitting is true and button is disabled + expect(multiClickWrapper.vm.submitting).toBe(true) + expect(multiClickWrapper.find('#reg-next-btn').attributes().disabled).toBeDefined() + vi.advanceTimersByTime(3000) await multiClickWrapper.vm.submitNext() await flushPromises()