From 22cba2d54b435a9d1f575a82d18da39db201624f Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 25 Mar 2026 10:57:59 -0400 Subject: [PATCH 01/10] 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/10] 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 d1ebb59cbb1132870138e42458df7c31d364fb87 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Thu, 26 Mar 2026 13:55:27 -0400 Subject: [PATCH 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 485c63e01775caa75256ae63947f70539b7c90d0 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 1 Apr 2026 11:47:58 -0400 Subject: [PATCH 10/10] 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,