diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index b766bd06..416c6394 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -62,6 +62,10 @@ const endpoints = { GetSupportingItems: { url: '/parts/api/v1/parts/supporting-items', method: 'POST' + }, + GetServiceabilityDetails: { + url: "/location/api/v1/location/serviceability-details", + method: "GET", }, GetVehicle: { url: '/vehicle/api/v1/vehicle/lookup', diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js new file mode 100644 index 00000000..019e7bae --- /dev/null +++ b/src/helpers/service-location-helper.js @@ -0,0 +1,26 @@ +import { useMainStore } from '@/store'; + +export async function getServiceabilityDetails(serviceZipCode, lineItems) { + const serviceabilityDetails = await useMainStore().getServiceabilityDetails( + { + serviceZipCode: serviceZipCode, + lineItems: lineItems, + }, + false + ); + return Promise.resolve(serviceabilityDetails); +} + +export async function getZipCodeData(zipCode) { + const serviceZipValidationResponse = await useMainStore().validateZip( + { zip: zipCode } + ); + + return { + containsMilitaryBase: serviceZipValidationResponse.data.containsMilitaryBase, + isValid: serviceZipValidationResponse.data.isValid, + isServiceable: serviceZipValidationResponse.data.isServiceable, + state: serviceZipValidationResponse.data.state, + zipCodeCtu: serviceZipValidationResponse.data.zipCodeCtu, + }; +} \ No newline at end of file diff --git a/src/layouts/service-location/service-location.spec.js b/src/layouts/service-location/service-location.spec.js index 7ec04948..d402619f 100644 --- a/src/layouts/service-location/service-location.spec.js +++ b/src/layouts/service-location/service-location.spec.js @@ -1,6 +1,43 @@ import { shallowMount } from "@vue/test-utils"; import { getMountOptions } from "@/helpers/unit-test-helper.js"; -import serviceLocationModal from "@/layouts/service-location/service-location.vue"; +import serviceLocation from "@/layouts/service-location/service-location.vue"; + +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + if (widgetName === linkWidgetName) { + return mockLinkCmsContent[cmsFieldName]; + } + + if (widgetName === modalWidgetName) { + return mockModalCmsContent[cmsFieldName]; + } + + return null; + }), + + getZipCodeData: jest.fn((zip) => { + if (zip === "43235" || zip === "55555") { + return Promise.resolve({ + containsMilitaryBase: false, + isValid: true, + state: "OH", + zipCodeCtu: "01820", + }); + } + + return Promise.resolve({ + containsMilitaryBase: false, + isValid: false, + state: null, + zipCodeCtu: null, + }); + }), + + onSubmit: jest.fn(), + onInvalidSubmit: jest.fn(), + }, +}; describe("navigation", () => { test("if the back button is clicked, navigate back", async () => { @@ -17,16 +54,62 @@ describe("navigation", () => { }); }); +describe("updating service zip", () => { + test("updates the page model after changing the service zip code", () => { + // Arrange + const { wrapper } = setupMocks({ + mixins: [mockMixin], + }); + + const newServiceZipCodeQuestion = { + zipCode: "61606", + state: "IL", + }; + + const serviceZipCodeComponent = wrapper.findComponent({ + ref: "serviceZipCodeQuestion", + }); + + // Act + serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion); + + // Assert + expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeQuestion); + }); + + test("resets appointment type selection when service zip code is updated by service zip modal", () => { + // Arrange + const { wrapper } = setupMocks({}); + + const serviceZipCodeComponent = wrapper.findComponent({ + ref: "serviceZipCodeQuestion", + }); + + const newServiceZipCodeQuestion = { + zipCode: "61606", + state: "IL", + }; + + wrapper.vm.selectedAppointmentType = "Inshop"; + + // Act + serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion); + + // Assert + expect(wrapper.vm.selectedAppointmentType).toStrictEqual(null); + }); +}); function setupMocks() { const wrapper = shallowMount( - serviceLocationModal, + serviceLocation, getMountOptions({ router: { navigate: jest.fn(), }, }) ); + return { wrapper }; } \ No newline at end of file diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 48d8c69f..afae6d68 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -3,19 +3,24 @@
+
+ + cmsWidgetName="ServiceTypeQuestionWidget" + :questionText="questionText" + :answers="answersFromCms" + buttonTypeString="listCard" + isRequired + v-model="selectedAppointmentType" + validationRules="selection-required"/>
@@ -49,41 +54,66 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import buttonQuestion from "@/digital-components/button-question/button-question"; +import { useMainStore } from "@/store"; +import { getServiceabilityDetails, getZipCodeData } from "@/helpers/service-location-helper"; // Import Component import baseFormMixin from "@/mixins/base-form-mixin"; +import baseMixin from "@/mixins/base-mixin"; import { Form, defineRule } from "vee-validate"; import siteFooter from "@/iss-components/site-footer/site-footer.vue"; import siteHeader from "@/iss-components/site-header/site-header.vue"; import siteSubHeader from "@/iss-components/site-sub-header/site-sub-header.vue"; +import serviceZipModalQuestion from '@/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue'; // DEFINE VALIDATION RULES defineRule("selection-required", required(errorMessages.OPTION_REQUIRED)); export default { name: "service-location", - mixins: [baseFormMixin], - components: { - siteFooter, - siteHeader, - siteSubHeader, - buttonQuestion, - Form, - }, - data() { - return {}; - }, + mixins: [baseFormMixin, baseMixin], async beforeRouteEnter(to, from, next) { + // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); + + const serviceZipCode = useMainStore().order.customer.address.zipCode; + const zipCodeData = getZipCodeData(serviceZipCode); + + const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode); + // Settle promises and get results - const promiseResultMap = [{ - resultKey: "cmsContent", - promise: cmsContentPromise, - }, ]; + const promiseResultMap = [ + { + resultKey: "cmsContent", + promise: cmsContentPromise, + }, + { + resultKey: "serviceabilityDetails", + promise: serviceabilityDetailsPromise, + }, + { + resultKey: "zipCodeData", + promise: zipCodeData, + }, + ]; + const resultMap = await settleAllPromises(promiseResultMap); + next((vm) => { vm.setCmsContent(resultMap.cmsContent); }); }, + setup() { + const mainStore = useMainStore(); + return { mainStore }; + }, + data() { + return { + zipCode: this.mainStore.order.customer.address.zipCode, + state: this.mainStore.order.customer.address.state, + isServiceZipServiceable: null, + selectedAppointmentType: "", + }; + }, computed: { questionText() { return this.getCmsContent("ServiceTypeQuestionWidget", "QuestionText"); @@ -91,6 +121,24 @@ export default { answersFromCms() { return this.getCmsContent("ServiceTypeQuestionWidget", "Answers"); }, + serviceZipCodeQuestion: { + get: function() { + return { + zipCode: this.zipCode, + state: this.state, + }; + }, + set: function (newValue) { + if (newValue.zipCode !== this.zipCode) { + this.selectedAppointmentType = null; + } + + this.zipCode = newValue.zipCode; + this.state = newValue.state; + + this.$nextTick(); + }, + } }, methods: { arePagePrerequisiteValid() { @@ -105,6 +153,14 @@ export default { async forwardButtonAction() {}, resetDependentState() {}, }, + components: { + siteFooter, + siteHeader, + siteSubHeader, + buttonQuestion, + Form, + serviceZipModalQuestion, + }, }; diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js new file mode 100644 index 00000000..201b93c7 --- /dev/null +++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js @@ -0,0 +1,251 @@ +import { mount, shallowMount } from "@vue/test-utils"; +import serviceZipModalQuestion from "./service-zip-modal-question"; +import crypto from "crypto"; +global.crypto = crypto; + +jest.mock("@/digital-components/textbox-question/textbox-question", () => ({ + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + return widgetName[cmsFieldName]; + }), +})); + +jest.mock("@/digital-components/modal/modal", () => ({ + methods: { + closeModal: jest.fn(), + resetButtonStyle: jest.fn(), + }, +})); + +const mockGetServiceabilityDetails = (mockServiceZipCode) => { + const serviceabilityDetails = { + isGlassServiceableInshop: true, + isRecalibrationServiceableInshop: true, + isGlassServiceableMobile: true, + isRecalibrationServiceableMobile: true, + }; + + return Promise.resolve(serviceabilityDetails); +}; + +const mockGetZipCodeData = (mockServiceZipCode) => { + if (mockServiceZipCode === "43235") { + return { + containsMilitaryBase: false, + isValid: true, + state: "OH", + zipCodeCtu: "01820", + }; + } + + if (mockServiceZipCode === "61606") { + return { + containsMilitaryBase: false, + isValid: true, + state: "IL", + zipCodeCtu: "01526", + }; + } + + return { + containsMilitaryBase: false, + isValid: false, + state: null, + zipCodeCtu: null, + }; +} + +jest.mock( + "@/helpers/service-location-helper", + () => ({ + getServiceabilityDetails: jest.fn((mockServiceZipCode) => { + return mockGetServiceabilityDetails(mockServiceZipCode); + }), + getZipCodeData: jest.fn((mockServiceZipCode) => { + return mockGetZipCodeData(mockServiceZipCode); + }), + }) +); + +const linkWidgetName = "linkWidgetName"; +const modalWidgetName = "modalWidgetName"; + +const mockLinkCmsContent = { + BodyText: "Sample link body text here.", +}; + +const mockModalCmsContent = { + FooterText: "Sample modal footer text here.", +}; + +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + if (widgetName === linkWidgetName) { + return mockLinkCmsContent[cmsFieldName]; + } + + if (widgetName === modalWidgetName) { + return mockModalCmsContent[cmsFieldName]; + } + return null; + }), + } +} + +describe("service-zip-modal-question.vue", () => { + it("Initial state on load with existing location info", async () => { + // Arrange + let serviceZipCodeQuestion = { + state: "OH", + zipCode: "43235", + }; + + const wrapper = mount(serviceZipModalQuestion, { + mixins: [mockMixin], + props: { + modelValue: serviceZipCodeQuestion, + linkWidgetName: linkWidgetName, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Assert + expect(wrapper.html()).not.toEqual(expect.stringContaining(mockLinkCmsContent["BodyText"])); + }); + + it("Should emit update:modelValue on setZipCode for a valid, serviceable zip", async () => { + // Arrange + let serviceZipCodeQuestion = { + state: "IL", + zipCode: "61606", + }; + + const zip = "43235"; + + const wrapper = mount(serviceZipModalQuestion, { + mixins: [mockMixin], + props: { + modelValue: serviceZipCodeQuestion, + mobileFeePart: {}, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + wrapper.vm.internalModel.zipCode = zip; + + await wrapper.vm.setZipCode(); + + let expectedEmit = [[{ state: "OH", zipCode: "43235" }]]; + + // Assert + expect(wrapper.emitted("update:modelValue")).toEqual(expectedEmit); + }); + + it("Should not emit update:modelValue on setZipCode for an invalid zip", async () => { + // Arrange + let serviceZipCodeQuestion = { + state: "IL", + zipCode: "61606", + }; + + const zip = ""; + + const wrapper = mount(serviceZipModalQuestion, { + mixins: [mockMixin], + props: { + modelValue: serviceZipCodeQuestion, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + wrapper.vm.internalModel.zipCode = zip; + await wrapper.vm.setZipCode(); + + // Assert + expect(wrapper.emitted("update:modelValue")).not.toBeTruthy(); + }); + + it("Should display invalid zip alerts for invalid ip inputs", async () => { + // Arrange + let serviceZipCodeQuestion = { + state: "IL", + zipCode: "61606", + }; + + const zip = "11111"; + + const wrapper = mount(serviceZipModalQuestion, { + mixins: [mockMixin], + props: { + modelValue: serviceZipCodeQuestion, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + wrapper.vm.internalModel.zipCode = zip; + await wrapper.vm.setZipCode(); + + // Assert + expect(wrapper.vm.displayInvalidZipAlert).toBe(true); + }); + + it("Should reset all alerts on onModalClosed", async () => { + // Arrange + let serviceZipCodeQuestion = { + state: "IL", + zipCode: "61606", + }; + + const zip = ""; + + const wrapper = mount(serviceZipModalQuestion, { + mixins: [mockMixin], + props: { + modelValue: serviceZipCodeQuestion, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + wrapper.vm.internalModel.zipCode = zip; + await wrapper.vm.setZipCode(); + wrapper.vm.onModalClosed(); + + // Assert + expect(wrapper.vm.displayInvalidZipAlert).toBe(false); + }); + + it("Should prepopulate the zip on onModalOpened", async () => { + // Arrange + let serviceZipCodeQuestion = { + state: "IL", + zipCode: "61606", + }; + + const zip = "123"; + + const wrapper = mount(serviceZipModalQuestion, { + mixins: [mockMixin], + props: { + modelValue: serviceZipCodeQuestion, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + wrapper.vm.internalModel.zipCode = zip; + wrapper.vm.onModalOpened(); + + // Assert + expect(wrapper.vm.internalModel.zipCode).toEqual("61606"); + }); +}); \ No newline at end of file diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue new file mode 100644 index 00000000..5e3493a3 --- /dev/null +++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.spec.js b/src/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.spec.js new file mode 100644 index 00000000..b3276a7a --- /dev/null +++ b/src/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.spec.js @@ -0,0 +1,40 @@ +import { shallowMount } from "@vue/test-utils"; +import serviceZipQuestion from "./service-zip-question"; + +describe("service-zip-question.vue", () => { + it("Should get the modelValue", async () => { + // Arrange + const text = "test"; + const wrapper = shallowMount(serviceZipQuestion, { + props: { + modelValue: text, + }, + attachTo: document.body, + }); + + // Act + const modelValueText = wrapper.vm.value; + wrapper.vm.value = "test also"; + + // Assert + expect(modelValueText).toEqual("test"); + }); + + it("Should emit to set value", async () => { + // Arrange + const text = "test"; + const wrapper = shallowMount(serviceZipQuestion, { + props: { + modelValue: text, + }, + attachTo: document.body, + }); + + // Act + const modelValueText = wrapper.vm.value; + wrapper.vm.value = "test also"; + + // Assert + expect(wrapper.emitted("update:modelValue")).toEqual([["test also"]]); + }); +}); \ No newline at end of file diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue b/src/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue new file mode 100644 index 00000000..c92816af --- /dev/null +++ b/src/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question.vue @@ -0,0 +1,49 @@ + + + \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index e6726940..54192a1f 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -451,7 +451,7 @@ export const useMainStore = defineStore({ console.error(error); return []; }); - }, + }, async getRainDefense() { return globalMethods @@ -525,6 +525,16 @@ export const useMainStore = defineStore({ return availableLineItems; }, + + getServiceabilityDetails({ serviceZipCode }) { + const lineItemsToSend = this.order.lineItems.supportingItems; + const encodedLineItems = encodeURIComponent(JSON.stringify(lineItemsToSend)); + return globalMethods.callHttpClient({ + method: endpoints.GetServiceabilityDetails.method, + endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&lineItems=${encodedLineItems}`, + }); + }, + lookupVehicleByVin(vin) { return globalMethods.callHttpClient({ method: endpoints.LookupVehicleByVin.method,