From d27b07d517ca9b9bb7b30c285800abbb1db4e08d Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 09:21:40 -0400 Subject: [PATCH 1/6] INSR-8882: Events added to payment page and order confirmation page --- src/constants/analytics.js | 19 +++- .../order-confirmation/order-confirmation.vue | 96 +++++++++++++++++-- .../payment-page-adyen/payment-page-adyen.vue | 13 +++ .../payment-return-adyen.vue | 16 ++++ 4 files changed, 134 insertions(+), 10 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 8cf147e8..7649a96b 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,3 +1,6 @@ +import packageNames from "./package-names"; +import { paymentMethods } from "./payment-method-constants"; + const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', EVENT: 'EVENT' @@ -40,4 +43,18 @@ const ValueToLogTypes = Object.freeze({ LAST_5: 'last_5' }); -export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes }; +const analyticsPaymentTypeMap = new Map([ + [paymentMethods.AFTERPAY, 'after_pay'], + [paymentMethods.CREDIT_CARD, 'credit_card'], + [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'], + [paymentMethods.PAYPAL, 'pay_pal'], + [paymentMethods.APPLEPAY, 'apple_pay'], +]) + +const analyticsServicePackageMap = new Map([ + [packageNames.TIER_ONE, 'basic'], + [packageNames.TIER_TWO, 'essentials'], + [packageNames.TIER_THREE, 'essentialsplus'] +]) + +export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap }; diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index 21ce4fc7..dd8a3198 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -55,7 +55,7 @@ :isRepair="isRepair" />
{{ wipersTitle }} {{ description }}
{{ rainRepelTitle }} {{ rainRepelBody }} @@ -135,6 +135,10 @@ import widgetFields from '@/constants/cms-widget-fields.js'; import { getGlassList } from '@/helpers/damage-helper'; import partTypeStrings from '@/constants/part-type-strings'; import coverageType from '@/constants/coverage-type'; +import { analyticsPaymentTypeMap, analyticsServicePackageMap } from '@/constants/analytics'; +import { containsRecalParts } from '@/helpers/recal-helper'; +import issPageValues from '@/router/router-constants/issPage-values'; +import { getCartTotal } from '@/helpers/cart-helper'; export default { name: 'order-confirmation', @@ -199,6 +203,7 @@ export default { hasRecalibrationPart, referralNumber: referralNumber?.toString(), isNoComp: this.submittedOrder.insuranceCoverage.coverageType === coverageType.NO_COMP, + isITAC: this.submittedOrder.insuranceCoverage.coverageType === coverageType.ITAC, widgets: { siteHeader: 'SiteHeaderWidget', emailConfirmation: 'EmailConfirmationWordingWidget', @@ -418,10 +423,16 @@ export default { // Temporarily set return true to see cart in localhost or dev environment return false; }, - displayWipers() { + hasWipers() { const hasWiperPart = this.submittedOrder.lineItems.vaps.some((part) => part.partType.toLowerCase().includes('wiper')); return hasWiperPart; }, + hasFrontWiper() { + return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER); + }, + hasRearWiper() { + return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER); + }, wipersTitle() { return this.getCmsContent( this.widgets.wipersText, @@ -431,13 +442,10 @@ export default { wipersBody() { const wiperTypesOnOrder = []; - const hasFrontWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER); - const hasRearWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER); - - if (hasFrontWiper) { + if (this.hasFrontWiper) { wiperTypesOnOrder.push(partTypeStrings.FRONT_WIPER); } - if (hasRearWiper) { + if (this.hasRearWiper) { wiperTypesOnOrder.push(partTypeStrings.REAR_WIPER); } @@ -447,7 +455,7 @@ export default { return wiperDescriptions; }, - displayRainRepel() { + hasRainRepel() { return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.RAIN_DEFENSE); }, rainRepelTitle() { @@ -508,6 +516,10 @@ export default { if (this.carrierUrl) { this.$refs.siteFooter.updateButtonText(`Go back to ${this.carrierName}`); } + + const isFirstLoad = this.mainStore.pageData(issPageValues.ORDER_CONFIRMATION)?.isFirstLoad ?? true; + this.savePageDataToStore(issPageValues.ORDER_CONFIRMATION, { isFirstLoad: false }); + this.handleAnalyticsEvents(isFirstLoad); }, methods: { arePagePrerequisitesValid() { @@ -611,6 +623,72 @@ export default { const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsPartType); return vapsTypeDescription?.Text ?? ''; + }, + handleAnalyticsEvents(isFirstLoad) { + const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop'; + const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified'; + const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; + this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true); + + if (this.payment.isPayInAdvance) { + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.payment.paymentMethod); + this.pushEventToGA('payment_page', 'pia_successful', paymentMethodForAnalytics, true); + } + + if (this.submittedOrder.servicePackage) { + const servicePackageForAnalytics = analyticsServicePackageMap.get(this.submittedOrder.servicePackage); + this.pushEventToGA('service_package', 'package_purchased', servicePackageForAnalytics, true); + } + + if (containsRecalParts(this.submittedOrder.lineItems)) { + let coverageType = ''; + if (this.isITAC) { + coverageType = 'ITAC'; + } else if (this.isNoComp) { + coverageType = 'no_comp'; + } else if (this.submittedOrder.isVerified) { + coverageType = 'verified'; + } else { + coverageType = 'unverified'; + } + const recalibrationType = this.submittedOrder.lineItems.glassParts.find((part) => part.requiresRecalibration).recalibrationType; + this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true); + } + + const ymms = `${this.vehicle.year}_${this.vehicle.make}_${this.vehicle.model}_${this.vehicle.style}`; + if (isFirstLoad && getCartTotal(this.submittedOrder) > 0) { + this.pushEventToGA('total_price', getCartTotal(this.submittedOrder).toString(), ymms, true); + } + + this.pushEventToGA('rain_defense', this.hasRainRepel ? 'purchased' : 'no_purchase', ymms, true); + + // TODO: Check if we're coming from SFA and add relevant events + // eslint-disable-next-line + if (false) { + this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'SFA', null); + } + else { + this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + } + + this.pushEventToGA('visitor_info_confirmation', 'client_name', this.mainStore.accountNameForEvents, true); + + const wiperAction = this.hasWipers ? 'purchased' : 'no_purchase'; + + let wiperLabel = this.wipersBody.join('_').toLowerCase().replace('
', '').replace(/beam/g, '').replace(/blades/g, '').trim().replace(/ /g, '_'); + if (wiperLabel.indexOf('front') < 0 && wiperLabel.indexOf('rear') < 0) { + wiperLabel = 'none_none'; + } else { + if (wiperLabel.indexOf('front') < 0) { + wiperLabel = 'front_none_' + wiperLabel; + } + if (wiperLabel.indexOf('rear') < 0) { + wiperLabel = wiperLabel + '_rear_none'; + } + } + wiperLabel = wiperLabel.replace(/__/g, '_'); + + this.pushEventToGA('wipers', wiperAction, wiperLabel, true); } } }; diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue index 91462f51..cdf6529f 100644 --- a/src/layouts/payment-page-adyen/payment-page-adyen.vue +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -75,6 +75,7 @@ import { mapAdyenToIssPaymentMethod, mapIssToAdyenPaymentMethod } from '@/helper import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { Dropin } from "@adyen/adyen-web/auto"; import applicationConfig from '@/constants/application-config'; +import { analyticsPaymentTypeMap } from '@/constants/analytics'; export default { name: 'payment-page-adyen', @@ -118,6 +119,7 @@ export default { }, mounted() { showIssLoadingModal(true); + this.pushEventToGA('payment_page', 'Mode', 'Adyen', true); this.initializeAdyen().finally(() => { showIssLoadingModal(false); }); @@ -353,6 +355,8 @@ export default { }, async paymentFailedPayLater() { this.showIssLoadingModal(true); + const paymentMethod = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', 'save_your_appointment_clicked', `${paymentMethod}_declined`, true); this.mainStore.savePaymentMethodChoice(paymentMethods.PAY_AT_TIME_OF_SERVICE); await submitWorkOrder({ submitType: submitType.SAFELITE }); this.$router.navigate( @@ -523,6 +527,8 @@ export default { const paymentMethodFromSession = adyenResponse?.paymentMethod; const paymentMethod = mapAdyenToIssPaymentMethod(paymentMethodFromSession); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); this.mainStore.savePaymentMethodChoice(paymentMethod); @@ -544,6 +550,10 @@ export default { await global.$logger.logError(stringToLog); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); + this.hasPaymentFailureError = true; } @@ -556,6 +566,9 @@ export default { const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`; await global.$logger.logError(stringToLog); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); this.hasPaymentFailureError = true; } diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index d306857d..33c35194 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -18,6 +18,7 @@ import navigationScenarios from '@/router/router-constants/navigation-scenarios' import submitType from '@/constants/submit-type'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; import { paymentMethods } from '@/constants/payment-method-constants'; +import { analyticsPaymentTypeMap } from '@/constants/analytics'; export default { name: 'payment-return-adyen', @@ -58,6 +59,8 @@ export default { const sessionInfo = await getSessionInfo(sessionId, result.sessionResult); const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); const amountDue = getCartTotal(store.order); const ccToken = generateCcToken(sessionInfo); if (paymentMethod === paymentMethods.AFTERPAY) { @@ -95,6 +98,11 @@ export default { } ); }, + computed: { + piaType() { + return this.mainStore.payment.paymentMethod; + } + }, methods: { finalizeAdyenPayment(sessionId, redirectResult) { return new Promise((resolve, reject) => { @@ -111,6 +119,10 @@ export default { const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`; global.$logger.logError(stringToLog); + + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); } reject({ @@ -123,6 +135,10 @@ export default { const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`; global.$logger.logError(stringToLog); + + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); } reject({ From 50404eb4e6239381046f3367f27d14243a3700e1 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 09:43:06 -0400 Subject: [PATCH 2/6] INSR-8882: Fix failing tests --- .../order-confirmation/order-confirmation.spec.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index be64fdc8..af243e29 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -27,7 +27,8 @@ jest.mock('@/helpers/cms-content-helper', () => ({ const mockMixin = { methods: { getCmsContent: jest.fn(), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; @@ -778,7 +779,8 @@ describe('OrderConfirmation.vue', () => { } return ''; }), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; }); @@ -827,7 +829,8 @@ describe('OrderConfirmation.vue', () => { } return ''; }), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; }); From d4f7213a251c21b01929e8d7976b06e37a2187c0 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 11:17:42 -0400 Subject: [PATCH 3/6] INSR-8882: add unit tests, address possible errors, formatting fixes --- src/constants/analytics.js | 8 +- .../order-confirmation.spec.js | 249 +++++++++++++++++- .../order-confirmation/order-confirmation.vue | 4 +- .../payment-page-adyen/payment-page-adyen.vue | 4 +- .../payment-return-adyen.vue | 4 +- 5 files changed, 258 insertions(+), 11 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 7649a96b..1cb1a160 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,5 +1,5 @@ -import packageNames from "./package-names"; -import { paymentMethods } from "./payment-method-constants"; +import packageNames from './package-names'; +import { paymentMethods } from './payment-method-constants'; const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', @@ -49,12 +49,12 @@ const analyticsPaymentTypeMap = new Map([ [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'], [paymentMethods.PAYPAL, 'pay_pal'], [paymentMethods.APPLEPAY, 'apple_pay'], -]) +]); const analyticsServicePackageMap = new Map([ [packageNames.TIER_ONE, 'basic'], [packageNames.TIER_TWO, 'essentials'], [packageNames.TIER_THREE, 'essentialsplus'] -]) +]); export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap }; diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index af243e29..ed1f1922 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -14,6 +14,8 @@ import coverageType from '@/constants/coverage-type'; import { deepClone } from '@/helpers/object-helper'; import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import { paymentMethods } from '@/constants/payment-method-constants'; +import partTypeStrings from '@/constants/part-type-strings'; +import packageNames from '@/constants/package-names'; jest.mock('@/helpers/layout-helper.js', () => jest.fn()); @@ -28,7 +30,8 @@ const mockMixin = { methods: { getCmsContent: jest.fn(), setCmsContent: jest.fn(), - savePageDataToStore: jest.fn() + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() } }; @@ -958,6 +961,250 @@ describe('OrderConfirmation.vue', () => { }); }); describe('Methods', () => { + describe('handleAnalyticsEvents', () => { + let mixin; + beforeEach(() => { + mixin = { + methods: { + getCmsContent: jest.fn(), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + }); + test('should call pushEventToGA with confirmation event for mobile repair', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; + order.damage.isRepair = true; + order.isVerified = false; + order.isMobileAppointment = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('mobile'), true); + }); + test('should call pushEventToGA with confirmation event for in-shop replace', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; + order.damage.isRepair = false; + order.isVerified = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('replace'), true); + }); + test('should call pushEventToGA for payment PIA successful when payment.isPayInAdvance is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = true; + order.payment.paymentMethod = paymentMethods.CREDIT_CARD; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('payment_page', 'pia_successful', expect.any(String), true); + }); + test('should not call pushEventToGA for PIA when payment.isPayInAdvance is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = false; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const paymentPageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'payment_page'); + expect(paymentPageCalls.length).toBe(0); + }); + test('should call pushEventToGA for service package when submittedOrder.servicePackage exists', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = packageNames.TIER_ONE; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('service_package', 'package_purchased', expect.any(String), true); + }); + test('should not call pushEventToGA for service package when submittedOrder.servicePackage is null', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = null; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const servicePackageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'service_package'); + expect(servicePackageCalls.length).toBe(0); + }); + test('should call pushEventToGA for total price when isFirstLoad is true and cart total greater than 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // Act + wrapper.vm.handleAnalyticsEvents(true); + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('total_price', expectedTotal.toString(), expect.any(String), true); + }); + test('should not call pushEventToGA for total price when isFirstLoad is false', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + useMainStore().pageData = jest.fn().mockReturnValue({ isFirstLoad: false }); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should not call pushEventToGA for total price when cart total is 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 0; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should call pushEventToGA for rain defense purchased when hasRainRepel is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.RAIN_DEFENSE }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for rain defense no_purchase when hasRainRepel is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'no_purchase', expect.any(String), true); + }); + test('should call pushEventToGA for visitor_info_confirmation with ClientSite referring_site', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + }); + test('should call pushEventToGA for visitor_info_confirmation with client_name', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'client_name', expect.any(String), true); + }); + test('should call pushEventToGA for wipers purchased when hasWipers is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.FRONT_WIPER }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const mixin = { + methods: { + getCmsContent: jest.fn().mockReturnValue([{ Text: 'Front Beam' }]), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for wipers no_purchase when hasWipers is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true); + }); + });; describe('formatDate', () => { test.each([ ['Wednesday, April 22, 2020', '2020-04-22'], diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index dd8a3198..063346f2 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -627,7 +627,7 @@ export default { handleAnalyticsEvents(isFirstLoad) { const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop'; const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified'; - const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; + const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true); if (this.payment.isPayInAdvance) { @@ -651,7 +651,7 @@ export default { } else { coverageType = 'unverified'; } - const recalibrationType = this.submittedOrder.lineItems.glassParts.find((part) => part.requiresRecalibration).recalibrationType; + const recalibrationType = this.submittedOrder.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType; this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true); } diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue index cdf6529f..8608bc29 100644 --- a/src/layouts/payment-page-adyen/payment-page-adyen.vue +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -551,7 +551,7 @@ export default { await global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); this.hasPaymentFailureError = true; @@ -567,7 +567,7 @@ export default { await global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); this.hasPaymentFailureError = true; diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index 33c35194..85e1888b 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -121,7 +121,7 @@ export default { global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); } @@ -137,7 +137,7 @@ export default { global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); } From 7146bc1656131264dcfd167ec696f54eb41496e6 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 11:58:57 -0400 Subject: [PATCH 4/6] INSR-8882: Fix test that failed when run in larger suite --- src/layouts/order-confirmation/order-confirmation.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index ed1f1922..df85a7ec 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -156,7 +156,8 @@ const sessionStorage = { currentDeductible: { replace: 100, repair: 0 - } + }, + isUnverified: false }; function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}, mixin = mockMixin) { @@ -1103,6 +1104,7 @@ describe('OrderConfirmation.vue', () => { const order = deepClone(sessionStorage); const expectedTotal = 0; order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }]; + order.insuranceCoverage.coverageType = coverageType.ITAC; const mockStoreActions = () => { useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); }; From 7e37f4b7fe429cdd4f9664ea7c6584471b8c577d Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 12:31:10 -0400 Subject: [PATCH 5/6] INSR-8882: absolute pathing for imports in analytics constants file --- src/constants/analytics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 1cb1a160..dc023b15 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,5 +1,5 @@ -import packageNames from './package-names'; -import { paymentMethods } from './payment-method-constants'; +import packageNames from '@/constants/package-names'; +import { paymentMethods } from '@/constants/payment-method-constants'; const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', From db4584bc00d5427358295f36897f0862eb9eb1a8 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Mon, 13 Apr 2026 12:33:13 -0400 Subject: [PATCH 6/6] INSR-8882: Move method call after const declaration --- src/layouts/order-confirmation/order-confirmation.spec.js | 2 +- src/layouts/payment-return-adyen/payment-return-adyen.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index df85a7ec..9438b038 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -1206,7 +1206,7 @@ describe('OrderConfirmation.vue', () => { // Assert expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true); }); - });; + }); describe('formatDate', () => { test.each([ ['Wednesday, April 22, 2020', '2020-04-22'], diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index 85e1888b..ce1ae7e5 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -60,7 +60,6 @@ export default { const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); - this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); const amountDue = getCartTotal(store.order); const ccToken = generateCcToken(sessionInfo); if (paymentMethod === paymentMethods.AFTERPAY) { @@ -70,6 +69,7 @@ export default { ccToken.expMonth = "03"; ccToken.expYear = "2030"; } + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); store.savePaymentMethodChoice(paymentMethod); store.updateCreditCardToken(ccToken);