diff --git a/src/constants/query-strings.js b/src/constants/query-strings.js index a3b3c732c..a21d93200 100644 --- a/src/constants/query-strings.js +++ b/src/constants/query-strings.js @@ -33,6 +33,9 @@ const queryStrings = { SERVICE_ZIP: "servicezip", EMAIL: "email", IS_INSURANCE: "isinsurance", + VIN_SELECTION: "vinselection", + SERVICE_PACKAGE: "servicepackage", + NUMBER_OF_CHIPS: "numberofchips", }; export { queryStrings }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 0001daf6a..acb06c41d 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -96,7 +96,8 @@ const storeActions = { CREATE_SUBMITTED_ORDER: "createSubmittedOrder", RESET_SUBMITTED_ORDER: "resetSubmittedOrder", - RESET_IS_LEAD_GEN: "resetIsLeadGen", + RESET_LEADGEN_STATE: "resetLeadGenState", + CREATE_LEADGEN_STATE: "createLeadGenState", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index f78b970c3..48700d530 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -88,6 +88,28 @@ const storeMutations = { // EXPERIMENT MUTATIONS UPDATE_EXPERIMENTS: "updateExperiments", UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry", + + // LEADGEN MUTATIONS + UPDATE_LEAD_GEN_YEAR: "updateLeadGenYear", + UPDATE_LEAD_GEN_MAKE: "updateLeadGenMake", + UPDATE_LEAD_GEN_MODEL: "updateLeadGenModel", + UPDATE_LEAD_GEN_STYLE: "updateLeadGenStyle", + UPDATE_LEAD_GEN_IS_REPAIR: "updateLeadGenIsRepair", + UPDATE_LEAD_GEN_NUMBER_OF_CHIPS: "updateLeadGenNumberOfChips", + UPDATE_LEAD_GEN_DAMAGE_TYPE: "updateLeadGenDamageType", + UPDATE_LEAD_GEN_ZIP_CODE: "updateLeadGenZipCode", + UPDATE_LEAD_GEN_EMAIL_ADDRESS: "updateLeadGenEmailAddress", + UPDATE_LEAD_GEN_IS_INSURANCE: "updateLeadGenIsInsurance", + UPDATE_LEAD_GEN_VIN_SELECTION: "updateLeadGenVinSelection", + UPDATE_LEAD_GEN_SERVICE_PACKAGE: "updateLeadGenServicePackage", + + //RESET LEADGEN MUTATIONS + RESET_LEADGEN_VEHICLE_STATE: "resetLeadGenVehicleState", + RESETL_EADGEN_DAMAGE_STATE: "resetLeadGenDamageState", + RESET_LEADGEN_ESTIMATE_STATE: "resetLeadGenEstimateState", + RESET_LEADGEN_SERVICEZIP_STATE: "resetLeadGenServiceZipState", + RESET_LEADGEN_QUOTE_STATE: "resetLeadGenQuoteState", + RESET_IS_LEADGEN: "resetIsLeadGen", }; export { storeMutations }; diff --git a/src/helpers/duration-length-helper.js b/src/helpers/duration-length-helper.js new file mode 100644 index 000000000..cd57f2c4e --- /dev/null +++ b/src/helpers/duration-length-helper.js @@ -0,0 +1,15 @@ +export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) { + const isLongAppointment = durationMaximum >= 120; + const isDurationRange = durationMinimum !== durationMaximum; + + const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum; + const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum; + + const durationText = isDurationRange + ? `${adjustedMinimum} - ${adjustedMaximum}` + : adjustedMinimum; + + const unitText = isLongAppointment ? "hours" : "minutes"; + + return `${durationText} ${unitText}`; +} diff --git a/src/helpers/duration-length-helper.spec.js b/src/helpers/duration-length-helper.spec.js new file mode 100644 index 000000000..b35585311 --- /dev/null +++ b/src/helpers/duration-length-helper.spec.js @@ -0,0 +1,24 @@ +import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper"; + +describe("damage-length-helper.js", () => { + it("Should return the expected duration for the appointment in hours", () => { + // Arrange / Act + const durationMinimum = 60; + const durationMaximum = 120; + + const duration = getDisplayTextForDurationLength(durationMinimum, durationMaximum); + + // Assert + expect(duration).toEqual("1 - 2 hours"); + }); + it("Should return the expected duration for the appointment in minutes", () => { + // Arrange / Act + const durationMinimum = 30; + const durationMaximum = 45; + + const duration = getDisplayTextForDurationLength(durationMinimum, durationMaximum); + + // Assert + expect(duration).toEqual("30 - 45 minutes"); + }); +}); diff --git a/src/helpers/service-package-helper.js b/src/helpers/service-package-helper.js index cab883cc6..310a87e0e 100644 --- a/src/helpers/service-package-helper.js +++ b/src/helpers/service-package-helper.js @@ -230,6 +230,15 @@ export function getHighestFullySatisfiedTier(glassToReplace, availableLineItems, return highestSatisfiedPackage; } +export function getPackageNameByType(packageType) { + const package_names = { + glassonly: packageNames.TIER_ONE, + standard: packageNames.TIER_TWO, + premium: packageNames.TIER_THREE, + }; + return package_names[packageType] || null; +} + function maxTier(tierA, tierB) { if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) { return packageNames.TIER_THREE; diff --git a/src/layouts/address-lookup/address-lookup.vue b/src/layouts/address-lookup/address-lookup.vue index 68a9e001c..9655b3b87 100644 --- a/src/layouts/address-lookup/address-lookup.vue +++ b/src/layouts/address-lookup/address-lookup.vue @@ -119,7 +119,7 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import { storeActions } from "@/constants/store-actions"; import { routerParams } from "@/router/router-constants/router-params"; import { getDamageString, isGlassAvailableForCarId } from "@/helpers/damage-helper"; - +import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; import vinPagesMixin from "@/mixins/vin-pages-mixin"; @@ -150,6 +150,10 @@ export default { // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); + if (store.getters.isLeadGen) { + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); + baseMixin.methods.hideFmgLoadingModal(); + } }); }, data() { diff --git a/src/layouts/confirmation/confirmation.spec.js b/src/layouts/confirmation/confirmation.spec.js index 0d8a1b273..39e5d8b49 100644 --- a/src/layouts/confirmation/confirmation.spec.js +++ b/src/layouts/confirmation/confirmation.spec.js @@ -25,6 +25,8 @@ beforeEach(() => { startTime: "09:00", endTime: "10:00", routeCode: "000", + jobMaxMinutes: "120", + jobMinMinutes: "60", }, lineItems: { glassParts: [ @@ -360,6 +362,41 @@ describe("computed properties...", () => { // Assert expect(testValue).toEqual("test1,
test, AZ 12345"); }); + test("Appointment Duration should return text in expected format.", () => { + //Arrange + + const { wrapper } = setupMocks({}); + + // Act + const testValue = wrapper.vm.AppointmentDuration; + + // Assert + expect(testValue).toEqual("Duration: 1 - 2 hours"); + }); + test("Appointment Duration should return with correct duration values in expected format for hours.", () => { + //Arrange + store.getters.submittedOrder.schedule.jobMaxMinutes = 120; + store.getters.submittedOrder.schedule.jobMinMinutes = 60; + const { wrapper } = setupMocks({}); + + // Act + const testValue = wrapper.vm.AppointmentDuration; + + // Assert + expect(testValue).toEqual("Duration: 1 - 2 hours"); + }); + test("Appointment Duration should return with correct duration values in expected format for minutes.", () => { + //Arrange + store.getters.submittedOrder.schedule.jobMaxMinutes = 45; + store.getters.submittedOrder.schedule.jobMinMinutes = 30; + const { wrapper } = setupMocks({}); + + // Act + const testValue = wrapper.vm.AppointmentDuration; + + // Assert + expect(testValue).toEqual("Duration: 30 - 45 minutes"); + }); }); function setupMocks({ customMountOptions }) { diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index 39ba55f17..efbc2e15f 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -42,6 +42,12 @@ :scheduleEndTime="ScheduleEndTime" />
+ +
@@ -82,6 +88,7 @@ import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import navbar from "@/fmg-components/nav-bar/nav-bar"; import addToCalendar from "@/layouts/confirmation/add-to-calendar/add-to-calendar"; import cart from "@/fmg-components/cart/cart"; +import textBlock from "@/digital-components/text-block/text-block"; //Supporting files import baseMixin from "@/mixins/base-mixin.js"; @@ -97,6 +104,7 @@ import { deepClone } from "@/helpers/object-helper"; import { Form } from "vee-validate"; import { get12HourTimeFormat, get12HourTimeMobileFormat } from "@/helpers/date-helper"; import { coverageStatus } from "@/constants/insurance"; +import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper"; export default { name: "confirmation", @@ -315,6 +323,11 @@ export default { isPia() { return store.getters.submittedOrder?.payment.isPia; }, + AppointmentDuration() { + const durationMaximum = store.getters.submittedOrder?.schedule?.jobMaxMinutes; + const durationMinimum = store.getters.submittedOrder?.schedule?.jobMinMinutes; + return "Duration: " + getDisplayTextForDurationLength(durationMinimum, durationMaximum); + }, }, methods: { arePagePrerequisitesValid() { @@ -365,6 +378,7 @@ export default { vehicleBanner, addToCalendar, cart, + textBlock, }, }; diff --git a/src/layouts/estimate/estimate.spec.js b/src/layouts/estimate/estimate.spec.js index 14752e171..3ea0babd1 100644 --- a/src/layouts/estimate/estimate.spec.js +++ b/src/layouts/estimate/estimate.spec.js @@ -12,6 +12,7 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import baseMixin from "../../mixins/base-mixin"; import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js"; import { experimentSettings } from "@/constants/experiments"; +import { nextTick } from "vue"; // Mock our module for promises. jest.mock("@/helpers/layout-helper.js", () => ({ @@ -213,20 +214,18 @@ describe("estimate.vue", () => { }); describe("estimate.vue", () => { - test("should call forwardButtonAction if isLeadGen is true", async () => { - // update store with isLeadGen as true + test("should call forwardButtonAction if isLeadGen is true and form is valid", async () => { store.commit(storeMutations.UPDATE_IS_LEAD_GEN, true); + store.commit(storeMutations.UPDATE_LEAD_GEN_VIN_SELECTION, "decline"); // Set up the component const { wrapper } = setupMocks({}); + wrapper.vm.forwardButtonAction = jest.fn(); + const nextFunction = jest.fn((c) => { c(wrapper.vm); }); - - // Set selectedVinLookupMethod to decline - wrapper.setData({ selectedVinLookupMethod: vinLookupMethodSelections.DECLINE }); - // Call the method that contains the if-else logic await estimate.beforeRouteEnter.call( wrapper.vm, @@ -234,7 +233,12 @@ describe("estimate.vue", () => { undefined, nextFunction ); + await wrapper.vm.$nextTick(); + expect(nextFunction).toHaveBeenCalled(); + expect(baseMixin.methods.isFormValid).toHaveBeenCalled(); + expect(baseMixin.methods.isFormValid()).toBe(true); + await wrapper.vm.$nextTick(); expect(wrapper.vm.forwardButtonAction).toHaveBeenCalled(); }); }); @@ -247,6 +251,7 @@ function setupMocks({ { Name: "Provide my VIN manually Most specific to your vehicle" }, { Name: "Provide my license plate # Most accurate VIN match" }, { Name: "Provide my home address Most convenient VIN match" }, + { Name: "I'd rather not share my VIN" }, ], FunnelFooterWidget = { ForwardButtonText: "test txt" }, mountOptionsMockData = { @@ -280,7 +285,7 @@ function setupMocks({ const apiPromise = Promise.resolve({ cmsContent }); settleAllPromises.mockImplementation(() => apiPromise); fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); - + baseMixin.methods.isFormValid = jest.fn().mockReturnValue(true); const mountOptions = getMountOptions({ ...mountOptionsMockData, mixins: [baseMixin, mockMixin], diff --git a/src/layouts/estimate/estimate.vue b/src/layouts/estimate/estimate.vue index 913520626..dcd47cca4 100644 --- a/src/layouts/estimate/estimate.vue +++ b/src/layouts/estimate/estimate.vue @@ -63,6 +63,7 @@ import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigat import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; +import { nextTick } from "vue"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -128,7 +129,7 @@ export default { const resultMap = await settleAllPromises(promiseResultMap); - next((vm) => { + next(async (vm) => { if (resultMap.cmsContent.FunnelFooterWidget.ForwardButtonText.includes("|")) { const forwardTextOption = resultMap.cmsContent.FunnelFooterWidget.ForwardButtonText.split("|"); @@ -146,10 +147,24 @@ export default { vm.setCmsContent(resultMap.cmsContent); if (store.getters.isLeadGen) { - vm.selectedVinLookupMethod = vinLookupMethodSelections.DECLINE; - vm.forwardButtonAction(); + if (store.getters.leadGenEstimate.vinSelection) { + vm.selectedVinLookupMethod = vinPagesMixin.methods.getVinlookupMethod( + store.getters.leadGenEstimate.vinSelection + ); + await nextTick(); + const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); + if (isValid) { + vm.forwardButtonAction(); + } else { + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); + baseMixin.methods.hideFmgLoadingModal(); + } + } else { + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); + baseMixin.methods.hideFmgLoadingModal(); + } } else { - baseMixin.methods.dispatchStoreAction(storeActions.RESET_IS_LEAD_GEN); + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); baseMixin.methods.hideFmgLoadingModal(); } }); diff --git a/src/layouts/insurance-company/insurance-company.vue b/src/layouts/insurance-company/insurance-company.vue index 8cdec3afa..bf8765b40 100644 --- a/src/layouts/insurance-company/insurance-company.vue +++ b/src/layouts/insurance-company/insurance-company.vue @@ -77,7 +77,7 @@ export default { vm.setCmsContent(resultMap.cmsContent); vm.originalList = resultMap.insuranceCompanyList; if (store.getters.isLeadGen) { - baseMixin.methods.dispatchStoreAction(storeActions.RESET_IS_LEAD_GEN); + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); baseMixin.methods.hideFmgLoadingModal(); } }); diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue index c0c94ecb7..4a87d639f 100644 --- a/src/layouts/license-plate-lookup/license-plate-lookup.vue +++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue @@ -112,7 +112,7 @@ import { getDamageString, isGlassAvailableForCarId } from "@/helpers/damage-help import { routerParams } from "@/router/router-constants/router-params"; import { required, regex } from "@/helpers/validation-rules"; import { Form, defineRule } from "vee-validate"; - +import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; import vinPagesMixin from "@/mixins/vin-pages-mixin"; @@ -148,6 +148,10 @@ export default { // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); + if (store.getters.isLeadGen) { + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); + baseMixin.methods.hideFmgLoadingModal(); + } }); }, props: { diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 9249a5a57..58be51c27 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -56,7 +56,7 @@ export default { next((vm) => { vm.setCmsContent(resultMap.cmsContent); if (store.getters.isLeadGen) { - baseMixin.methods.dispatchStoreAction(storeActions.RESET_IS_LEAD_GEN); + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); baseMixin.methods.hideFmgLoadingModal(); } }); diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index 9fd4153f7..a453b9c1a 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -58,6 +58,9 @@ jest.mock("@/mixins/base-mixin", () => ({ filterOutFees(items) { return null; }, + isFormValid(form) { + return true; + }, }, })); @@ -66,6 +69,9 @@ const mockMixin = { filterOutFees: jest.fn().mockImplementation(() => { return null; }), + isFormValid: jest.fn().mockImplementation(() => { + return true; + }), getSettingValue: jest.fn((settingName) => { if (settingName === experimentSettings.SERVICE_PACKAGE_DISCOUNT) { return true; @@ -641,7 +647,7 @@ describe("quote.vue", () => { }); }); describe("quote.vue", () => { - test("should call forwardButtonAction if isLeadGen is true and Insurance is true", async () => { + test("should call forwardButtonAction if isLeadGen and Insurance is true and form is valid", async () => { // Set up the store with isLeadGen as true store.getters = { @@ -666,6 +672,10 @@ describe("quote.vue", () => { }, }, isLeadGen: true, + leadGenQuote: { + isInsurance: true, + servicePackage: "glassonly", + }, payment: { isInsurance: true, inactivePromos: [], @@ -693,72 +703,24 @@ describe("quote.vue", () => { undefined, nextFunction ); + await nextTick(); expect(nextFunction).toHaveBeenCalled(); + expect(baseMixin.methods.isFormValid).toHaveBeenCalled(); + expect(baseMixin.methods.isFormValid()).toBe(true); + await wrapper.vm.$nextTick(); expect(wrapper.vm.forwardButtonAction).toHaveBeenCalled(); }); - test("should not call forwardButtonAction if isLeadGen is true and Insurance is false", async () => { - // Set up the store with isLeadGen as true - store.getters = { - lineItems: { - glassParts: ["item", "item2"], - }, - order: { - lineItems: { - glassParts: ["item", "item2"], - }, - serviceLocation: { - zipCode: "12345", - zipCodeCtu: "value", - }, - damage: { - isRepair: false, - }, - referralNumber: "1234567", - payment: { - isInsurance: null, - inactivePromos: [], - }, - }, - isLeadGen: true, - payment: { - isInsurance: null, - inactivePromos: [], - }, - vehicle: { - cardId: "123", - }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, - }; - - // Set up the component - const { wrapper } = setupMocks({}); - wrapper.vm.$route = { query: null }; - wrapper.vm.forwardButtonAction = jest.fn(); - const nextFunction = jest.fn((c) => { - c(wrapper.vm); - }); - - // Call the method that contains the if-else logic - await quote.beforeRouteEnter.call( - wrapper.vm, - { query: { fmgPage: "quote" } }, - undefined, - nextFunction - ); - expect(wrapper.vm.forwardButtonAction).not.toHaveBeenCalled(); - }); }); function setupMocks({ customMountOptions }) { const mountOptions = getMountOptions({ ...customMountOptions, }); + baseMixin.methods.hideFmgLoadingModal = jest.fn(); + baseMixin.methods.isFormValid = jest.fn().mockReturnValue(true); mountOptions.global.mocks["$store"] = store; mountOptions["attachTo"] = document.body; - const wrapper = shallowMount(quote, mountOptions); wrapper.vm.setCmsContent = jest.fn(); return { wrapper }; diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index a85fd7d29..3eb513ba5 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -33,6 +33,7 @@ :isInsuranceSelected="isInsuranceSelected" @vapsItemsSelected="vapsItemsSelectedAction" @servicePackageDiscountSelected="servicePackageDiscountSelectedAction" + :servicePackage="servicePackage" :activePromos="lineItems.promos" v-on="{ 'buttonEvent.openModal': openModalAction }" validationRules="option-required" @@ -124,6 +125,7 @@ import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-moda import { experimentSettings } from "@/constants/experiments"; import { partTypeStrings } from "@/constants/part-type-strings"; import { containsLineItemWithPartType } from "@/helpers/service-package-helper"; +import { nextTick } from "vue"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); export default { @@ -245,7 +247,7 @@ export default { // End of promo logic // Call the "next" function to complete the transition to this page. - next((vm) => { + next(async (vm) => { vm.setCmsContent(resultMap.cmsContent); vm.addableVaps = addableVaps; vm.lineItems = lineItems; @@ -271,14 +273,23 @@ export default { ); } if (store.getters.isLeadGen) { - if (store.getters.order.payment.isInsurance == true) { - vm.forwardButtonAction(); + if (store.getters.leadGenQuote.isInsurance == true) { + vm.isInsuranceSelected = true; + vm.servicePackage = store.getters.leadGenQuote.servicePackage; + await nextTick(); + const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); + if (isValid) { + vm.forwardButtonAction(); + } else { + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); + baseMixin.methods.hideFmgLoadingModal(); + } } else { - baseMixin.methods.dispatchStoreAction(storeActions.RESET_IS_LEAD_GEN); + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); baseMixin.methods.hideFmgLoadingModal(); } } else { - baseMixin.methods.dispatchStoreAction(storeActions.RESET_IS_LEAD_GEN); + baseMixin.methods.dispatchStoreAction(storeActions.RESET_LEADGEN_STATE); baseMixin.methods.hideFmgLoadingModal(); } }); @@ -289,6 +300,7 @@ export default { availableLineItems: null, lineItems: [], addableVaps: [], + servicePackage: null, }; }, computed: { @@ -454,7 +466,12 @@ export default { }, }; -