From 22cba2d54b435a9d1f575a82d18da39db201624f Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 25 Mar 2026 10:57:59 -0400 Subject: [PATCH 01/29] CASH-2481 - when navigating forward, skip service-zip page if zip exists and is valid --- src/layouts/estimate/estimate.vue | 3 ++- src/layouts/service-zip/service-zip.vue | 8 ++++++++ src/layouts/vehicle-damage/vehicle-damage.vue | 5 +++-- src/layouts/vehicle/vehicle.vue | 10 ++++++++-- .../methods/helpers/initialize-from-querystrings.js | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/layouts/estimate/estimate.vue b/src/layouts/estimate/estimate.vue index 60d050d73..36e27127a 100644 --- a/src/layouts/estimate/estimate.vue +++ b/src/layouts/estimate/estimate.vue @@ -62,6 +62,7 @@ import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; import { nextTick } from "vue"; import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -169,7 +170,7 @@ export default { baseMixin.methods.ResetExternalParamsAndHideModal(); } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } } else { baseMixin.methods.ResetExternalParamsAndHideModal(); diff --git a/src/layouts/service-zip/service-zip.vue b/src/layouts/service-zip/service-zip.vue index ab99adc6f..c82a0a0e3 100644 --- a/src/layouts/service-zip/service-zip.vue +++ b/src/layouts/service-zip/service-zip.vue @@ -80,6 +80,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; import { routeData } from "@/router/constants/routes"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -137,6 +138,13 @@ export default { }, false ); + vm.serviceZipCode = store.getters.externalParameterServiceZip.zipCode; + const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); + if (isValid) { + showFmgLoadingModal(true); + vm.forwardButtonAction(); + return; + } } if (store.getters.externalParameterCustomer?.phoneNumber) { diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index 5e128e867..abad0da7d 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -93,6 +93,7 @@ import { errorMessages } from "@/constants/error-messages"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { debugLog } from "@/helpers/debug-log-helper"; import store from "@/store"; @@ -199,7 +200,7 @@ export default { return vm.forwardButtonAction(); } } - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); //clear any validation errors for external parameter flow const form = vm.$refs.theForm; @@ -209,7 +210,7 @@ export default { } } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } }); }, diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index 4521a6e2f..92a7aa810 100644 --- a/src/layouts/vehicle/vehicle.vue +++ b/src/layouts/vehicle/vehicle.vue @@ -109,6 +109,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { applicationConfig } from "../../constants/application-config"; import { queryStrings } from "@/constants/query-strings"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { debugLog } from "@/helpers/debug-log-helper"; //define validation rules @@ -291,7 +292,12 @@ export default { resultMap.modelQuestionInitialData, resultMap.styleQuestionInitialData ); - if (store.getters.externalParameterState?.isExternalParameter) { + if ( + store.getters.externalParameterState?.isExternalParameter && + store.getters.externalParameterVehicle.year && + store.getters.externalParameterVehicle.make && + store.getters.externalParameterVehicle.model + ) { await vm.getVehicleDetails(); if (!vm.displayNoServiceAlert) { const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); @@ -304,7 +310,7 @@ export default { baseMixin.methods.ResetExternalParamsAndHideModal(); } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } }); }, diff --git a/src/router/methods/helpers/initialize-from-querystrings.js b/src/router/methods/helpers/initialize-from-querystrings.js index 8dff4423d..6b6cecff2 100644 --- a/src/router/methods/helpers/initialize-from-querystrings.js +++ b/src/router/methods/helpers/initialize-from-querystrings.js @@ -95,6 +95,7 @@ function updateExternalParameterState() { } if (externalParameterZipCode) { store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_ZIP_CODE, externalParameterZipCode); + store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, externalParameterStatus.ACTIVE); } if (externalParameterEmail) { store.commit( From c8c3b406b4faee3457e29a4aacbbfdb63e6c0f6e Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 25 Mar 2026 11:04:39 -0400 Subject: [PATCH 02/29] CASH-2481 - update unit test --- src/layouts/vehicle/vehicle.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layouts/vehicle/vehicle.spec.js b/src/layouts/vehicle/vehicle.spec.js index d4bc1f726..ba1c4c621 100644 --- a/src/layouts/vehicle/vehicle.spec.js +++ b/src/layouts/vehicle/vehicle.spec.js @@ -93,8 +93,11 @@ describe("vehicle.vue", () => { }); describe("vehicle.vue", () => { - test("should call forwardButtonAction if isExternalParameter is true and form is valid", async () => { + test("should call forwardButtonAction if isExternalParameter is true, has a year, make and model, and form is valid", async () => { store.getters.externalParameterState.isExternalParameter = true; + store.getters.externalParameterVehicle.year = "1886"; + store.getters.externalParameterVehicle.make = "Benz"; + store.getters.externalParameterVehicle.model = "Patent-Motorwagen"; // Create a shallow mount of MyComponent const { wrapper } = setupMocks(); // Set displayNoServiceAlert to false From f5f720526fb64a7e07e38a67950cf8fa7a6fae50 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Wed, 25 Mar 2026 15:10:57 -0400 Subject: [PATCH 03/29] CASH-2415 CASH-2415 changes to allow forcing experiments on querystring --- src/constants/endpoints.js | 4 ++++ src/constants/store-actions.js | 1 + src/constants/store-mutations.js | 1 - .../helpers/initialize-from-querystrings.js | 17 ++++++++++++----- src/router/methods/helpers/run-experiments.js | 12 ++++++++++++ src/store/index.js | 19 +++++++++++++++++++ 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index d25fce6a3..c77b1bbad 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -196,6 +196,10 @@ const endpoints = { url: "/experiments/api/v1/experiments/run", method: "POST", }, + ForceExperiments: { + url: "/experiments/api/v1/experiments/forceVariations", + method: "POST", + }, GetInsuranceCompanyList: { url: "/account/api/v1/account/insurance-companies", method: "GET", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 26bcc25e1..4f405eb0f 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -47,6 +47,7 @@ const storeActions = { TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA: "taxOrderItemsAndSaveServerData", GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser", RUN_EXPERIMENTS_FOR_TRIGGER: "runExperimentsForTrigger", + FORCE_EXPERIMENTS: "forceExperiments", CLEAR_VIN: "clearVin", RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise", GET_SIGNATURE: "getSignature", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 5c6c2e22d..567812702 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -118,7 +118,6 @@ const storeMutations = { UPDATE_EXTERNAL_PARAMETER_IS_INSURANCE: "updateExternalParameterIsInsurance", UPDATE_EXTERNAL_PARAMETER_VIN_SELECTION: "updateExternalParameterVinSelection", UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE: "updateExternalParameterServicePackage", - UPDATE_EXTERNAL_PARAMETER_SOURCE: "updateExternalParameterSource", UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER: "updateExternalParameterPhoneNumber", UPDATE_EXTERNAL_PARAMETER_QS_STASH: "updateExternalParameterQsStash", diff --git a/src/router/methods/helpers/initialize-from-querystrings.js b/src/router/methods/helpers/initialize-from-querystrings.js index 8dff4423d..24f137a8f 100644 --- a/src/router/methods/helpers/initialize-from-querystrings.js +++ b/src/router/methods/helpers/initialize-from-querystrings.js @@ -9,6 +9,7 @@ import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { queryStrings } from "@/constants/query-strings"; import { externalParameterStatus } from "@/constants/external-parameters"; import { debugLog } from "@/helpers/debug-log-helper"; +import { runExperiments, forceExperiments } from "@/router/methods/helpers/run-experiments"; export async function initializeFromQueryStrings() { // Get advertiser info @@ -16,6 +17,17 @@ export async function initializeFromQueryStrings() { // Read all external paramter queries and write to store. updateExternalParameterState(); + + forceExperimentsFromQuerystrings(); +} + +async function forceExperimentsFromQuerystrings() { + const experimentsFromQueryString = getQuerystringParameter(queryStrings.EXPERIMENTS); + if (experimentsFromQueryString) { + var qs = decodeURIComponent(window.location.search); + let forcedExperiments = qs.substring(qs.indexOf(queryStrings.EXPERIMENTS)); + await forceExperiments(forcedExperiments); + } } function setupAdvertisersAndAffiliates() { @@ -39,7 +51,6 @@ function updateExternalParameterState() { const externalParameterVinSelection = getQuerystringParameter(queryStrings.VIN_SELECTION); const externalParameterServicePackage = getQuerystringParameter(queryStrings.SERVICE_PACKAGE); const externalParameterNumberOfChips = getQuerystringParameter(queryStrings.NUMBER_OF_CHIPS); - const externalParameterSource = getQuerystringParameter(queryStrings.EXPERIMENTS); const externalParameterPhoneNumber = getQuerystringParameter(queryStrings.PHONE_NUMBER); if (!externalParameterZipCode) { @@ -58,7 +69,6 @@ function updateExternalParameterState() { debugLog("externalParameterVinSelection:", externalParameterVinSelection); debugLog("externalParameterServicePackage:", externalParameterServicePackage); debugLog("externalParameterNumberOfChips:", externalParameterNumberOfChips); - debugLog("externalParameterSource:", externalParameterSource); debugLog("externalPhoneNumber:", externalParameterPhoneNumber); } @@ -115,9 +125,6 @@ function updateExternalParameterState() { externalParameterVinSelection ); } - if (externalParameterSource) { - store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_SOURCE, externalParameterSource); - } if (externalParameterPhoneNumber) { store.commit( storeMutations.UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER, diff --git a/src/router/methods/helpers/run-experiments.js b/src/router/methods/helpers/run-experiments.js index 97472ad0c..edcfddeb4 100644 --- a/src/router/methods/helpers/run-experiments.js +++ b/src/router/methods/helpers/run-experiments.js @@ -28,3 +28,15 @@ export async function runExperiments(nextPageName) { nextPageName ); } + +export async function forceExperiments(forceExperimentQueryStringValues) { + await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.FORCE_EXPERIMENTS, + { + deviceId: getDeviceIdValue(), + queryStringExperiments: forceExperimentQueryStringValues, + }, + "/", + false + ); +} diff --git a/src/store/index.js b/src/store/index.js index e740bf0d3..23b8cf2a3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1816,6 +1816,25 @@ export const actions = { context.commit(storeMutations.UPDATE_EXPERIMENTS, response.data.experiments); }, + async forceExperiments( + context, + { payload: { deviceId, queryStringExperiments }, pageNameToLog } + ) { + var payload = { + applicationName: applicationConfig.APPLICATION_NAME, + deviceId: deviceId, + queryStringExperiments: queryStringExperiments, + }; + + const response = await globalMethods.callHttpClient({ + method: endpoints.ForceExperiments.method, + endpoint: endpoints.ForceExperiments.url, + payload: payload, + logApiCall: true, + pageNameToLog: pageNameToLog, + }); + }, + getEvoxImage(context, { relativeUrl }) { return globalMethods.callHttpClient({ method: endpoints.GetPageData.method, From d1ebb59cbb1132870138e42458df7c31d364fb87 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Thu, 26 Mar 2026 13:55:27 -0400 Subject: [PATCH 04/29] CASH-2497 log full querystring on init session CASH-2497 log full querystring on init session --- src/constants/endpoints.js | 4 -- src/constants/store-actions.js | 1 - src/global-methods.js | 1 - src/mixins/analytics-mixin.js | 73 ++++--------------------- src/router/methods/after-each.js | 3 - src/router/methods/route-logic/error.js | 2 - src/store/index.js | 32 +---------- 7 files changed, 11 insertions(+), 105 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index d25fce6a3..1a4cc4677 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -180,10 +180,6 @@ const endpoints = { url: "/analytics/api/v1/analytics/log-part-questions", method: "POST", }, - LogDigitalConsumer: { - url: "/analytics/api/v1/analytics/digitalconsumer-log", - method: "POST", - }, LogFmgSessionData: { url: "/analytics/api/v1/analytics/digitalconsumer-session-logging", method: "POST", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 26bcc25e1..04d51056a 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -63,7 +63,6 @@ const storeActions = { LOG_CUSTOM_EVENT: "logCustomEvent", INITIALIZE_SESSION: "initializeSession", LOG_PART_QUESTIONS: "logPartQuestions", - LOG_DIGITALCONSUMER: "logDigitalConsumer", LOG_FMG_SESSION_DATA: "logFmgSessionData", // DEPENDENCY MUTATIONS diff --git a/src/global-methods.js b/src/global-methods.js index 4f1dc601b..f408230f8 100644 --- a/src/global-methods.js +++ b/src/global-methods.js @@ -102,7 +102,6 @@ export default { }, (error) => { if ( - endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url) || endpoint.toLowerCase().includes(endpoints.LogFmgSessionData.url) || endpoint.toLowerCase().includes(endpoints.LogPageView.url) || endpoint.toLowerCase().includes(endpoints.LogCustomEvent.url) || diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 781c0cb34..9386846d7 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -132,51 +132,6 @@ export default { } }, - async logDigitalConsumer() { - const currentPageName = getPageNameFromRouter(); - const universes = store.getters.applicationUser.experiments; - - const variationNames = universes - .filter((item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL) - .map((item) => item.variationName) - .filter(Boolean); // removes undefined/null - - const conceptVariation = variationNames.length > 0 ? variationNames[0] : ""; - - const isConceptExposed = universes.find( - (item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL - )?.isExposed; - - const submittedOrder = baseMixin.methods.getSubmittedOrder(); - const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); - const hasSubmittedOrderAtConfirmationPage = - hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name; - - var payload = { - actionName: `Browser page:${currentPageName}`, - referralSequenceNumber: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.referralSequenceNumber - : store.getters.order.referralSequenceNumber, - referralNumber: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.referralNumber - : store.getters.order.referralNumber, - workOrderId: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.workOrderId - : store.getters.order.workOrderId, - workOrderNumber: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.workOrderNumber - : store.getters.order.workOrderNumber, - conceptVariation: conceptVariation, - isConceptExposed: isConceptExposed, - }; - - await baseMixin.methods.dispatchStoreAction( - storeActions.LOG_DIGITALCONSUMER, - payload, - false - ); - }, - async pushEventForChatsToGA(category, action, label, pushToLogApp = false) { const currentPageName = getPageNameFromRouter(); const value = `2.0_${currentPageName}`; @@ -206,6 +161,11 @@ export default { // S3 bucket, safelite-dev-digitalconsumer-session-data-us-east-2/1. // This bucket data is then picked up by snowflake for analytics use. async pushFmgSessionData() { + var currentPageName = getPageNameFromRouter(true); + if (!currentPageName) { + return; + } + const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.getters.order; @@ -247,22 +207,6 @@ export default { } var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`; - var currentPageName = getPageNameFromRouter(); - - // add query strings to the page name for debugging. on the vehicle page, if from an external link, pull it from the stash - if (currentPageName === "vehicle") { - if (!window.location.search) { - if (store.getters.externalParameterState?.qsStash) { - currentPageName += `${store.getters.externalParameterState.qsStash}`; - } - } else { - currentPageName += `${window.location.search}`; - } - } else { - if (window.location.search) { - currentPageName += `${window.location.search}`; - } - } var sessionData = {}; sessionData.currentPage = currentPageName; @@ -911,6 +855,9 @@ export default { setSessionIdIfUnset(response.data.sessionId); } } + + // logging on session init so we capture complete query string data + await this.pushFmgSessionData(); }, noSession() { @@ -977,7 +924,7 @@ function pushToDataLayerIfDefined(data) { } } -function getPageNameFromRouter() { +function getPageNameFromRouter(useDefaultUrl = false) { if ( router && router.currentRoute && @@ -987,7 +934,7 @@ function getPageNameFromRouter() { return router.currentRoute.value.name; } - return window.location.href.replace(/\/$/, "").split("/").pop(); + return useDefaultUrl ? window.location.search : window.location.href.replace(/\/$/, "").split("/").pop(); } function getValueToLog(value, valueToLogType) { diff --git a/src/router/methods/after-each.js b/src/router/methods/after-each.js index 608119399..43ed0d4b5 100644 --- a/src/router/methods/after-each.js +++ b/src/router/methods/after-each.js @@ -5,9 +5,6 @@ import { storeMutations } from "@/constants/store-mutations"; export async function afterEach(to, from) { store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); - // digital consumer logging - analyticsMixin.methods.logDigitalConsumer(); - // digital consumer fmg session logging to snowflake analyticsMixin.methods.pushFmgSessionData(); diff --git a/src/router/methods/route-logic/error.js b/src/router/methods/route-logic/error.js index fb9dd2cbc..162e5b914 100644 --- a/src/router/methods/route-logic/error.js +++ b/src/router/methods/route-logic/error.js @@ -19,7 +19,6 @@ export async function errorBeforeEnter(to, from) { nextPage: to?.name, }; - analyticsMixin.methods.logDigitalConsumer(); await handleHardError(errorPayload); return; } @@ -33,7 +32,6 @@ export async function errorBeforeEnter(to, from) { nextPage: to?.name, }; - analyticsMixin.methods.logDigitalConsumer(); await handleHardError(errorPayload); return; } else { diff --git a/src/store/index.js b/src/store/index.js index e740bf0d3..8a3628164 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1630,37 +1630,6 @@ export const actions = { }); }, - logDigitalConsumer( - context, - { - actionName, - referralSequenceNumber, - referralNumber, - workOrderId, - workOrderNumber, - conceptVariation, - isConceptExposed, - } - ) { - var payload = { - sessionId: getSessionIdValue(), - deviceId: getDeviceIdValue(), - actionName: actionName ?? "", - referralSequenceNumber: referralSequenceNumber ?? "", - referralNumber: referralNumber ?? "", - applicationName: baseMixin.methods.isMobileDevice() ? "2.0 Mobile" : "2.0", - workOrderId: workOrderId ?? "", - workOrderNumber: workOrderNumber ?? "", - conceptVariation: conceptVariation, - isConceptExposed: isConceptExposed, - }; - - return globalMethods.callHttpClient({ - method: endpoints.LogDigitalConsumer.method, - endpoint: endpoints.LogDigitalConsumer.url, - payload: payload, - }); - }, logFmgSessionData( context, @@ -1770,6 +1739,7 @@ export const actions = { method: endpoints.LogFmgSessionData.method, endpoint: endpoints.LogFmgSessionData.url, payload: payload, + logApiCall: false, }); }, From 38d50f6612a90ba926f32dd38ad68a97e7dcc4c5 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 26 Mar 2026 15:39:40 -0400 Subject: [PATCH 05/29] CASH-1689: MSR fee in cart for insurance --- src/constants/store-actions.js | 1 + src/constants/store-mutations.js | 1 + src/fmg-components/cart/cart.vue | 108 ++++++++++++- .../remove-msr-fee-modal.vue | 153 ++++++++++++++++++ .../content-group-modal.vue | 4 + src/helpers/pricing-helper.js | 19 +++ src/layouts/confirmation/confirmation.vue | 7 +- src/layouts/payment-adyen/payment-adyen.vue | 1 + src/layouts/payment-method/payment-method.vue | 16 +- src/layouts/payment/payment.vue | 1 + src/layouts/schedule/schedule.vue | 8 + src/router/constants/navigation-scenarios.js | 1 + src/router/constants/routing-table.js | 4 + src/store/index.js | 12 ++ 14 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 26bcc25e1..4590249f3 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -114,6 +114,7 @@ const storeActions = { SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling", SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable", + SAVE_IS_MSR_FEE_COVERED_BY_INSURANCE: "saveIsMSRFeeCoveredByInsurance", CREATE_SUBMITTED_STATE: "createSubmittedState", RESET_SUBMITTED_STATE: "resetSubmittedState", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 5c6c2e22d..b1344dcb5 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -72,6 +72,7 @@ const storeMutations = { UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling", UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", + UPDATE_IS_MSR_FEE_COVERED_BY_INSURANCE: "updateIsMSRFeeCoveredByInsurance", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", // EVENT BUS MUTATIONS diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index e890e53cc..7de87b678 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -77,7 +77,7 @@ i % 2 == 0 ? 'even' : 'odd', cartItem.isRemovable ? 'removable-cart-item' : '', ]"> - {{ cartItem.name }} + {{ cartItem.name }} + + {{ msrFeeCartItem.name }} + + Info Icon + + +
+
@@ -162,6 +179,7 @@ import textLink from "@/ux-components/text-link/text-link"; import textBlock from "@/digital-components/text-block/text-block"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question"; +import removeMsrFeeModal from "./remove-msr-fee-modal/remove-msr-fee-modal.vue"; // Mixins import baseMixin from "@/mixins/base-mixin.js"; @@ -183,6 +201,7 @@ import { getSalesTax, getAmountDueWithDonation, } from "@/helpers/pricing-helper.js"; +import { formatToUSDollar } from "@/helpers/cms-content-helper"; // Constants import { partTypeStrings } from "@/constants/part-type-strings"; @@ -191,6 +210,7 @@ import { cartItemTypes } from "@/constants/cart-item-types"; import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance"; import { experimentSettings } from "@/constants/experiments"; import { quotePageDiscountTable } from "@/constants/quote-page-discounts"; +import { partNumberStrings } from "@/constants/part-number-strings"; export default { name: "cart", @@ -203,6 +223,7 @@ export default { pageName: String, allowItemRemoval: Boolean, recyclingModalCmsWidgetName: String, + msrModalCmsWidgetName: String, showAsPaid: Boolean, isInsurance: Boolean, insuranceDeductible: Number, @@ -214,6 +235,7 @@ export default { isExpandedOnLoad: Boolean, isCollapsible: { type: Boolean, default: true }, isMSRFeeApplicable: Boolean, + IsMSRFeeCoveredByInsurance: Boolean, donationCartItem: Object, }, data() { @@ -311,6 +333,10 @@ export default { }, async removeItem(cartItemType, category) { + if (cartItemType == cartItemTypes.MOBILE_FEE && this.isMSRFeeApplicable) { + this.$refs.removeMsrFeeModal.openModal(); + return false; + } this.lineItems[category] = this.lineItems[category].filter( (lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType ); @@ -375,6 +401,12 @@ export default { } return []; }, + shouldShowInfoIcon(cartItem) { + return cartItem == this.recycleFeeCartItem || cartItem == this.msrFeeCartItem; + }, + switchToInshop() { + this.$emit("switchToInshop"); + }, }, computed: { isCartReadyToLoad() { @@ -490,6 +522,10 @@ export default { cartItems.push(this.mobileFeeCartItem); } + if (this.msrFeeCartItem) { + cartItems.push(this.msrFeeCartItem); + } + if (this.quotePageDiscountCartItem) { cartItems.push(this.quotePageDiscountCartItem); } @@ -892,7 +928,9 @@ export default { let cartItem = null; let showMobileFeeCartItem = false; const mobileFeeLineItem = this.supportingItems.find( - (lineItem) => lineItem.partType == partTypeStrings.MOBILE_FEE + (lineItem) => + lineItem.partType == partTypeStrings.MOBILE_FEE && + lineItem.partNumber == partTypeStrings.MOBILE_FEE ); var mobileTotal = @@ -929,6 +967,67 @@ export default { } return cartItem; }, + msrFeeCartItemName() { + return this.getCmsContent("MSRFeeTextWidget", "Text"); + }, + getMsrFeeLineItem() { + return this.supportingItems.find( + (lineItem) => + lineItem.partType == partTypeStrings.MOBILE_FEE && + lineItem.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE + ); + }, + msrFeeCartItem() { + let cartItem = null; + const msrFeeLineItem = this.getMsrFeeLineItem; + const shouldCreateMsrFeeCartItem = + this.isInsurance && + !this.IsMSRFeeCoveredByInsurance && + msrFeeLineItem && + this.msrFeeAmount > 0; + + if (shouldCreateMsrFeeCartItem) { + cartItem = { + name: this.msrFeeCartItemName, + category: cartItemCategories.SUPPORTING_ITEMS, + cartItemType: cartItemTypes.MOBILE_FEE, + isDisplayed: this.isInsurance, + isRemovable: true, + subTotal: 0, + salesTax: 0, + lineItems: [], + isCoveredByInsurance: this.IsMSRFeeCoveredByInsurance, + }; + msrFeeLineItem.cartItemType = cartItem.cartItemType; + cartItem.lineItems.push(msrFeeLineItem); + cartItem.subTotal += this.msrFeeAmount; + cartItem.salesTax += msrFeeLineItem.salesTax ?? 0; + } + return cartItem; + }, + msrFeeAmount() { + const msrFeeLineItem = this.getMsrFeeLineItem; + if (!msrFeeLineItem) { + return 0; + } + return ( + msrFeeLineItem.laborAmount + msrFeeLineItem.sellingPrice + msrFeeLineItem.kitPrice + ); + }, + msrModalTextBlock() { + let cmsContentText = this.getCmsContent("MSRModal", "BodyText"); + if (cmsContentText) { + cmsContentText = cmsContentText.replaceAll( + "{custom:mobileFee}", + formatToUSDollar(this.msrFeeAmount) + ); + cmsContentText = cmsContentText.replaceAll( + "{custom:submittedOrder.policy.insuranceCompanyName}", + this.insuranceCompanyName + ); + } + return cmsContentText; + }, quotePageDiscountCartItemName() { return this.getCmsContent("ServicePackageDiscountTextWidget", "Text"); }, @@ -1189,6 +1288,7 @@ export default { textLink, contentGroupModal, promoModalQuestion, + removeMsrFeeModal, }, }; @@ -1289,6 +1389,10 @@ export default { order: 3; margin-right: 100%; text-decoration: none; + + &#msr-fee-cartItem { + margin: 0 0 0 0.2rem; + } } } .package-type, diff --git a/src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue b/src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue new file mode 100644 index 000000000..6c2cd1fb6 --- /dev/null +++ b/src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue @@ -0,0 +1,153 @@ + + + + diff --git a/src/fmg-components/content-group-modal/content-group-modal.vue b/src/fmg-components/content-group-modal/content-group-modal.vue index 25c50b2df..e4a5241c4 100644 --- a/src/fmg-components/content-group-modal/content-group-modal.vue +++ b/src/fmg-components/content-group-modal/content-group-modal.vue @@ -26,6 +26,7 @@ export default { name: "content-group-modal", props: { cmsWidgetName: String, + customBodyText: String, footerButtonActionName: { type: String, default: null, @@ -43,6 +44,9 @@ export default { return this.getCmsContent(this.cmsWidgetName, "SubheaderText"); }, ModalBodyText() { + if (this.customBodyText) { + return this.customBodyText; + } return this.getCmsContent(this.cmsWidgetName, "BodyText"); }, ModalSubBodyText() { diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js index 68de04515..9564b5c1f 100644 --- a/src/helpers/pricing-helper.js +++ b/src/helpers/pricing-helper.js @@ -2,6 +2,7 @@ import store from "@/store"; import { storeActions } from "@/constants/store-actions"; import baseMixin from "@/mixins/base-mixin.js"; import { deepClone } from "@/helpers/object-helper"; +import { partNumberStrings } from "@/constants/part-number-strings"; export function getDisplayAmountDue(lineItemsObject, includeTax = true) { return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", { @@ -32,6 +33,10 @@ export function getAmountDue(lineItemsObject, includeTax = true) { if (store.getters.coverageIsVerified && !order.policy.isNoComp && !order.policy.isItac) { amountDue = order.policy.currentDeductible; + + if (order.isMSRFeeApplicable && !order.isMSRFeeCoveredByInsurance) { + amountDue += getMSRFeePartPrice(lineItemsObject?.supportingItems, includeTax); + } } if (lineItemsObject?.vaps) { @@ -100,6 +105,20 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) { return pricingResults[0]; } +export function getMSRFeePartPrice(supportingItems, includeTax) { + let msrFeePrice = 0; + const msrFeeLineItem = supportingItems?.find( + (lineItem) => lineItem.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE + ); + if (msrFeeLineItem) { + msrFeePrice = baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts( + [msrFeeLineItem], + includeTax + ); + } + return msrFeePrice; +} + export function addPricesToLineItems(lineItems, pricingLineItems) { lineItems.forEach((lineItem) => { const lineItemIndex = pricingLineItems.findIndex( diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index fa0e53425..e26c5f9b1 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -62,6 +62,7 @@ :showAsPaid="isPia" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" @@ -70,7 +71,8 @@ :isItac="isItac" :isNoComp="isNoComp" :isExpandedOnLoad="false" - :isMSRFeeApplicable="isMSRFeeApplicable" /> + :isMSRFeeApplicable="isMSRFeeApplicable" + :IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance" />
@@ -419,6 +421,9 @@ export default { isMSRFeeApplicable() { return this.submittedOrder?.isMSRFeeApplicable; }, + isMSRFeeCoveredByInsurance() { + return this.submittedOrder?.isMSRFeeCoveredByInsurance; + }, isInsurance() { return this.submittedOrder?.payment?.isInsurance; }, diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 4a4936a7f..693263ebb 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -29,6 +29,7 @@ v-model="lineItems" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 48f076f84..e5b08bdbc 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -26,6 +26,7 @@ pageName="payment-method" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" @@ -35,7 +36,9 @@ :isNoComp="isNoComp" :isExpandedOnLoad="false" :isMSRFeeApplicable="isMSRFeeApplicable" - @itemRemoved="evaluatePromosAndTaxItemsOnOrder" /> + :IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance" + @itemRemoved="evaluatePromosAndTaxItemsOnOrder" + @switchToInshop="navigateToSchedulePage" /> { isRecalAckOptIn: false, isRecalAcknowledgedForScheduling: "", isMSRFeeApplicable: false, + isMSRFeeCoveredByInsurance: false, }, applicationUser: { eventBus: [], @@ -359,6 +360,9 @@ export const mutations = { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { state.order.isMSRFeeApplicable = isMSRFeeApplicable; }, + updateIsMSRFeeCoveredByInsurance(state, isMSRFeeCoveredByInsurance) { + state.order.isMSRFeeCoveredByInsurance = isMSRFeeCoveredByInsurance; + }, updateCashPriceSubTotal(state, cashPriceSubTotal) { state.order.cashPriceSubTotal = cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString(); @@ -2611,6 +2615,7 @@ export const actions = { lockToken: order.lockToken, isRecalAckOptIn: order.isRecalAckOptIn, isMSRFeeApplicable: order.isMSRFeeApplicable, + isMSRFeeCoveredByInsurance: order.isMSRFeeCoveredByInsurance, cashPriceSubTotal: order.cashPriceSubTotal, }, }, @@ -3043,6 +3048,13 @@ export const actions = { context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable); }, + saveIsMSRFeeCoveredByInsurance(context, isMSRFeeCoveredByInsurance) { + context.commit( + storeMutations.UPDATE_IS_MSR_FEE_COVERED_BY_INSURANCE, + isMSRFeeCoveredByInsurance + ); + }, + saveParentAccountNumber(context, parentAccountNumber) { context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber); }, From 4c82d807999785e160b5be66b4000387af8547b6 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Fri, 27 Mar 2026 08:12:15 -0400 Subject: [PATCH 06/29] CASH-1689: Prettier --- src/mixins/analytics-mixin.js | 6 ++++-- src/store/index.js | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 9386846d7..3ab575f58 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -165,7 +165,7 @@ export default { if (!currentPageName) { return; } - + const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.getters.order; @@ -934,7 +934,9 @@ function getPageNameFromRouter(useDefaultUrl = false) { return router.currentRoute.value.name; } - return useDefaultUrl ? window.location.search : window.location.href.replace(/\/$/, "").split("/").pop(); + return useDefaultUrl + ? window.location.search + : window.location.href.replace(/\/$/, "").split("/").pop(); } function getValueToLog(value, valueToLogType) { diff --git a/src/store/index.js b/src/store/index.js index d76a0e0da..94128a402 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1634,7 +1634,6 @@ export const actions = { }); }, - logFmgSessionData( context, { From 2f71aa32f2641cc1c4dd36ff627b6aea5b837c87 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Fri, 27 Mar 2026 14:53:57 -0400 Subject: [PATCH 07/29] CASH-1689: Add feature toggle for MSR Split pay --- src/constants/experiments.js | 1 + src/fmg-components/cart/cart.vue | 1 + src/layouts/payment-adyen/payment-adyen.vue | 4 ++++ src/layouts/payment/payment.vue | 4 ++++ src/layouts/schedule/schedule.vue | 14 ++++++++++++-- 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index b9f38ca36..97e049df0 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -23,6 +23,7 @@ const experimentSettings = { INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL: "NextGen_InternalInsuranceTabDisplayThreshold", INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL: "NextGen_ExternalInsuranceTabDisplayThreshold", DISPLAY_MSR: "DisplayMSR", + ENABLE_MSR_SPLIT_PAY: "EnableMSRSplitPay", DISPLAY_WAITLIST: "DisplayWaitlist2.0", WAITLIST_THRESHOLD_DAYS: "Waitlist_Threshold_Days", PRICING_BY_DAY: "DisplayPricingByDay", diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 7de87b678..f4626caa5 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -981,6 +981,7 @@ export default { let cartItem = null; const msrFeeLineItem = this.getMsrFeeLineItem; const shouldCreateMsrFeeCartItem = + this.isMSRFeeApplicable && this.isInsurance && !this.IsMSRFeeCoveredByInsurance && msrFeeLineItem && diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 693263ebb..bcb827623 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -34,6 +34,7 @@ :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" + :isMSRFeeApplicable="isMSRFeeApplicable" :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> @@ -569,6 +570,9 @@ export default { insuranceCompanyName() { return this.$store.getters.policy.insuranceCompanyName; }, + isMSRFeeApplicable() { + return this.$store.getters.order.isMSRFeeApplicable; + }, lineItems() { return deepClone(this.$store.getters.lineItems); }, diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 68e57ba9a..739376d94 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -52,6 +52,7 @@ :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" + :isMSRFeeApplicable="isMSRFeeApplicable" :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> @@ -411,6 +412,9 @@ export default { insuranceCompanyName() { return this.$store.getters.policy.insuranceCompanyName; }, + isMSRFeeApplicable() { + return this.$store.getters.order.isMSRFeeApplicable; + }, isPaypal() { if (this.paymentType == "pp") { return true; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 967ea1828..8068ac6c1 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -704,8 +704,11 @@ export default { }, isMobileStaticRecalibrationApplicable() { return ( - this.displayMSR && - this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE + (this.displayMSR && + this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE && + this.enableMSRSplitPay) || + this.isCashItacNoComp || + this.mobileFeePart?.isInsurable ); }, displayMSR() { @@ -715,6 +718,13 @@ export default { ?.toLowerCase() === "true" ); }, + enableMSRSplitPay() { + return ( + experimentMixin.methods + .getSettingValue(experimentSettings.ENABLE_MSR_SPLIT_PAY) + ?.toLowerCase() === "true" + ); + }, isMSRFeeNotCoveredByInsurance() { return ( this.isMobileSelected && From 0b7d0a9083a6f7b28b2dc39976b3b3b8e8f90b9b Mon Sep 17 00:00:00 2001 From: CarlNation Date: Fri, 27 Mar 2026 23:14:16 -0400 Subject: [PATCH 08/29] CASH-2481 add null check to fix error involving external parameter CASH-2481 add null check to fix error involving external parameter --- src/layouts/vehicle/vehicle.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index 92a7aa810..ac4a54f62 100644 --- a/src/layouts/vehicle/vehicle.vue +++ b/src/layouts/vehicle/vehicle.vue @@ -253,17 +253,17 @@ export default { const matchingMake = resultMap.makeQuestionInitialData?.filter( (item) => item.toLowerCase() === - store.getters.externalParameterVehicle.make.toLowerCase() + store.getters.externalParameterVehicle.make?.toLowerCase() ); const matchingModel = resultMap.modelQuestionInitialData?.filter( (item) => item.toLowerCase() === - store.getters.externalParameterVehicle.model.toLowerCase() + store.getters.externalParameterVehicle.model?.toLowerCase() ); const matchingStyle = resultMap.styleQuestionInitialData?.filter( (item) => item.toLowerCase() === - store.getters.externalParameterVehicle.style.toLowerCase() + store.getters.externalParameterVehicle.style?.toLowerCase() ); baseMixin.methods.dispatchStoreAction( storeActions.UPDATE_EXTERNAL_PARAMETER_MMS, From 2391748d6059e5231439e9cf7556c3e7c60d64ee Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 30 Mar 2026 14:39:30 -0400 Subject: [PATCH 09/29] Always set vm.isInsuranceSelected Some cases were not setting it if external parameter state was marked 0 or 1 --- src/layouts/quote/quote.vue | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index b428ab905..339e543df 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -583,6 +583,8 @@ export default { debugLog("--- quote.vue thresholds end ---"); } + let setInsuranceSelected = true; + if (isExternalParameter === externalParameterStatus.NOT_SET) { // prettier-ignore { @@ -590,12 +592,6 @@ export default { } // there are no active or inactive external parameters; use internal threshold if (internalThreshold) thresholdToUse = internalThreshold; - - vm.isInsuranceSelected = getIsInsuranceSelectedValue( - vm.availableLineItems, - thresholdToUse - ); - baseMixin.methods.ResetExternalParamsAndHideModal(); } else { // user came from an external source, however, the externalParms may have been reset on service-zip, property-questions, etc... if (getBoolFromString(store.getters.externalParameterQuote?.isInsurance)) { @@ -614,7 +610,9 @@ export default { await vm.skip(insuranceSelection, servicePackage); } - baseMixin.methods.ResetExternalParamsAndHideModal(); + if (vm.isInsuranceSelected != null) { + setInsuranceSelected = false; + } } else { if (externalThreshold) thresholdToUse = externalThreshold; @@ -622,13 +620,16 @@ export default { { debugLog("quote.vue tab select NOT EXTERNAL"); } + } + //Set isInsuranceSelected if + if (setInsuranceSelected) { vm.isInsuranceSelected = getIsInsuranceSelectedValue( vm.availableLineItems, thresholdToUse ); - baseMixin.methods.ResetExternalParamsAndHideModal(); } + baseMixin.methods.ResetExternalParamsAndHideModal(); } if (vm.skipToInsurance && !vm.showSaveProgressPopup) { From 849443fa1c849b946720a1b7809aaac4e9ae08b6 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 30 Mar 2026 15:42:33 -0400 Subject: [PATCH 10/29] simplify logic -> set isInsuranceSelected if its null/undefined --- src/layouts/quote/quote.vue | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 339e543df..106c04bfa 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -583,8 +583,6 @@ export default { debugLog("--- quote.vue thresholds end ---"); } - let setInsuranceSelected = true; - if (isExternalParameter === externalParameterStatus.NOT_SET) { // prettier-ignore { @@ -609,10 +607,6 @@ export default { ) { await vm.skip(insuranceSelection, servicePackage); } - - if (vm.isInsuranceSelected != null) { - setInsuranceSelected = false; - } } else { if (externalThreshold) thresholdToUse = externalThreshold; @@ -621,17 +615,16 @@ export default { debugLog("quote.vue tab select NOT EXTERNAL"); } } - - //Set isInsuranceSelected if - if (setInsuranceSelected) { - vm.isInsuranceSelected = getIsInsuranceSelectedValue( - vm.availableLineItems, - thresholdToUse - ); - } - baseMixin.methods.ResetExternalParamsAndHideModal(); } + if (vm.isInsuranceSelected == null) { + vm.isInsuranceSelected = getIsInsuranceSelectedValue( + vm.availableLineItems, + thresholdToUse + ); + } + baseMixin.methods.ResetExternalParamsAndHideModal(); + if (vm.skipToInsurance && !vm.showSaveProgressPopup) { // skip ahead only if no email popup. await vm.skip(true, externalParamPackageLabels.GLASS_ONLY); From 94678ed7ba922c8a91a92ff9de00fa55de6f0c66 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Tue, 31 Mar 2026 12:57:19 -0400 Subject: [PATCH 11/29] CASH-2491 pass querystring on init to send to logapp CASH-2491 pass querystring on init to send to logapp --- src/store/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/store/index.js b/src/store/index.js index 94128a402..023466b89 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1597,6 +1597,7 @@ export const actions = { operatorId: "WEB", userName: "SafeliteConceptFunnel", referrer: referrer, + clientData: window.location.search, }; return globalMethods From 485c63e01775caa75256ae63947f70539b7c90d0 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 1 Apr 2026 11:47:58 -0400 Subject: [PATCH 12/29] Revert "CASH-623 revert" --- src/store/index.js | 41 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/src/store/index.js b/src/store/index.js index 10218892c..e91ec6c0b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2014,52 +2014,27 @@ export const actions = { }, getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) { - const escapeRecalibrationType = (rt) => rt.split("&").join("%26"); - - const partialLineItemsObjects = context.getters.order.lineItems.glassParts?.map( - (lineItem) => ({ - partNumber: lineItem.partNumber, - recalibrationType: - lineItem.recalibrationType && isRecalPartOrHasChildRecalPart(lineItem) - ? escapeRecalibrationType(lineItem.recalibrationType) - : undefined, - }) + const flattenedLineItemsWithChildParts = getFlattenedArrayOfLineItemsWithChildParts( + context.getters.order.lineItems.glassParts ); - var lineItems = null; - if (partialLineItemsObjects) { - lineItems = buildQueryStringParameterFromArrayOfComplexObjects( - partialLineItemsObjects, - "lineItems" - ); - } + const lineItemsList = flattenedLineItemsWithChildParts + .map((item) => item.partNumber) + .join(","); const vehicle = context.getters.vehicle; const carId = vehicle.carId; const damage = context.getters.damage; - const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace); - - const glassPieces = buildQueryStringParameterFromArrayOfComplexObjects( - glassArray, - "glassPieces" - ); const parentAccountNumber = context.getters.order.payment.parentAccountNumber ?? applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; - const referralSequenceNumber = context.getters.order.referralSequenceNumber; - var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&parentAccountNumber=${parentAccountNumber}&referralSequenceNumber=${referralSequenceNumber}&applicationName=${applicationConfig.ANALYTICS_APPLICATION_NAME}`; - if (lineItems) { - endPoint += `&${lineItems}`; + var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&parentAccountNumber=${parentAccountNumber}&isRepair=${damage.isRepair}`; + if (lineItemsList) { + endPoint += `&lineItems=${lineItemsList}`; } - if (glassPieces) { - endPoint += `&${glassPieces}`; - } - - endPoint += `&isHeavyTruckVehicle=${context.getters.order.vehicle.isBigTruck}`; - return globalMethods.callHttpClient({ method: endpoints.GetServiceabilityDetails.method, endpoint: endPoint, From a4e853e528799f38defcbdf950745bcfae9dc13a Mon Sep 17 00:00:00 2001 From: CarlNation Date: Wed, 1 Apr 2026 13:49:06 -0400 Subject: [PATCH 13/29] CASH-2508 check externalparameterstate correctly CASH-2508 check externalparameterstate correctly. It should check more than the flag because parms can be optional. So make sure we're checking damage options stored in external params so the continue button is properly enabled --- src/layouts/vehicle-damage/vehicle-damage.vue | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index abad0da7d..e93e0fbd0 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -100,6 +100,7 @@ import store from "@/store"; import baseMixin from "@/mixins/base-mixin"; import { nextTick } from "vue"; import { getBoolFromString } from "@/helpers/boolean-helper"; +import { externalParameterStatus } from "@/constants/external-parameters"; // DEFINE VALIDATION RULES defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED)); @@ -143,7 +144,9 @@ export default { vm.$refs.backGlassOptions.initializeComponent( resultMap.damageOptions.backGlassOptions.availableReplacementOptions ); - if (store.getters.externalParameterState?.isExternalParameter) { + if (store.getters.externalParameterState?.isExternalParameter && + store.getters.externalParameterState?.isExternalParameter === externalParameterStatus.ACTIVE && + store.getters.externalParameterState.vehicleDamage?.damageType) { await nextTick(); const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); debugLog(`**** vehicle-damage is form valid: ${isValid} ****`); @@ -355,7 +358,9 @@ export default { ) { windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPLACE; - if (!store.getters.externalParameterState.isExternalParameter) { + if (!store.getters.externalParameterState.isExternalParameter || + store.getters.externalParameterState?.isExternalParameter !== externalParameterStatus.ACTIVE || + !store.getters.externalParameterState.vehicleDamage?.damageType) { windShieldOptions.selectedWindshieldReplaceOptions.push( damageLocationsSelected.SINGLE ); @@ -666,12 +671,20 @@ export default { return this.$store.getters.requiresVerifiedRedirecting; }, isContinueEnabled() { - return ( + var isEnabled = ( this.isAnyDamageSelected && this.isWindshieldValid && this.isRearGlassValid && this.isSideGlassValid ); + if (!isEnabled) { + debugLog(`************************ isContinueEnabled: ${isEnabled} *****************************`); + debugLog(`**** this.isAnyDamageSelected: ${this.isAnyDamageSelected} ****`); + debugLog(`**** this.isWindshieldValid: ${this.isWindshieldValid} ****`); + debugLog(`**** this.isRearGlassValid: ${this.isRearGlassValid} ****`); + debugLog(`**** this.isSideGlassValid: ${this.isSideGlassValid} ****`); + } + return isEnabled; }, isAnyDamageSelected() { return this.selectedDamageLocations != null && this.selectedDamageLocations.length != 0; From a33149c621c5748515c7cc4246b92d42b5d7b4b7 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Wed, 1 Apr 2026 14:10:48 -0400 Subject: [PATCH 14/29] prettier --- src/layouts/vehicle-damage/vehicle-damage.vue | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index e93e0fbd0..ee8d3e074 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -144,9 +144,12 @@ export default { vm.$refs.backGlassOptions.initializeComponent( resultMap.damageOptions.backGlassOptions.availableReplacementOptions ); - if (store.getters.externalParameterState?.isExternalParameter && - store.getters.externalParameterState?.isExternalParameter === externalParameterStatus.ACTIVE && - store.getters.externalParameterState.vehicleDamage?.damageType) { + if ( + store.getters.externalParameterState?.isExternalParameter && + store.getters.externalParameterState?.isExternalParameter === + externalParameterStatus.ACTIVE && + store.getters.externalParameterState.vehicleDamage?.damageType + ) { await nextTick(); const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); debugLog(`**** vehicle-damage is form valid: ${isValid} ****`); @@ -358,9 +361,12 @@ export default { ) { windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPLACE; - if (!store.getters.externalParameterState.isExternalParameter || - store.getters.externalParameterState?.isExternalParameter !== externalParameterStatus.ACTIVE || - !store.getters.externalParameterState.vehicleDamage?.damageType) { + if ( + !store.getters.externalParameterState.isExternalParameter || + store.getters.externalParameterState?.isExternalParameter !== + externalParameterStatus.ACTIVE || + !store.getters.externalParameterState.vehicleDamage?.damageType + ) { windShieldOptions.selectedWindshieldReplaceOptions.push( damageLocationsSelected.SINGLE ); @@ -671,14 +677,15 @@ export default { return this.$store.getters.requiresVerifiedRedirecting; }, isContinueEnabled() { - var isEnabled = ( + var isEnabled = this.isAnyDamageSelected && this.isWindshieldValid && this.isRearGlassValid && - this.isSideGlassValid - ); + this.isSideGlassValid; if (!isEnabled) { - debugLog(`************************ isContinueEnabled: ${isEnabled} *****************************`); + debugLog( + `************************ isContinueEnabled: ${isEnabled} *****************************` + ); debugLog(`**** this.isAnyDamageSelected: ${this.isAnyDamageSelected} ****`); debugLog(`**** this.isWindshieldValid: ${this.isWindshieldValid} ****`); debugLog(`**** this.isRearGlassValid: ${this.isRearGlassValid} ****`); From b7dcd92c9e0e23971c760f5fa48ca2e869156307 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Wed, 1 Apr 2026 15:20:49 -0400 Subject: [PATCH 15/29] CASH-2508 tests CASH-2508 tests --- src/layouts/vehicle-damage/vehicle-damage.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/layouts/vehicle-damage/vehicle-damage.spec.js b/src/layouts/vehicle-damage/vehicle-damage.spec.js index 7ea899fda..3b8e46d22 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.spec.js +++ b/src/layouts/vehicle-damage/vehicle-damage.spec.js @@ -17,6 +17,7 @@ import store from "@/store"; import { validate } from "vee-validate"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import * as cookieHelper from "@/helpers/heritage-integration/cookie-helper"; +import { externalParameterStatus } from "@/constants/external-parameters"; // Mock our module for promises. jest.mock("@/helpers/layout-helper.js", () => ({ @@ -817,7 +818,11 @@ describe("vehicle-damage.vue", () => { describe("vehicle-damage.vue", () => { test("should call forwardButtonAction if isExternalParameter is true and form is valid", async () => { // Set up the store with isExternalParameter as true - store.getters.externalParameterState = { isExternalParameter: true }; + store.getters.externalParameterState = { + isExternalParameter: true, + isExternalParameter: externalParameterStatus.ACTIVE, + vehicleDamage: { damageType: "windshieldReplace" }, + }; store.getters.externalParameterDamage = { damageType: "windshieldReplace", isRepair: false, From 59d9921dae6327ddf11d7a0d0058d0e361d7d1eb Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 2 Apr 2026 15:22:42 -0400 Subject: [PATCH 16/29] CASH-1684: Alert message UI updates --- src/layouts/schedule/schedule.vue | 1 + src/ux-components/alert/alert.vue | 38 +++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 8068ac6c1..f5462fb9e 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -85,6 +85,7 @@ ref="alertMSRNotCoveredByInsurance" class="mt-5 mb-5" v-if="isMSRFeeNotCoveredByInsurance" + :leftAlignCopy="true" :manualHeadline="MSRFeeAlertHeadlineText" :manualCopy="MSRFeeAlertBodyText" :showWarningIcon="true" diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue index deb7bca8b..2ea618f9a 100644 --- a/src/ux-components/alert/alert.vue +++ b/src/ux-components/alert/alert.vue @@ -1,6 +1,6 @@ @@ -177,9 +165,9 @@ // Components import textLink from "@/ux-components/text-link/text-link"; import textBlock from "@/digital-components/text-block/text-block"; -import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question"; import removeMsrFeeModal from "./remove-msr-fee-modal/remove-msr-fee-modal.vue"; +import toolTip from "@/ux-components/tool-tip/tool-tip.vue"; // Mixins import baseMixin from "@/mixins/base-mixin.js"; @@ -1015,7 +1003,10 @@ export default { msrFeeLineItem.laborAmount + msrFeeLineItem.sellingPrice + msrFeeLineItem.kitPrice ); }, - msrModalTextBlock() { + recyclingToolTipBodyText() { + return this.getCmsContent("RecycleTextBlock", "Text"); + }, + msrToolTipBodyText() { let cmsContentText = this.getCmsContent("MSRModal", "BodyText"); if (cmsContentText) { cmsContentText = cmsContentText.replaceAll( @@ -1287,9 +1278,9 @@ export default { components: { textBlock, textLink, - contentGroupModal, promoModalQuestion, removeMsrFeeModal, + toolTip, }, }; diff --git a/src/ux-components/tool-tip/tool-tip.spec.js b/src/ux-components/tool-tip/tool-tip.spec.js new file mode 100644 index 000000000..1e4cca40e --- /dev/null +++ b/src/ux-components/tool-tip/tool-tip.spec.js @@ -0,0 +1,121 @@ +import { shallowMount, mount } from "@vue/test-utils"; +import toolTip from "@/ux-components/tool-tip/tool-tip.vue"; + +const mockMixin = { + methods: { + getCmsContent: jest.fn(() => "Mock CMS Content"), + }, +}; + +function setupMocks(mountOptionsMockData = {}) { + const defaultMountOptions = { + propsData: { + cmsWidgetName: "testWidget", + }, + mixins: [mockMixin], + }; + return { ...defaultMountOptions, ...mountOptionsMockData }; +} + +describe("toolTip.vue", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should not show tooltip initially", () => { + // Arrange + const wrapper = shallowMount(toolTip, setupMocks()); + + // Assert + expect(wrapper.vm.isVisible).toBe(false); + expect(wrapper.find(".tooltip").exists()).toBe(false); + }); + + it("should show tooltip when icon is clicked", async () => { + // Arrange + const wrapper = shallowMount(toolTip, setupMocks()); + const icon = wrapper.find(".tooltip-icon"); + + // Act + await icon.trigger("click"); + + // Assert + expect(wrapper.vm.isVisible).toBe(true); + expect(wrapper.find(".tooltip").exists()).toBe(true); + }); + + it("should hide tooltip when clicked outside", async () => { + // Arrange + const wrapper = shallowMount(toolTip, setupMocks()); + const icon = wrapper.find(".tooltip-icon"); + + // Show tooltip first + await icon.trigger("click"); + expect(wrapper.vm.isVisible).toBe(true); + + // Act - simulate click outside + const clickEvent = new Event("click"); + Object.defineProperty(clickEvent, "target", { value: document.body }); + document.dispatchEvent(clickEvent); + + // Wait for next tick to allow event handling + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.isVisible).toBe(false); + expect(wrapper.find(".tooltip").exists()).toBe(false); + }); + + it("should toggle tooltip visibility when icon is clicked multiple times", async () => { + // Arrange + const wrapper = shallowMount(toolTip, setupMocks()); + const icon = wrapper.find(".tooltip-icon"); + + // Act & Assert - First click shows + await icon.trigger("click"); + expect(wrapper.vm.isVisible).toBe(true); + + // Second click hides + await icon.trigger("click"); + expect(wrapper.vm.isVisible).toBe(false); + + // Third click shows again + await icon.trigger("click"); + expect(wrapper.vm.isVisible).toBe(true); + }); + + it("should not hide tooltip when clicking inside the tooltip container", async () => { + // Arrange + const wrapper = shallowMount(toolTip, setupMocks()); + const icon = wrapper.find(".tooltip-icon"); + + // Show tooltip first + await icon.trigger("click"); + expect(wrapper.vm.isVisible).toBe(true); + + // Act - simulate click inside container + const container = wrapper.find(".tooltip-container").element; + const clickEvent = new Event("click"); + Object.defineProperty(clickEvent, "target", { value: container }); + document.dispatchEvent(clickEvent); + + // Wait for next tick + await wrapper.vm.$nextTick(); + + // Assert - tooltip should still be visible + expect(wrapper.vm.isVisible).toBe(true); + }); + + it("should calculate lineItemContainerLength based on parent element width", () => { + // Arrange + const wrapper = mount(toolTip, setupMocks()); + const parentDiv = document.createElement("div"); + Object.defineProperty(parentDiv, "offsetWidth", { value: 300, writable: true }); + + const container = wrapper.vm.$refs.container; + Object.defineProperty(container, "parentElement", { value: parentDiv, writable: true }); + + // Act & Assert + expect(wrapper.vm.lineItemContainerLength).toBe(-300); + }); +}); diff --git a/src/ux-components/tool-tip/tool-tip.vue b/src/ux-components/tool-tip/tool-tip.vue new file mode 100644 index 000000000..648c5adbd --- /dev/null +++ b/src/ux-components/tool-tip/tool-tip.vue @@ -0,0 +1,140 @@ + + + + + From 7c024990562e66948f9382ebe5eb2d0f20c6d736 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Tue, 7 Apr 2026 16:14:34 -0400 Subject: [PATCH 22/29] CASH-2521, CASH-2526 - Handle infinite loop and add logging --- jest.config.js | 1 + src/App.vue | 10 ++ src/constants/session-storage.js | 2 + .../heritage-integration/navigation-helper.js | 4 +- src/router/methods/before-each.js | 38 +++- src/router/methods/error.js | 11 +- src/router/methods/error.spec.js | 168 ++++++++++++++++++ src/router/methods/route-logic/heritage.js | 1 + 8 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 src/router/methods/error.spec.js diff --git a/jest.config.js b/jest.config.js index 6c0addf5f..a8063f324 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,6 +15,7 @@ module.exports = { "!src/main.js", "!src/constants/*.js", "!src/router/**/*.js", + "src/router/methods/error.js", // Re-include after the exclusion above (Jest applies later positive globs as overrides). "!src/helpers/unit-test-helper.js", "!src/helpers/logger.js", "!src/layouts/vehicle-damage/windshield-options/windshield-options.vue", diff --git a/src/App.vue b/src/App.vue index cae31dd24..2b9bc5a33 100644 --- a/src/App.vue +++ b/src/App.vue @@ -64,6 +64,16 @@ window.onerror = (msg, url, line, col, error) => { var suppressErrorAlert = true; return suppressErrorAlert; }; + +// Log Promise rejections that are never handled (.catch / await try/catch), e.g. fire-and-forget async. +window.addEventListener("unhandledrejection", (event) => { + const reason = event.reason; + const message = reason instanceof Error ? reason.message : String(reason); + global.$logger.logError(`unhandledrejection: ${message}`, { + stack: reason instanceof Error ? reason.stack : undefined, + reason: reason instanceof Error ? undefined : reason, + }); +});