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, + }); +}); 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/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/helpers/heritage-integration/navigation-helper.js b/src/helpers/heritage-integration/navigation-helper.js index 539d36648..a2b4452d5 100644 --- a/src/helpers/heritage-integration/navigation-helper.js +++ b/src/helpers/heritage-integration/navigation-helper.js @@ -27,7 +27,7 @@ export async function navigateToHeritageFunnel({ shouldSaveSession, pageNameToLo var heritageParms = { corid: store.getters.order.referralCorrelationId, src: "concept-funnel", - conceptsqid: store.getters.applicationUser.savedSessionId, + conceptsqid: store.getters.applicationUser.savedSessionId ?? "", isInsurance: store.getters.payment.isInsurance, }; 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/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/payment-adyen/payment-adyen.spec.js b/src/layouts/payment-adyen/payment-adyen.spec.js index 26d3a6aec..1a506b860 100644 --- a/src/layouts/payment-adyen/payment-adyen.spec.js +++ b/src/layouts/payment-adyen/payment-adyen.spec.js @@ -187,6 +187,118 @@ describe("payment-adyen.vue", () => { expect(result).toBe(true); }); }); + + describe("splitStreetAddress", () => { + const setInShopStreet = (streetAddress) => { + store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; + store.getters.order.serviceLocation.provider.address.streetAddress = streetAddress; + }; + + test("splits typical address into first token as number and remainder as street", () => { + setInShopStreet("123 West Elm St"); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "123", + street: "West Elm St", + }); + }); + + test("two tokens: number and single-word street", () => { + setInShopStreet("456 Oak"); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "456", + street: "Oak", + }); + }); + + test("single token with no spaces: full string in number, empty street", () => { + setInShopStreet("123OakStreet"); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "123OakStreet", + street: "", + }); + }); + + test("collapses multiple spaces between words", () => { + setInShopStreet("123 West Elm St"); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "123", + street: "West Elm St", + }); + }); + + test("trims leading and trailing whitespace on full address", () => { + setInShopStreet(" 99 Maple Ave "); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "99", + street: "Maple Ave", + }); + }); + + test("empty string yields empty number and street", () => { + setInShopStreet(""); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "", + street: "", + }); + }); + + test("whitespace-only address yields empty number and street", () => { + setInShopStreet(" \t "); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "", + street: "", + }); + }); + + test("null address yields empty number and street", () => { + setInShopStreet(null); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "", + street: "", + }); + }); + + test("uses mobile service address when appointment is mobile", () => { + store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; + store.getters.order.serviceLocation.address = "700 Broadway Blvd"; + store.getters.order.serviceLocation.city = "Columbus"; + store.getters.order.serviceLocation.state = "OH"; + store.getters.order.serviceLocation.zipCode = "43235"; + + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "700", + street: "Broadway Blvd", + }); + }); + + test("splits on tabs and other whitespace runs", () => { + setInShopStreet("10\tPine\tLane"); + const wrapper = setupMocks({}); + + expect(wrapper.vm.splitStreetAddress).toEqual({ + number: "10", + street: "Pine Lane", + }); + }); + }); }); function setupMocks({ customMountOptions } = {}) { diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 4a4936a7f..aa75487da 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -29,10 +29,12 @@ v-model="lineItems" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" + :isMSRFeeApplicable="isMSRFeeApplicable" :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> @@ -514,15 +516,14 @@ export default { }, splitStreetAddress() { - const address = this.locationInfo.address; - const tokens = address.split(" ") ?? [""]; - const number = tokens[0]; - - const street = tokens.slice(1).reduce((prev, next) => `${prev} ${next}`); + const address = (this.locationInfo?.address ?? "").trim(); + const tokens = address.length ? address.split(/\s+/) : [""]; + const number = tokens[0] ?? ""; + const street = tokens.length > 1 ? tokens.slice(1).join(" ") : ""; return { - number: number ?? "", - street: street ?? "", + number, + street, }; }, @@ -568,6 +569,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-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" /> @@ -410,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/quote/quote.vue b/src/layouts/quote/quote.vue index b428ab905..106c04bfa 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -590,12 +590,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)) { @@ -613,8 +607,6 @@ export default { ) { await vm.skip(insuranceSelection, servicePackage); } - - baseMixin.methods.ResetExternalParamsAndHideModal(); } else { if (externalThreshold) thresholdToUse = externalThreshold; @@ -622,15 +614,17 @@ export default { { debugLog("quote.vue tab select NOT EXTERNAL"); } - - 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); diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ec8051cc4..c483c90df 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" @@ -705,7 +706,8 @@ export default { isMobileStaticRecalibrationApplicable() { return ( this.displayMSR && - this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE + this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE && + (this.enableMSRSplitPay || this.isCashItacNoComp || this.mobileFeePart?.isInsurable) ); }, displayMSR() { @@ -715,6 +717,13 @@ export default { ?.toLowerCase() === "true" ); }, + enableMSRSplitPay() { + return ( + experimentMixin.methods + .getSettingValue(experimentSettings.ENABLE_MSR_SPLIT_PAY) + ?.toLowerCase() === "true" + ); + }, isMSRFeeNotCoveredByInsurance() { return ( this.isMobileSelected && @@ -1173,6 +1182,13 @@ export default { isMSRFeeApplicable ); }, + updateAndSaveIsMSRFeeCoveredByInsurance() { + const isMSRFeeCoveredByInsurance = this.mobileFeePart?.isInsurable; + this.dispatchStoreAction( + this.storeActions.SAVE_IS_MSR_FEE_COVERED_BY_INSURANCE, + isMSRFeeCoveredByInsurance + ); + }, updateAndSaveSupportingItems() { let supportingItems = store.getters.lineItems.supportingItems; let shouldSaveSupportingItems = false; @@ -1528,6 +1544,7 @@ export default { this.updateAndSaveSupportingItems(); this.updateAndSaveIsMSRFeeApplicable(); + this.updateAndSaveIsMSRFeeCoveredByInsurance(); this.updateSupportingItems(); 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.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, diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index 5e128e867..e68598bd4 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -93,12 +93,14 @@ 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"; 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)); @@ -142,7 +144,12 @@ 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} ****`); @@ -199,7 +206,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 +216,7 @@ export default { } } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } }); }, @@ -354,7 +361,12 @@ 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 ); @@ -487,6 +499,14 @@ export default { policy.policyNumber !== ""; const skipVin = await skipVinLookup(); + + this.pushEventToGA( + this.GaCategories.VIN_ELIGIBLE, + skipVin ? this.GaActions.FALSE : this.GaActions.TRUE, + this.$store.getters.vehicle.carId, + true + ); + if (vehicleChangedDuringPolicyLookupInHeritage) { if (skipVin) { this.$router.navigateWithSaving( @@ -665,12 +685,21 @@ export default { return this.$store.getters.requiresVerifiedRedirecting; }, isContinueEnabled() { - return ( + var isEnabled = this.isAnyDamageSelected && this.isWindshieldValid && this.isRearGlassValid && - this.isSideGlassValid - ); + 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; 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 diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index 4521a6e2f..5c80694bf 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 @@ -140,6 +141,8 @@ export default { styleOptions: [], displayNoServiceAlert: false, canSafeliteService: this.canSafeliteServiceFromStore(), + skipVINLookup: this.getSkipVINLookupfromStore(), + skipPartQuestions: this.getSkipPartQuestionsfromStore(), }; }, @@ -252,17 +255,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, @@ -291,7 +294,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 +312,7 @@ export default { baseMixin.methods.ResetExternalParamsAndHideModal(); } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } }); }, @@ -480,6 +488,8 @@ export default { this.vehicleSubType = result?.data.vehicleSubType; this.vehicleSpecialClass = result?.data.vehicleSpecialClass; this.isBigTruck = result?.data.isBigTruck; + this.skipVINLookup = result?.data.skipVINLookup; + this.skipPartQuestions = result?.data.skipPartQuestions; }, resetAlert() { this.displayNoServiceAlert = false; @@ -536,6 +546,8 @@ export default { imageUrl: this.imageUrl, imageVifNumber: this.imageVifNumber, imageVifColor: this.imageVifColor, + skipVINLookup: this.skipVINLookup, + skipPartQuestions: this.skipPartQuestions, }, false ); @@ -621,6 +633,12 @@ export default { getIsBigTruckfromStore() { return store.getters.vehicle.isBigTruck; }, + getSkipVINLookupfromStore() { + return store.getters.vehicle.skipVINLookup; + }, + getSkipPartQuestionsfromStore() { + return store.getters.vehicle.skipPartQuestions; + }, }, components: { diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 781c0cb34..3ab575f58 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,9 @@ 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/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js index c8d6a554f..2f64df7b0 100644 --- a/src/mixins/vehicle-questions-mixin.js +++ b/src/mixins/vehicle-questions-mixin.js @@ -5,6 +5,8 @@ import { storeActions } from "@/constants/store-actions"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; +import { getIsWindshieldOnly } from "@/helpers/damage-helper"; +import { partTypeStrings } from "@/constants/part-type-strings"; export default { methods: { @@ -367,8 +369,13 @@ export default { const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions); const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions); + const isWindshieldOnly = + getIsWindshieldOnly().toUpperCase() === partTypeStrings.WINDSHIELD; + const skipPartQuestions = store.getters.vehicle?.skipPartQuestions && isWindshieldOnly; + if ( hasPartQuestions && + !skipPartQuestions && this.currentPageComesBeforePage(currentPage, routeData.PART_QUESTIONS.name) ) { self.$router.navigateWithPageData( diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index 0d0ee836f..21bcc46f3 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -64,6 +64,7 @@ const navigationScenarios = { CLICKED_PAY_NOW: "CLICKED_PAY_NOW", CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN", CLICKED_PAY_LATER: "CLICKED_PAY_LATER", + CLICKED_SWITCH_TO_INSHOP: "CLICKED_SWITCH_TO_INSHOP", PIA_ERROR: "PIA_ERROR", PIA_CC_ERROR: "PIA_CC_ERROR", PIA_APPLE_ERROR: "PIA_APPLE_ERROR", diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 2d2f49a25..a7be5f387 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -553,6 +553,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_INSURANCE, destinationPageData: routeData.INSURANCE_COMPANY, }, + { + scenario: navigationScenarios.CLICKED_SWITCH_TO_INSHOP, + destinationPageData: routeData.SCHEDULE, + }, ], }, { 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/before-each.js b/src/router/methods/before-each.js index 15eae5d8c..237014ee6 100644 --- a/src/router/methods/before-each.js +++ b/src/router/methods/before-each.js @@ -7,6 +7,7 @@ import { isSavedSessionStillActive } from "@/helpers/heritage-integration/sessio import analyticsMixin from "@/mixins/analytics-mixin"; import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes"; import { runExperiments } from "@/router/methods/helpers/run-experiments"; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { handleSoftError, handleHardError } from "@/router/methods/error"; import { checkPagePrerequisites } from "@/router/methods/page-prerequisites"; @@ -16,6 +17,8 @@ import { handleHeritageReturn } from "@/router/methods/helpers/handle-heritage-r import { isVirtualRoute } from "@/router/methods/helpers/is-virtual-route"; import router from "@/router"; +const MAX_HERITAGE_SUPPRESS_REDIRECTS_PER_SESSION = 5; + export async function beforeEach(to, from) { try { await analyticsMixin.methods.validateSession(); @@ -27,10 +30,37 @@ export async function beforeEach(to, from) { } if (getFunnelCookie()?.SuppressConceptFunnel) { - // Redirect to heritage. - return { - name: routeData.HERITAGE.name, - }; + const redirectKey = sessionStorageKeyConstants.HERITAGE_SUPPRESS_REDIRECT_COUNT; + const redirectCount = Number(window.sessionStorage.getItem(redirectKey) ?? 0); + + if (redirectCount < MAX_HERITAGE_SUPPRESS_REDIRECTS_PER_SESSION) { + window.sessionStorage.setItem(redirectKey, String(redirectCount + 1)); + navigateToHeritageFunnel({ + shouldSaveSession: false, + pageNameToLog: to.name, + }); + return false; + } else { + // If the redirect count is greater than the max allowed, we need to handle the error. + + // Reset the redirect count to 0. + window.sessionStorage.setItem(redirectKey, "0"); + + debugLog( + "SuppressConceptFunnel: skipped heritage redirect after max attempts this session", + { redirectCount, to: to.name } + ); + + const errorPayload = { + cause: "Handling heritage redirect after max attempts this session.", + currentPage: from?.name, + nextPage: to?.name, + }; + + // Eject user from Vue app in this scenario and clear localstorage. + await handleSoftError(errorPayload, true); + return false; + } } // Ensure session has not expired diff --git a/src/router/methods/error.js b/src/router/methods/error.js index aa845d452..15cabb9a0 100644 --- a/src/router/methods/error.js +++ b/src/router/methods/error.js @@ -12,7 +12,7 @@ export async function handleSoftError(errorPayload, forceRestart = false) { deleteFunnelCookie(); } - analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload); + logErrorToDataLayerAndLogger("handleSoftError", errorPayload, forceRestart); router.push({ name: routeData.ERROR.name, @@ -27,7 +27,7 @@ export async function handleHardError(errorPayload) { ); if (isInStaticErrorExperiment) { - analyticsMixin?.methods?.pushPageErrorToDataLayer(errorPayload); + logErrorToDataLayerAndLogger("handleHardError", errorPayload, null); window.top.location = "/fmg/static/error"; return; } else { @@ -38,3 +38,10 @@ export async function handleHardError(errorPayload) { await handleSoftError(errorPayload, true); } } + +async function logErrorToDataLayerAndLogger(context, errorPayload, forceRestart = null) { + const forceRestartSegment = forceRestart === true ? " (force restart)" : ""; + const message = `${context}${forceRestartSegment}: ${errorPayload?.cause ?? "unknown cause"}`; + analyticsMixin?.methods?.pushPageErrorToDataLayer(errorPayload); + global.$logger.logError(message, errorPayload); +} diff --git a/src/router/methods/error.spec.js b/src/router/methods/error.spec.js new file mode 100644 index 000000000..c48d63fda --- /dev/null +++ b/src/router/methods/error.spec.js @@ -0,0 +1,168 @@ +import { handleSoftError, handleHardError } from "./error"; +import router from "@/router"; +import store from "@/store"; +import analyticsMixin from "@/mixins/analytics-mixin"; +import experimentMixin from "../../mixins/experiment-mixin"; +import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; +import { storeActions } from "@/constants/store-actions"; +import { routeData } from "@/router/constants/routes"; + +jest.mock("@/router", () => ({ + __esModule: true, + default: { + push: jest.fn(), + }, +})); + +jest.mock("@/store", () => ({ + __esModule: true, + default: { + dispatch: jest.fn(() => Promise.resolve()), + }, +})); + +jest.mock("@/helpers/heritage-integration/cookie-helper", () => ({ + deleteFunnelCookie: jest.fn(), +})); + +jest.mock("@/mixins/analytics-mixin", () => ({ + __esModule: true, + default: { + methods: { + pushPageErrorToDataLayer: jest.fn(), + }, + }, +})); + +jest.mock("../../mixins/experiment-mixin", () => ({ + __esModule: true, + default: { + methods: { + hasSettingEqualTo: jest.fn(), + }, + }, +})); + +describe("router/methods/error", () => { + const errorPayload = { cause: "test cause", currentPage: "from", nextPage: "to" }; + + beforeEach(() => { + jest.clearAllMocks(); + store.dispatch.mockResolvedValue(undefined); + global.$logger = { + logInformation: jest.fn(), + logWarning: jest.fn(), + logError: jest.fn(), + logCritical: jest.fn(), + }; + }); + + describe("handleSoftError", () => { + it("logs and navigates to the error route without reset when forceRestart is false", async () => { + await handleSoftError(errorPayload, false); + + expect(store.dispatch).not.toHaveBeenCalled(); + expect(deleteFunnelCookie).not.toHaveBeenCalled(); + expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith( + errorPayload + ); + expect(global.$logger.logError).toHaveBeenCalledWith( + "handleSoftError: test cause", + errorPayload + ); + expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name }); + }); + + it("dispatches reset, deletes funnel cookie, logs, and navigates when forceRestart is true", async () => { + await handleSoftError(errorPayload, true); + + expect(store.dispatch).toHaveBeenCalledWith(storeActions.RESET_STATE); + expect(deleteFunnelCookie).toHaveBeenCalled(); + expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith( + errorPayload + ); + expect(global.$logger.logError).toHaveBeenCalledWith( + "handleSoftError (force restart): test cause", + errorPayload + ); + expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name }); + }); + + it("uses unknown cause in the log message when payload has no cause", async () => { + await handleSoftError({}, false); + + expect(global.$logger.logError).toHaveBeenCalledWith( + "handleSoftError: unknown cause", + {} + ); + }); + }); + + describe("handleHardError", () => { + let topLocationValue; + + beforeEach(() => { + topLocationValue = undefined; + Object.defineProperty(window, "top", { + configurable: true, + value: {}, + }); + Object.defineProperty(window.top, "location", { + configurable: true, + set(value) { + topLocationValue = value; + }, + get() { + return topLocationValue; + }, + }); + }); + + it("logs and sets static error location when UseStaticErrorPage experiment is on", async () => { + experimentMixin.methods.hasSettingEqualTo.mockReturnValue(true); + + await handleHardError(errorPayload); + + expect(experimentMixin.methods.hasSettingEqualTo).toHaveBeenCalledWith( + "UseStaticErrorPage", + "true" + ); + expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith( + errorPayload + ); + expect(global.$logger.logError).toHaveBeenCalledWith( + "handleHardError: test cause", + errorPayload + ); + expect(topLocationValue).toBe("/fmg/static/error"); + expect(router.push).not.toHaveBeenCalled(); + }); + + it("delegates to handleSoftError with force restart when static error page is off", async () => { + experimentMixin.methods.hasSettingEqualTo.mockReturnValue(false); + + await handleHardError(errorPayload); + + expect(store.dispatch).toHaveBeenCalledWith(storeActions.RESET_STATE); + expect(deleteFunnelCookie).toHaveBeenCalled(); + expect(global.$logger.logError).toHaveBeenCalledWith( + "handleSoftError (force restart): test cause", + errorPayload + ); + expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name }); + expect(topLocationValue).toBeUndefined(); + }); + + it("delegates to handleSoftError with force restart when experiment check throws", async () => { + experimentMixin.methods.hasSettingEqualTo.mockImplementation(() => { + throw new Error("experiment failure"); + }); + + await handleHardError(errorPayload); + + expect(store.dispatch).toHaveBeenCalledWith(storeActions.RESET_STATE); + expect(deleteFunnelCookie).toHaveBeenCalled(); + expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name }); + }); + }); +}); diff --git a/src/router/methods/helpers/initialize-from-querystrings.js b/src/router/methods/helpers/initialize-from-querystrings.js index 8dff4423d..4d342cdc0 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); } @@ -95,6 +105,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( @@ -115,9 +126,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/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/router/methods/route-logic/heritage.js b/src/router/methods/route-logic/heritage.js index d1a4c41c4..509c135eb 100644 --- a/src/router/methods/route-logic/heritage.js +++ b/src/router/methods/route-logic/heritage.js @@ -1,5 +1,6 @@ import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; +// Unsure if this is needed. I don't think its used anywhere export async function heritageBeforeEnter(to, from) { await navigateToHeritageFunnel({ shouldSaveSession: false, pageNameToLog: to.name }); } diff --git a/src/store/index.js b/src/store/index.js index e740bf0d3..da45f3a61 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -9,7 +9,6 @@ import { storeActions } from "@/constants/store-actions"; import { applicationConfig } from "@/constants/application-config"; import { experimentTriggers } from "@/constants/experiments"; import { damageLocationsSelected } from "@/constants/damage-locations-selected"; -import { singleWindshieldCarIds } from "@/constants/single-windshield-carids"; import { routeData } from "@/router/constants/routes"; import { deleteFunnelCookie, @@ -83,6 +82,8 @@ const getDefaultState = () => { registration: { licensePlate: null, }, + skipVINLookup: false, + skipPartQuestions: false, }, serviceLocation: { address: null, @@ -192,6 +193,7 @@ const getDefaultState = () => { isRecalAckOptIn: false, isRecalAcknowledgedForScheduling: "", isMSRFeeApplicable: false, + isMSRFeeCoveredByInsurance: false, }, applicationUser: { eventBus: [], @@ -251,6 +253,15 @@ export const mutations = { updateVehicleCanSafeliteService(state, canSafeliteService) { state.order.vehicle.canSafeliteService = canSafeliteService; }, + updateSkipVINLookup(state, skipVINLookup) { + state.order.vehicle.skipVINLookup = skipVINLookup; + }, + updateSkipPartQuestions(state, skipPartQuestions) { + state.order.vehicle.skipPartQuestions = skipPartQuestions; + }, + updateSkipCapabilitiesQuestions(state, skipCapabilitiesQuestions) { + state.order.vehicle.skipCapabilitiesQuestions = skipCapabilitiesQuestions; + }, updateVehicleImageUrl(state, imageUrl) { state.order.vehicle.imageUrl = imageUrl; }, @@ -359,6 +370,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(); @@ -1593,6 +1607,7 @@ export const actions = { operatorId: "WEB", userName: "SafeliteConceptFunnel", referrer: referrer, + clientData: window.location.search, }; return globalMethods @@ -1630,38 +1645,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 +1753,7 @@ export const actions = { method: endpoints.LogFmgSessionData.method, endpoint: endpoints.LogFmgSessionData.url, payload: payload, + logApiCall: false, }); }, @@ -1816,6 +1800,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, @@ -2058,52 +2061,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, @@ -2146,6 +2124,7 @@ export const actions = { const carId = context.getters.vehicle.carId; const isRepair = context.getters.damage.isRepair; const numberOfChips = context.getters.damage.numberOfChips; + const apptType = context.getters.order?.serviceLocation?.appointmentType; return globalMethods.callHttpClient({ method: endpoints.GetSupportingItems.method, @@ -2156,6 +2135,9 @@ export const actions = { parentAccountNumber: applicationConfig.CASH_PARENT_ACCOUNT_NUMBER, parts: glassPartsArray, numberOfRepairChips: isRepair ? numberOfChips : 0, + providerNumber: context.getters.order.serviceLocation.zipCodeCtu, + facilityType: + apptType && apptType.trim() !== "" ? apptType : AppointmentTypeStrings.IN_SHOP, }, logApiCall: true, pageNameToLog: pageNameToLog, @@ -2611,6 +2593,7 @@ export const actions = { lockToken: order.lockToken, isRecalAckOptIn: order.isRecalAckOptIn, isMSRFeeApplicable: order.isMSRFeeApplicable, + isMSRFeeCoveredByInsurance: order.isMSRFeeCoveredByInsurance, cashPriceSubTotal: order.cashPriceSubTotal, }, }, @@ -2723,6 +2706,8 @@ export const actions = { imageUrl, imageVifNumber, imageVifColor, + skipVINLookup, + skipPartQuestions, } ) { if ( @@ -2748,6 +2733,8 @@ export const actions = { context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl); context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber); context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor); + context.commit(storeMutations.UPDATE_SKIP_VIN_LOOKUP, skipVINLookup); + context.commit(storeMutations.UPDATE_SKIP_PART_QUESTIONS, skipPartQuestions); } }, @@ -3043,6 +3030,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); }, @@ -3535,25 +3529,7 @@ export const actions = { }, isVinOptionalVehicle(context) { - //Optional for carIds with only a single windshield - if (singleWindshieldCarIds.find((item) => item === context.state.order.vehicle.carId)) { - return true; - } - //Optional for specific YMMSs - switch (context.state.order.vehicle.make.toLowerCase()) { - case "porsche": - return true; - default: - } - if ( - (context.state.order.vehicle.make.toLowerCase() === "bmw" && - context.state.order.vehicle.year < 2010) || - (context.state.order.vehicle.make.toLowerCase() === "volkswagen" && - context.state.order.vehicle.year < 2010) || - (context.state.order.vehicle.make.toLowerCase() === "audi" && - context.state.order.vehicle.year < 2010) - ) - return true; + if (context.state.order.vehicle.skipVINLookup) return true; return false; }, diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 71e2ec894..ea896116d 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -4261,6 +4261,8 @@ describe("isVinOptionalVehicle", () => { }, }; + mutations.updateSkipVINLookup(context.state, expectedVinSkip); + var vinOptionalResult = actions.isVinOptionalVehicle(context); expect(vinOptionalResult).toEqual(expectedVinSkip); } @@ -4291,6 +4293,8 @@ describe("isVinOptionalVehicle", () => { }, }; + mutations.updateSkipVINLookup(context.state, expectedVinSkip); + var vinOptionalResult = actions.isVinOptionalVehicle(context); expect(vinOptionalResult).toEqual(expectedVinSkip); } diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue index deb7bca8b..448917c1d 100644 --- a/src/ux-components/alert/alert.vue +++ b/src/ux-components/alert/alert.vue @@ -1,6 +1,6 @@