diff --git a/src/constants/bailout-codes.js b/src/constants/bailout-codes.js new file mode 100644 index 000000000..af6290363 --- /dev/null +++ b/src/constants/bailout-codes.js @@ -0,0 +1,5 @@ +const bailoutCodes = { + PARTS_NOT_FOUND: 10, +}; + +export { bailoutCodes }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index ad31474c3..a50ff4bb9 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -132,6 +132,8 @@ const storeActions = { UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey", CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry", + + SAVE_BAILOUT_CODE: "saveBailoutCode", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index e8bde83a5..d2fc2b0fc 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -115,6 +115,9 @@ const storeMutations = { UPDATE_EXPERIMENTS: "updateExperiments", UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry", + // BAILOUT MUTATIONS + UPDATE_BAILOUT_CODE: "updateBailoutCode", + // EXTERNAL_PARAMETER MUTATIONS UPDATE_IS_EXTERNAL_PARAMETER: "updateIsExternalParameter", UPDATE_EXTERNAL_PARAMETER_YEAR: "updateExternalParameterYear", diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index a3dc363f5..613f239f2 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -106,6 +106,14 @@ export async function saveQuote({ pageNameToLog }) { return; } +export async function submitBailout({ pageNameToLog }) { + await saveSession({ + pageNameToLog: pageNameToLog, + shouldAwaitSaveSessionQueue: true, + submitAfterSave: false, + }); +} + // PRIVATE FUNCTIONS // /* diff --git a/src/layouts/bailout-success/bailout-success.spec.js b/src/layouts/bailout-success/bailout-success.spec.js new file mode 100644 index 000000000..5d5f73f14 --- /dev/null +++ b/src/layouts/bailout-success/bailout-success.spec.js @@ -0,0 +1,56 @@ +// Components +import bailoutSuccess from "@/layouts/bailout-success/bailout-success.vue"; + +// Supporting Files +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +// Mock fetchCmsContentForPage +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: jest.fn(), +})); + +describe("bailout-success.vue", () => { + test("renders funnelHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true); + }); + + test("renders funnelSubHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true); + }); + + test("renders Form component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true); + }); + + test("renders buttonMain component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "buttonMain" }).exists()).toBe(true); + }); +}); + +function setupMocks() { + const mountOptions = getMountOptions({}); + + //Mock props + const mockMixin = { + methods: { + getCmsContent: jest.fn(), + }, + }; + + mountOptions.mixins = [mockMixin]; + const wrapper = shallowMount(bailoutSuccess, mountOptions); + + wrapper.vm.setCmsContent = jest.fn(); + + return { wrapper }; +} diff --git a/src/layouts/bailout-success/bailout-success.vue b/src/layouts/bailout-success/bailout-success.vue new file mode 100644 index 000000000..97f186ca6 --- /dev/null +++ b/src/layouts/bailout-success/bailout-success.vue @@ -0,0 +1,91 @@ + + + diff --git a/src/layouts/bailout/bailout.spec.js b/src/layouts/bailout/bailout.spec.js index ebed2697a..905f9e5a0 100644 --- a/src/layouts/bailout/bailout.spec.js +++ b/src/layouts/bailout/bailout.spec.js @@ -16,6 +16,26 @@ jest.mock("@/helpers/cms-content-helper", () => ({ })); describe("bailout.vue", () => { + test("renders funnelHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true); + }); + + test("renders funnelSubHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true); + }); + + test("renders Form component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true); + }); + + test("renders navbar component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "navbar" }).exists()).toBe(true); + }); + test("arePagePrerequisitesValid should be true ", async () => { //Arrange const { wrapper } = setupMocks(); diff --git a/src/layouts/bailout/bailout.vue b/src/layouts/bailout/bailout.vue index a64f7a416..ae5600e08 100644 --- a/src/layouts/bailout/bailout.vue +++ b/src/layouts/bailout/bailout.vue @@ -1,21 +1,80 @@ diff --git a/src/mixins/bailout-mixin.js b/src/mixins/bailout-mixin.js new file mode 100644 index 000000000..f8c724da2 --- /dev/null +++ b/src/mixins/bailout-mixin.js @@ -0,0 +1,13 @@ +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; + +export default { + methods: { + navigateToBailoutPage(vm, bailoutCode) { + const self = vm ?? this; + + self.dispatchStoreAction(self.storeActions.SAVE_BAILOUT_CODE, bailoutCode).then(() => { + self.$router.navigateWithoutSaving(navigationScenarios.BAILOUT, self.pageName); + }); + }, + }, +}; diff --git a/src/mixins/bailout-mixin.spec.js b/src/mixins/bailout-mixin.spec.js new file mode 100644 index 000000000..c213de767 --- /dev/null +++ b/src/mixins/bailout-mixin.spec.js @@ -0,0 +1,103 @@ +import bailoutMixin from "@/mixins/bailout-mixin"; +import { storeActions } from "@/constants/store-actions.js"; +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; +import { bailoutCodes } from "@/constants/bailout-codes.js"; + +describe("bailout-mixin.js", () => { + test("navigateToBailoutPage: dispatches SAVE_BAILOUT_CODE action with bailout code", async () => { + // Arrange + const mockVm = createMockVm(); + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + bailoutCode + ); + }); + + test("navigateToBailoutPage: navigates to bailout page after saving bailout code", async () => { + // Arrange + const mockVm = createMockVm(); + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith( + navigationScenarios.BAILOUT, + mockVm.pageName + ); + }); + + test("navigateToBailoutPage: uses current context (this) when vm is not provided", async () => { + // Arrange + const mockRouter = { + navigateWithoutSaving: jest.fn().mockResolvedValue(undefined), + }; + const mockThis = { + dispatchStoreAction: jest.fn().mockResolvedValue(undefined), + $router: mockRouter, + storeActions: storeActions, + pageName: "test-page", + }; + + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage.call(mockThis, undefined, bailoutCode); + + // Assert + expect(mockThis.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + bailoutCode + ); + }); + + test("navigateToBailoutPage: passes correct bailout code to store", async () => { + // Arrange + const mockVm = createMockVm(); + const customBailoutCode = 999; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, customBailoutCode); + + // Assert + expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + customBailoutCode + ); + }); + + test("navigateToBailoutPage: calls navigateWithoutSaving with correct parameters", async () => { + // Arrange + const mockVm = createMockVm(); + const mockPageName = "vehicle-damage"; + mockVm.pageName = mockPageName; + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith( + navigationScenarios.BAILOUT, + mockPageName + ); + }); +}); + +function createMockVm() { + return { + dispatchStoreAction: jest.fn().mockResolvedValue(undefined), + $router: { + navigateWithoutSaving: jest.fn().mockResolvedValue(undefined), + }, + storeActions, + pageName: "test-page", + }; +} diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index fc195d910..2309cbde5 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -1,9 +1,11 @@ import { storeActions } from "@/constants/store-actions.js"; import store from "@/store"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; +import bailoutMixin from "@/mixins/bailout-mixin"; import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import { experimentSettings } from "@/constants/experiments"; import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js"; +import { bailoutCodes } from "@/constants/bailout-codes"; export default { computed: { @@ -34,6 +36,11 @@ export default { const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS, { pageNameToLog: pageName, }); + + if (result.PartsNotFound) { + bailoutMixin.methods.navigateToBailoutPage(this, bailoutCodes.PARTS_NOT_FOUND); + } + const partsOrQuestions = result.data.partsOrQuestions; vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this); diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index fef66ddbb..e4aac41c1 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -13,6 +13,11 @@ const navigationScenarios = { CLICKED_FORWARD_WITH_CASH: "CLICKED_FORWARD_WITH_CASH", CLICKED_FORWARD_WITH_INSURANCE: "CLICKED_FORWARD_WITH_INSURANCE", + // Bailout + BAILOUT: "BAILOUT", + BAILOUT_SUCCESS: "BAILOUT_SUCCESS", + CLICKED_BACK_TO_HOMEPAGE: "CLICKED_BACK_TO_HOMEPAGE", + // TODO: use virtual page? // Vin selection CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN", diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index fc54025ef..479d0899d 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -171,6 +171,14 @@ export const routeData = { path: "/virtual/restart", virtual: true, }, + BAILOUT: { + name: "bailout", + path: "/bailout", + }, + BAILOUT_SUCCESS: { + name: "bailout-success", + path: "/bailout-success", + }, }; export const FUNNEL_START_PAGE = routeData.VEHICLE; diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 77876a096..877802995 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -274,6 +274,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, destinationPageData: routeData.QUOTE, }, + { + scenario: navigationScenarios.BAILOUT, + destinationPageData: routeData.BAILOUT, + }, ], }, { @@ -802,6 +806,24 @@ const routingTable = function () { }, ], }, + { + pageName: routeData.BAILOUT.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.BAILOUT_SUCCESS, + }, + ], + }, + { + pageName: routeData.BAILOUT_SUCCESS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_TO_HOMEPAGE, + destinationPageData: routeData.RESTART, + }, + ], + }, ]; }; diff --git a/src/router/methods/navigate.js b/src/router/methods/navigate.js index b80d31769..1cdd87aac 100644 --- a/src/router/methods/navigate.js +++ b/src/router/methods/navigate.js @@ -70,7 +70,13 @@ export async function navigateWithSaving(scenario, currentPageName) { export async function navigateWithPageData(scenario, currentPageName, pageData = {}) { const nextPage = getDestination(currentPageName, scenario); - await savePageData(nextPage.name, pageData); + + if (pageData && pageData.bailoutCode) { + pageData.AppName = "FixMyGlass"; + await savePageData(currentPageName, pageData); + } else { + await savePageData(nextPage.name, pageData); + } return await navigate(scenario, currentPageName, true); } diff --git a/src/router/methods/routes.js b/src/router/methods/routes.js index 88cc17f94..34056ba76 100644 --- a/src/router/methods/routes.js +++ b/src/router/methods/routes.js @@ -51,6 +51,8 @@ export const routes = [ createRoute(routeData.RECALIBRATION_INFO), createRoute(routeData.COVERAGE_STATEMENT), createRoute(routeData.VERIFY_DETAILS), + createRoute(routeData.BAILOUT), + createRoute(routeData.BAILOUT_SUCCESS), // Virtual pages (resolve to a non-virtual page.) createVirtualRoute(routeData.LANDING, landingBeforeEnter), createVirtualRoute(routeData.HERITAGE, heritageBeforeEnter), diff --git a/src/store/index.js b/src/store/index.js index 18b34f023..5b7449c4b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -217,6 +217,7 @@ const getDefaultState = () => { affiliateCookies: [], loggingOption: false, hasAlreadyTriggeredError: false, + bailoutCode: null, }, idempotencyKeyFields: { referralCorrelationId: null, @@ -486,9 +487,11 @@ export const mutations = { state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate; }, updateServiceZip(state, serviceZipInfo) { - state.order.serviceLocation.state = serviceZipInfo.state; - state.order.serviceLocation.zipCode = serviceZipInfo.zipCode; - state.order.serviceLocation.zipCodeCtu = serviceZipInfo.zipCodeCtu; + state.order.serviceLocation.state = serviceZipInfo.state || serviceZipInfo.payload?.state; + state.order.serviceLocation.zipCode = + serviceZipInfo.zipCode || serviceZipInfo.payload?.zipCode; + state.order.serviceLocation.zipCodeCtu = + serviceZipInfo.zipCodeCtu || serviceZipInfo.payload?.zipCodeCtu; }, updateServiceLocation(state, serviceLocationInfo) { state.order.serviceLocation.address = serviceLocationInfo.address; @@ -973,6 +976,9 @@ export const mutations = { state.idempotencyKeyFields.totalInCents = totalInCents; state.idempotencyKeyFields.expiryTime = expiryTime; }, + updateBailoutCode(state, bailoutCode) { + state.applicationUser.bailoutCode = bailoutCode; + }, }; // Export Getters @@ -1926,21 +1932,38 @@ export const actions = { // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); - const response = await globalMethods.callHttpClient({ - method: endpoints.GetPartsOrQuestions.method, - endpoint: endpoints.GetPartsOrQuestions.url, - payload: { - carId: carId, - glassPieces: glassArrayForPayload, - zip: zipCode, - vin: vin, - serviceType: serviceType, - referralSeqNumber: referralSeqNumber, - parentAccountNumber: parentAccountNumber, - }, - logApiCall: true, - pageNameToLog: pageNameToLog, - }); + const response = await globalMethods + .callHttpClient({ + method: endpoints.GetPartsOrQuestions.method, + endpoint: endpoints.GetPartsOrQuestions.url, + payload: { + carId: carId, + glassPieces: glassArrayForPayload, + zip: zipCode, + vin: vin, + serviceType: serviceType, + referralSeqNumber: referralSeqNumber, + parentAccountNumber: parentAccountNumber, + }, + logApiCall: true, + pageNameToLog: pageNameToLog, + }) + .catch((error) => { + if (error.status == 500) { + return { PartsNotFound: true }; + } + }); + + // Triggers bailout + if (response.PartsNotFound) { + return response; + } + + // Check if we only have MISC parts to trigger bailout + const miscPartsResponse = checkIfMiscParts(response.data.partsOrQuestions); + if (miscPartsResponse.PartsNotFound) { + return miscPartsResponse; + } // Flatten location and name properties response.data.partsOrQuestions = convertGlassPieceNamingFromApi( @@ -3846,6 +3869,10 @@ export const actions = { context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey); } }, + + saveBailoutCode(context, bailoutCode) { + context.commit(storeMutations.UPDATE_BAILOUT_CODE, bailoutCode); + }, }; export default createStore({ @@ -4387,3 +4414,17 @@ const timeSlotCallFlags = { shop: false, mobile: false, }; + +function checkIfMiscParts(partsOrQuestions) { + if (!partsOrQuestions || partsOrQuestions.length === 0) { + return { PartsNotFound: true }; + } else if ( + partsOrQuestions.length === 1 && + partsOrQuestions[0].parts && + partsOrQuestions[0].parts.length === 1 && + partsOrQuestions[0].parts[0].partNumber.startsWith("MISC") + ) { + return { PartsNotFound: true }; + } + return { PartsNotFound: false }; +}