diff --git a/azure-pipelines-automated-testing.yml b/azure-pipelines-automated-testing.yml index 8defd878..d267877f 100644 --- a/azure-pipelines-automated-testing.yml +++ b/azure-pipelines-automated-testing.yml @@ -47,7 +47,7 @@ stages: targetUrl: $(BASE_URL) dockerFileName: 'Dockerfile.playwright' isRegression: ${{ variables.IS_REGRESSION }} - filterTags: 'Liberty Mutual' + filterTags: 'Liberty Mutual|Safeco' playwrightTestsPath: 'playwright-tests' npmServePath: '.' npmrcPath: 'playwright-tests/.npmrc' diff --git a/src/constants/bailoutCode.js b/src/constants/bailoutCode.js index 2cdd8326..26e85ec7 100644 --- a/src/constants/bailoutCode.js +++ b/src/constants/bailoutCode.js @@ -18,7 +18,9 @@ const bailoutCode = Object.freeze({ ApiError: 15, RouterError: 16, CoverageCancelledByUser: 17, - NeedHelp: 18, + FmgPartNotFound: 18, + NeedHelp: 19, + YMMNotFound: 20, }); export default bailoutCode; diff --git a/src/constants/bailoutMessage.js b/src/constants/bailoutMessage.js index b08bb9d9..4c7a73ee 100644 --- a/src/constants/bailoutMessage.js +++ b/src/constants/bailoutMessage.js @@ -101,7 +101,11 @@ const bailoutMessage = Object.freeze({ NeedHelp: () => ({ code: bailoutCode.NeedHelp, message: 'User clicked the Need Help link in the app.' - }) + }), + YMMNotFound: () => ({ + code: bailoutCode.YMMNotFound, + message: 'Vin Required vehicle' + }) }); export default bailoutMessage; diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 59515bd3..c3c14480 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -43,7 +43,7 @@ const endpoints = Object.freeze({ method: 'POST' }, GetMobilePremiumFee: { - url: `${PARTS_V1_BASE_URL}/mobile-premium-fee`, + url: `${PARTS_V1_BASE_URL}/mobile-premium-fee`, method: 'GET' }, GetVehicleYears: { @@ -63,15 +63,19 @@ const endpoints = Object.freeze({ method: 'GET' }, GetDamageOptions: { - url: `${PARTS_V1_BASE_URL}/damage-options`, + url: `${PARTS_V1_BASE_URL}/damage-options`, method: 'GET' }, GetPartsOrQuestions: { - url: `${PARTS_V1_BASE_URL}/parts-or-questions`, + url: `${PARTS_V1_BASE_URL}/parts-or-questions`, + method: 'POST' + }, + GetPartsOrQuestionsVinRequired: { + url: `${PARTS_V2_BASE_URL}/parts-or-questions`, method: 'POST' }, GetParts: { - url: `${PARTS_V1_BASE_URL}/parts`, + url: `${PARTS_V1_BASE_URL}/parts`, method: 'POST' }, GetITACPriceOrderItems: { @@ -100,19 +104,19 @@ const endpoints = Object.freeze({ method: 'GET' }, GetCapabilityQuestions: { - url: `${PARTS_V1_BASE_URL}/capability-questions`, + url: `${PARTS_V1_BASE_URL}/capability-questions`, method: 'GET' }, GetPartFromCapabilityAnswer: { - url: `${PARTS_V1_BASE_URL}/part-from-capability-answer`, + url: `${PARTS_V1_BASE_URL}/part-from-capability-answer`, method: 'POST' }, GetWipers: { - url: `${PARTS_V1_BASE_URL}/wipers`, + url: `${PARTS_V1_BASE_URL}/wipers`, method: 'GET' }, GetRainDefense: { - url: `${PARTS_V1_BASE_URL}/rain-repel`, + url: `${PARTS_V1_BASE_URL}/rain-repel`, method: 'GET' }, GetRecalParts: { @@ -124,19 +128,19 @@ const endpoints = Object.freeze({ zipCode, applicationName, referralSequenceNumber - ) => `${PARTS_V1_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, + ) => `${PARTS_V1_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, method: 'GET' }, GetGlassFees: { - url: `${PARTS_V1_BASE_URL}/glass-fees`, + url: `${PARTS_V1_BASE_URL}/glass-fees`, method: 'GET' }, GetSupportingItems: { - url: `${PARTS_V1_BASE_URL}/supporting-items`, + url: `${PARTS_V1_BASE_URL}/supporting-items`, method: 'POST' }, GetMobileFeePart: { - url: `${PARTS_V2_BASE_URL}/mobile-fee`, + url: `${PARTS_V2_BASE_URL}/mobile-fee`, method: 'GET' }, GetServiceabilityDetails: { diff --git a/src/helpers/order-helper.js b/src/helpers/order-helper.js index cbc5c890..449f79b6 100644 --- a/src/helpers/order-helper.js +++ b/src/helpers/order-helper.js @@ -24,7 +24,8 @@ export async function saveSession({ shouldAwaitSaveSessionQueue = false, submitA store.setSaveSessionPromise(saveSessionPromise); - if (!store.applicationUser.savedSessionId || shouldAwaitSaveSessionQueue) { + // This should await anytime the referral number is not set, or the shouldAwaitSaveSessionQueue is set to true. + if (!store.order.referralNumber || shouldAwaitSaveSessionQueue) { await saveSessionPromise; } } diff --git a/src/layouts/address-lookup/address-lookup.vue b/src/layouts/address-lookup/address-lookup.vue index 92569c91..52fe0050 100644 --- a/src/layouts/address-lookup/address-lookup.vue +++ b/src/layouts/address-lookup/address-lookup.vue @@ -92,6 +92,7 @@ import { import vinPagesMixin from '@/mixins/vin-pages-mixin'; import { useMainStore } from '@/store/index.js'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; +import bailoutMessage from '@/constants/bailoutMessage'; export default { name: 'address-lookup', @@ -335,10 +336,7 @@ export default { // If a different vehicle is found than the one entered and the selected glass // is not available for that vehicle then navigate back to "vehicle-damage" // display vehicle changed alert on that page. - if ( - this.isCarIdDifferent - && !this.isSelectedGlassAvailableForVehicle - ) { + if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { this.$router.navigate( this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS, this.$route, @@ -346,6 +344,13 @@ export default { { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true } ); } else if (matchingCars.length === 1) { + if (this.mainStore.order.vehicle.vinRequired) { + this.mainStore.setBailout(bailoutMessage.YMMNotFound()); + return this.$router.navigate( + this.navigationScenarios.BAILOUT, + this.$route + ); + } await this.navigateForwardWithSingleCarMatch(); } else { this.$router.navigate( diff --git a/src/layouts/address-vehicles/address-vehicles.vue b/src/layouts/address-vehicles/address-vehicles.vue index a04d1216..7b651e7b 100644 --- a/src/layouts/address-vehicles/address-vehicles.vue +++ b/src/layouts/address-vehicles/address-vehicles.vue @@ -256,6 +256,14 @@ export default { false ); + if (this.mainStore.order.vehicle.vinRequired) { + this.mainStore.setBailout(bailoutMessage.YMMNotFound()); + return this.$router.navigate( + this.navigationScenarios.BAILOUT, + this.$route + ); + } + await this.navigateForward(); }, async navigateForward() { diff --git a/src/layouts/bailout-page/__snapshots__/bailout-page.spec.js.snap b/src/layouts/bailout-page/__snapshots__/bailout-page.spec.js.snap index b191b402..52ff87d6 100644 --- a/src/layouts/bailout-page/__snapshots__/bailout-page.spec.js.snap +++ b/src/layouts/bailout-page/__snapshots__/bailout-page.spec.js.snap @@ -22,6 +22,7 @@ Object { "disclaimerText": "DisclaimerWidget", "noTpa": "ContentGroupNoTPAWidget", "notSeeingPreferredShop": "ContentGroupNotSeeingPreferredShop", + "ymmNotFound": "YMMNotFoundWidget", }, } `; diff --git a/src/layouts/bailout-page/bailout-page.spec.js b/src/layouts/bailout-page/bailout-page.spec.js index 3ccd149b..83841efb 100644 --- a/src/layouts/bailout-page/bailout-page.spec.js +++ b/src/layouts/bailout-page/bailout-page.spec.js @@ -211,6 +211,27 @@ describe('Bailout page', () => { // Assert expect(name).toBe(expected); }); + test('returns YMMNotFound widget name when bailout code is BailoutCode.YMMNotFound', () => { + // Arrange + const mainInitialState = { + applicationUser: { + pageData: { + 'bailout-page': { + bailoutCode: bailoutCode.YMMNotFound + } + } + } + }; + const initialData = { }; + const { wrapper } = getMountedComponent(mainInitialState, initialData); + const expected = 'YMMNotFoundWidget'; + + // Act + const name = wrapper.vm.subHeaderCmsWidgetName; + + // Assert + expect(name).toBe(expected); + }); test('returns default widget name when bailoutCode does not match any other content bailout codes', () => { // Arrange const mainInitialState = { diff --git a/src/layouts/bailout-page/bailout-page.vue b/src/layouts/bailout-page/bailout-page.vue index 8e7fa5dd..0e780a5a 100644 --- a/src/layouts/bailout-page/bailout-page.vue +++ b/src/layouts/bailout-page/bailout-page.vue @@ -18,7 +18,7 @@ :contentProperty="subHeaderContentProperty" :stripRteStyle="stripRteStyle" :subContentProperty="subContentProperty" - class="sub-header-content mt-2 mb-5" + class="sub-header-content mb-5" justification="left" />
@@ -159,6 +159,7 @@ export default { defaultSiteHeader: 'SiteSubHeaderWidget', noTpa: 'ContentGroupNoTPAWidget', notSeeingPreferredShop: 'ContentGroupNotSeeingPreferredShop', + ymmNotFound: 'YMMNotFoundWidget', disclaimerText: 'DisclaimerWidget' }, rules: { @@ -172,20 +173,21 @@ export default { computed: { subHeaderCmsWidgetName() { switch (this.bailout.bailoutCode) { - case BailoutCode.TPANotEnabled: - return this.widget.noTpa; - case BailoutCode.DoNotSeeMyShop: return this.widget.notSeeingPreferredShop; - + case BailoutCode.TPANotEnabled: + return this.widget.noTpa; + case BailoutCode.YMMNotFound: + return this.widget.ymmNotFound; default: return this.widget.defaultSiteHeader; } }, subHeaderContentProperty() { switch (this.bailout.bailoutCode) { - case BailoutCode.TPANotEnabled: case BailoutCode.DoNotSeeMyShop: + case BailoutCode.TPANotEnabled: + case BailoutCode.YMMNotFound: return widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT; default: @@ -194,8 +196,9 @@ export default { }, subContentProperty() { switch (this.bailout.bailoutCode) { - case BailoutCode.TPANotEnabled: case BailoutCode.DoNotSeeMyShop: + case BailoutCode.TPANotEnabled: + case BailoutCode.YMMNotFound: return widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT; default: @@ -259,6 +262,10 @@ export default { min-height: 1px; padding-left: .9375rem; padding-right: .9375rem; + + .sub-header-content { + margin-top: 1.25rem; + } } } diff --git a/src/layouts/contact-confirmation/contact-confirmation.vue b/src/layouts/contact-confirmation/contact-confirmation.vue index 2819efd6..a21f7a1a 100644 --- a/src/layouts/contact-confirmation/contact-confirmation.vue +++ b/src/layouts/contact-confirmation/contact-confirmation.vue @@ -7,7 +7,9 @@
@@ -20,6 +22,8 @@ import siteHeader from '@/iss-components/site-header/site-header.vue'; import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue'; // Supporting files +import BailoutCode from '@/constants/bailoutCode'; +import widgetFields from '@/constants/cms-widget-fields.js'; import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import issPageValues from '@/router/router-constants/issPage-values.js'; import settleAllPromises from '@/helpers/layout-helper'; @@ -55,12 +59,44 @@ export default { return { mainStore }; }, data() { - const contactConfirmationData = useMainStore().pageData(issPageValues.CONTACT_CONFIRMATION); + const bailout = useMainStore().pageData(issPageValues.BAILOUT_PAGE); return { - contactConfirmationModel: contactConfirmationData + bailout: bailout ?? { bailoutCode: BailoutCode.Unknown }, + contactConfirmationModel: useMainStore().pageData(issPageValues.CONTACT_CONFIRMATION), + widget: { + defaultSiteHeader: 'SiteSubHeaderWidget', + ymmNotFound: 'YMMNotFoundWidget' + }, }; }, - computed: {}, + computed: { + subHeaderCmsWidgetName() { + switch (this.bailout.bailoutCode) { + case BailoutCode.YMMNotFound: + return this.widget.ymmNotFound; + default: + return this.widget.defaultSiteHeader; + } + }, + subHeaderContentProperty() { + switch (this.bailout.bailoutCode) { + case BailoutCode.YMMNotFound: + return widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT; + + default: + return widgetFields.SUB_HEADER_WIDGET.SUB_HEADER_TEXT; + } + }, + subContentProperty() { + switch (this.bailout.bailoutCode) { + case BailoutCode.YMMNotFound: + return widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT; + + default: + return widgetFields.SUB_HEADER_WIDGET.SECONDARY_TEXT; + } + }, + }, methods: {} }; diff --git a/src/layouts/entry-page/entry-page.vue b/src/layouts/entry-page/entry-page.vue index ce114928..89609dd7 100644 --- a/src/layouts/entry-page/entry-page.vue +++ b/src/layouts/entry-page/entry-page.vue @@ -13,9 +13,9 @@ import { validateISSClientTag, validateISSClientSignature } from '@/helpers/clie import { getISSCookie, updateOrCreateISSCookie } from '@/helpers/cookie-helper.js'; import { useMainStore } from '@/store'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; -import applicationConfig from '@/constants/application-config'; import { toPossessive } from '@/helpers/text-helper'; import analyticsMixin from '@/mixins/analytics-mixin'; +import routerParams from '@/router/router-constants/router-params'; export default { name: 'entry-page', @@ -80,23 +80,19 @@ export default { } this.mainStore.applicationUser.coverageAttempts = 0; - if ( - applicationConfig.CURRENT_ENVIRONMENT === 'Localhost' - || applicationConfig.CURRENT_ENVIRONMENT === 'Dev' - || applicationConfig.CURRENT_ENVIRONMENT === 'SysTest' - ) { - this.navigateForward(); - } else { - // Forced full location redirect here. We do not want the entry page as part of the router/flow/path history. - window.location = `/?issPage=${issPageValues.WELCOME_PAGE}`; - } + + // Forced full location redirect here. We do not want the entry page as part of the router/flow/path history. + window.location = `/?issPage=${issPageValues.WELCOME_PAGE}`; }, methods: { navigateForward() { + // If we use navigateForward we need to skip saving session (creating referral). this.$router.navigate( this.navigationScenarios.MOVE_FORWARD_ENTRY_PAGE, - this.$route + this.$route, + {}, + { [routerParams.SKIP_SAVE_SESSION]: true } ); }, parseQueryParms() { diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue index aeb58dcb..d70c7b39 100644 --- a/src/layouts/license-plate-lookup/license-plate-lookup.vue +++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue @@ -76,6 +76,7 @@ import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import settleAllPromises from '@/helpers/layout-helper'; import { useMainStore } from '@/store'; +import bailoutMessage from '@/constants/bailoutMessage'; import errorMessages from '@/constants/error-messages'; import { required } from '@/helpers/validation-rules'; import { defineRule, Form } from 'vee-validate'; @@ -303,6 +304,14 @@ export default { } }); + if (this.mainStore.order.vehicle.vinRequired) { + this.mainStore.setBailout(bailoutMessage.YMMNotFound()); + return this.$router.navigate( + this.navigationScenarios.BAILOUT, + this.$route + ); + } + return this.navigateForward(); }, async navigateForward() { diff --git a/src/layouts/vehicle-lookup/vin-lookup-methods/vin-lookup-methods.vue b/src/layouts/vehicle-lookup/vin-lookup-methods/vin-lookup-methods.vue index fa5868aa..2ebf89a4 100644 --- a/src/layouts/vehicle-lookup/vin-lookup-methods/vin-lookup-methods.vue +++ b/src/layouts/vehicle-lookup/vin-lookup-methods/vin-lookup-methods.vue @@ -65,6 +65,15 @@ export default { const homeAddressIndex = answers.findIndex(answer => answer.Name === vinLookupMethodSelections.HOMEADDRESS); answers.splice(homeAddressIndex, 1); } + + const isVinRequired = useMainStore().order.vehicle.vinRequired; + if (isVinRequired) { + const vinIndex = answers.findIndex(answer => answer.Name === vinLookupMethodSelections.NOVIN); + if (vinIndex >= 0) { + answers.splice(vinIndex, 1); + } + } + this.answersFromCms = answers; } }, diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js index 5e82c2f8..820d2820 100644 --- a/src/layouts/vin-lookup/vin-lookup.spec.js +++ b/src/layouts/vin-lookup/vin-lookup.spec.js @@ -50,6 +50,10 @@ const getPartsOrQuestions = { methodName: 'getPartsOrQuestions', mockResponse: null }; +const getPartsOrQuestionsVinRequired = { + methodName: 'getPartsOrQuestionsVinRequired', + mockResponse: null +}; const vehicleWithPartQuestionsMockResponse = { data: { @@ -372,6 +376,8 @@ describe('vin-lookup.vue', () => { .mockResolvedValue(lookupVehicleByVin.mockResponse); jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName) .mockResolvedValue(getPartsOrQuestions.mockResponse); + jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName) + .mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse); const { container } = render(VinLookupComponent, mountOptions); @@ -403,6 +409,8 @@ describe('vin-lookup.vue', () => { .mockResolvedValue(lookupVehicleByVin.mockResponse); jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName) .mockResolvedValue(getPartsOrQuestions.mockResponse); + jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName) + .mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse); const { container } = render(VinLookupComponent, mountOptions); @@ -436,6 +444,8 @@ describe('vin-lookup.vue', () => { .mockResolvedValue(lookupVehicleByVin.mockResponse); jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName) .mockResolvedValue(getPartsOrQuestions.mockResponse); + jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName) + .mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse); store.getCapabilityQuestions.mockResolvedValueOnce({ data: [] }); const { container } = render(VinLookupComponent, mountOptions); @@ -469,6 +479,8 @@ describe('vin-lookup.vue', () => { .mockResolvedValue(lookupVehicleByVin.mockResponse); jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName) .mockResolvedValue(getPartsOrQuestions.mockResponse); + jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName) + .mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse); const { container } = render(VinLookupComponent, mountOptions); @@ -505,6 +517,8 @@ describe('vin-lookup.vue', () => { .mockResolvedValue(lookupVehicleByVin.mockResponse); jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName) .mockResolvedValue(getPartsOrQuestions.mockResponse); + jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName) + .mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse); const { container } = render(VinLookupComponent, mountOptions); diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 9057e864..1729c3c7 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -176,6 +176,14 @@ export default { this.$refs.siteFooter.enableForwardAction(); showIssLoadingModal(true); + if (this.mainStore.order.vehicle.vinRequired) { + this.mainStore.setBailout(bailoutMessage.YMMNotFound()); + return this.$router.navigate( + this.navigationScenarios.BAILOUT, + this.$route + ); + } + if (this.needToLookupVehicle) { try { const vehicleLookupResponse = await this.lookupVehicleByVin(this.vin); @@ -262,6 +270,30 @@ export default { return; } + /* + * If the vehicle is vinRequired, we need to call getPartsOrQuestionsVinRequired() to get the parts or questions. + * If the vehicle is not vinRequired, we can call getPartsOrQuestions() to get the parts or questions. + */ + /* + if (this.mainStore.order.vehicle.vinRequired) { + try { + const partsOrQuestionsResponse = await this.getPartsOrQuestionsVinRequired(); + await this.navigateForward( + partsOrQuestionsResponse.data.partsOrQuestions, + this + ); + } catch (e) { + this.mainStore.setBailout(bailoutMessage.HeavyTruckVehicle(this.vehicleFromLookup.carId)); + this.$router.navigate( + this.navigationScenarios.BAILOUT, + this.$route + ); + throw e; + } + + return; + } + */ const partsOrQuestionsResponse = await this.getPartsOrQuestions(); // Comes from vehicleQuestionsMixin.navigateForward() diff --git a/src/layouts/welcome-page/welcome-page.vue b/src/layouts/welcome-page/welcome-page.vue index 92ddeab2..38b35558 100644 --- a/src/layouts/welcome-page/welcome-page.vue +++ b/src/layouts/welcome-page/welcome-page.vue @@ -371,7 +371,6 @@ export default { } } - await saveSession({ shouldAwaitSaveSessionQueue: true, bailoutOnError: true }) this.navigateForward(); }, async configureZip() { @@ -388,6 +387,7 @@ export default { navigateForward() { if ((this.mainStore.applicationUser.duplicateOrders?.length > 0 ?? false) && !this.answeredContinueModal) { + // Duplicate orders found; navigating to duplicate check page and skipping save session. this.$router.navigate( this.navigationScenarios.CLICKED_FORWARD_WITH_DUPLICATES, this.$route, diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 956ef0af..2330dde9 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -68,7 +68,7 @@ export default { const store = useMainStore(); const issConfig = store.issConfig; const currentPageName = this.getPageNameByQueryString(); - //await this.validateSession(); + // await this.validateSession(); const submittedOrder = store.getSubmittedOrder(); const hasSubmittedOrder = store.hasSubmittedOrder(); @@ -98,7 +98,7 @@ export default { const store = useMainStore(); const issConfig = store.issConfig; const currentPageName = this.getPageNameByQueryString(); - //await this.validateSession(); + // await this.validateSession(); const submittedOrder = store.getSubmittedOrder(); const hasSubmittedOrder = store.hasSubmittedOrder(); @@ -246,6 +246,18 @@ export default { payload.vehicleStyle = ""; } + if (isDefined(order.vehicle.carId)) { + payload.vehicleCarId = order.vehicle.carId; + } else { + payload.vehicleCarId = ""; + } + + if (isDefined(order.vehicle.vinRequired)) { + payload.vehicleIsVinRequired = order.vehicle.vinRequired; + } else { + payload.vehicleIsVinRequired = ""; + } + // Glass pieces const glass = order.damage.glassToReplace ?? []; if (glass.length === 0) { @@ -257,7 +269,7 @@ export default { payload.glassToReplace = glassString; } - //EON + // EON if (order.eon) { payload.eon = order.eon; } else { @@ -555,7 +567,6 @@ export default { }, async initSession() { - regenerateDeviceId(); regenerateUserId(); diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js index b4bdad07..54331ae6 100644 --- a/src/mixins/vehicle-questions-mixin.js +++ b/src/mixins/vehicle-questions-mixin.js @@ -355,6 +355,9 @@ export default { async getPartsOrQuestions() { return useMainStore().getPartsOrQuestions(); }, + async getPartsOrQuestionsVinRequired() { + return useMainStore().getPartsOrQuestionsVinRequired(); + }, // Can't use `this` because navigateForward is also called from vin-pages-mixin async navigateForward(partsOrQuestions, vm) { diff --git a/src/router/index.js b/src/router/index.js index 1fbe148f..25400a08 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -46,6 +46,7 @@ const routes = [ if (getISSCookie() !== null && !isSavedSessionStillActive()) { // await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE); await GoToStartOn404(next); + return null; } // Process ISS cookie. @@ -66,9 +67,10 @@ const routes = [ if (!arePagePrerequisitesValid(component)) { await GoToStartOn404(next); + return null; } - return next({name: issPageToUse, query: to.query, params: to.params}); + return next({name: issPageToUse, query: to.query, params: to.params, state: to.state}); } var routeData = []; @@ -123,13 +125,15 @@ const routes = [ const tempMsgCopy = 'Pre Requisites failed, need to handle. Default is Welcome Page.'; const tempMsgHeadline = `${issPageToUse}: pre-req failed...`; await GoToStartOn404(next, tempMsgCopy, tempMsgHeadline); + return null; } // Assign current query string parameters, as well as our issPage one. next({ name: routeData[0].name, query: Object.assign(to.query, {issPage: routeData[0].name}), - params: to.params + params: to.params, + state: to.state }); return null; } @@ -146,11 +150,29 @@ const router = createRouter({ }); router.beforeEach(async (to, from) => { + const store = useMainStore(); const fromQueryPage = from.query?.issPage; + if (fromQueryPage === undefined) { showIssLoadingModal(true); } + const callSaveSession = !store.order?.referralNumber && (to.name === issPageValues.VEHICLE_SELECTION || to.name === issPageValues.POLICY_VEHICLES); + if (callSaveSession) { + // Forcing a synchronous savesession call here to create the referral for the first time. + // AfterEach does not support synchronous calls that block navigation. Which is why this is in the beforeEach method. + // This only needs to be called once in a certain location, all other saveSession calls are async in afterEach (except the last one on order submission) + // NOTE: This call should be creating the referral number. After this point in the site flow, referral number is critical for several pieces of logic and logging. + // Therefore, we want to force a bailout here if it errors out. + showIssLoadingModal(true); + try { + await saveSession({ shouldAwaitSaveSessionQueue: true, bailoutOnError: true }); + } + finally { + showIssLoadingModal(false); + } + } + const toQueryPage = to.query?.issPage; const notToPayInAdvanceReturn = toQueryPage !== issPageValues.PAYMENT_RETURN; const isInIframe = window !== window.top || fromQueryPage === issPageValues.PAYMENT_PAGE; @@ -168,7 +190,6 @@ router.beforeEach(async (to, from) => { return false; } - const store = useMainStore(); // Prevent navigating backwards if we enter a bailout that we are not allowed to go back on if (store.isBailout && from.name === issPageValues.BAILOUT_PAGE && to.name !== 'root' && to.name !== issPageValues.CONTACT_CONFIRMATION && !canBailoutNavigateBack()) { @@ -196,11 +217,15 @@ router.afterEach(async (to, from) => { await analyticsMixin.methods.validateSession(); - const skipSaveSession = !!router.options.history.state[routerParams.SKIP_SAVE_SESSION]; - if (from.name !== undefined && !skipSaveSession && !store.hasSubmittedOrder()) { - await saveSession({ bailoutOnError: from.name === issPageValues.ENTRY_PAGE}); - } + const skipSaveSession = !!( + to.state?.[routerParams.SKIP_SAVE_SESSION] + ?? router.options.history.state?.[routerParams.SKIP_SAVE_SESSION] + ); + if (from.name !== undefined && !skipSaveSession && !store.hasSubmittedOrder()) { + await saveSession({ bailoutOnError: false }); + } + document.title = routerTitles[to.query.issPage] || 'Safelite Solutions®'; if (shouldRunExperiments) { diff --git a/src/router/router.afterEach.spec.js b/src/router/router.afterEach.spec.js new file mode 100644 index 00000000..6f2aee8f --- /dev/null +++ b/src/router/router.afterEach.spec.js @@ -0,0 +1,115 @@ +import issPageValues from '@/router/router-constants/issPage-values'; +import routerParams from '@/router/router-constants/router-params'; + +let router; +let saveSession; +let mockAfterEachHandler; +let mockStore; + +jest.mock('vue-router', () => ({ + createWebHistory: jest.fn(() => ({ state: {} })), + createRouter: jest.fn((options) => ({ + options, + beforeEach: jest.fn(), + afterEach: jest.fn((handler) => { + mockAfterEachHandler = handler; + }), + addRoute: jest.fn(), + getRoutes: jest.fn(() => []), + hasRoute: jest.fn(() => true), + push: jest.fn(), + go: jest.fn(), + currentRoute: { value: { query: { issPage: 'welcomePage' } } } + })) +})); + +jest.mock('@/helpers/order-helper.js', () => ({ + saveSession: jest.fn(() => Promise.resolve()) +})); + +jest.mock('@/mixins/analytics-mixin', () => ({ + __esModule: true, + default: { + methods: { + validateSession: jest.fn(() => Promise.resolve()), + logDigitalConsumer: jest.fn(), + pushIssSessionData: jest.fn(), + pushPageViewToGA: jest.fn(), + pushExperimentsToDataLayer: jest.fn(), + pushOrderToDataLayer: jest.fn() + } + } +})); + +jest.mock('@/store', () => ({ + useMainStore: jest.fn(() => mockStore) +})); + +describe('Router afterEach skipSaveSession', () => { + beforeAll(() => { + router = require('@/router/').default; + ({ saveSession } = require('@/helpers/order-helper.js')); + }); + + beforeEach(() => { + mockStore = { + applicationUser: { triggeredSiteEntry: true }, + runExperimentsForTrigger: jest.fn(() => Promise.resolve()), + updateLastPageVisited: jest.fn(), + clearSaveSessionPromise: jest.fn(), + hasSubmittedOrder: jest.fn(() => false) + }; + + router.options.history.state = {}; + jest.clearAllMocks(); + }); + + it('should skip saveSession when skipSaveSession is true on to.state', async () => { + await mockAfterEachHandler( + { + name: issPageValues.WELCOME_PAGE, + query: { issPage: issPageValues.WELCOME_PAGE }, + state: { [routerParams.SKIP_SAVE_SESSION]: true } + }, + { + name: issPageValues.ENTRY_PAGE, + redirectedFrom: undefined + } + ); + + expect(saveSession).not.toHaveBeenCalled(); + }); + + it('should skip saveSession when skipSaveSession is true on history state', async () => { + router.options.history.state = { [routerParams.SKIP_SAVE_SESSION]: true }; + + await mockAfterEachHandler( + { + name: issPageValues.WELCOME_PAGE, + query: { issPage: issPageValues.WELCOME_PAGE } + }, + { + name: issPageValues.ENTRY_PAGE, + redirectedFrom: undefined + } + ); + + expect(saveSession).not.toHaveBeenCalled(); + }); + + it('should call saveSession when skipSaveSession is not present', async () => { + await mockAfterEachHandler( + { + name: issPageValues.WELCOME_PAGE, + query: { issPage: issPageValues.WELCOME_PAGE } + }, + { + name: issPageValues.ENTRY_PAGE, + redirectedFrom: undefined + } + ); + + expect(saveSession).toHaveBeenCalledTimes(1); + expect(saveSession).toHaveBeenCalledWith({ bailoutOnError: false }); + }); +}); \ No newline at end of file diff --git a/src/router/router.beforeEach.spec.js b/src/router/router.beforeEach.spec.js new file mode 100644 index 00000000..85c9160e --- /dev/null +++ b/src/router/router.beforeEach.spec.js @@ -0,0 +1,72 @@ +import issPageValues from '@/router/router-constants/issPage-values'; + +let router; +let saveSession; +let showIssLoadingModal; +let mockBeforeEachHandler; +let mockStore; + +jest.mock('vue-router', () => ({ + createWebHistory: jest.fn(() => ({ state: {} })), + createRouter: jest.fn((options) => ({ + options, + beforeEach: jest.fn((handler) => { + mockBeforeEachHandler = handler; + }), + afterEach: jest.fn(), + addRoute: jest.fn(), + getRoutes: jest.fn(() => []), + hasRoute: jest.fn(() => true), + push: jest.fn(), + go: jest.fn(), + currentRoute: { value: { query: { issPage: 'welcome-page' } } } + })) +})); + +jest.mock('@/helpers/order-helper.js', () => ({ + saveSession: jest.fn(() => Promise.resolve()) +})); + +jest.mock('@/helpers/loading-modal-helper', () => jest.fn()); + +jest.mock('@/store', () => ({ + useMainStore: jest.fn(() => mockStore) +})); + +describe('Router beforeEach callSaveSession', () => { + beforeAll(() => { + router = require('@/router/').default; + ({ saveSession } = require('@/helpers/order-helper.js')); + const loadingModalModule = require('@/helpers/loading-modal-helper'); + showIssLoadingModal = loadingModalModule.default || loadingModalModule; + }); + + beforeEach(() => { + mockStore = { + order: {}, + isBailout: false + }; + + jest.clearAllMocks(); + }); + + it('calls saveSession synchronously when entering vehicle-selection without a referral number', async () => { + const result = await mockBeforeEachHandler( + { + name: issPageValues.VEHICLE_SELECTION, + query: { issPage: issPageValues.VEHICLE_SELECTION }, + href: '/?issPage=vehicle-selection' + }, + { + name: issPageValues.WELCOME_PAGE, + query: { issPage: issPageValues.WELCOME_PAGE } + } + ); + + expect(saveSession).toHaveBeenCalledTimes(1); + expect(saveSession).toHaveBeenCalledWith({ shouldAwaitSaveSessionQueue: true, bailoutOnError: true }); + expect(showIssLoadingModal).toHaveBeenNthCalledWith(1, true); + expect(showIssLoadingModal).toHaveBeenNthCalledWith(2, false); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/store/index.js b/src/store/index.js index 386832f3..091ffec1 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -4,13 +4,13 @@ import bailoutCode from '@/constants/bailoutCode'; import coverageStatuses from '@/constants/coverage-statuses'; import coverageType from '@/constants/coverage-type'; import damageLocationsSelected from '@/constants/damage-locations-selected'; -import endorsementOptions from '@/constants/endorsement-options'; import endpoints from '@/constants/endpoints'; import { experimentSettings, experimentTriggers, experimentUniverses } from '@/constants/experiments'; import partNumberStrings from '@/constants/part-number-strings'; import partTypeStrings from '@/constants/part-type-strings'; import { paymentMethods } from '@/constants/payment-method-constants'; import { AppointmentTypeStrings } from '@/constants/schedule-constants'; +import submitType from '@/constants/submit-type'; import webStorageConstants from '@/constants/web-storage-constants'; import globalMethods from '@/global-methods'; import { getSessionKeyValue, getUserIdValue, deleteISSCookie, getDeviceIdValue } from '@/helpers/cookie-helper'; @@ -27,7 +27,6 @@ import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal, getHasRecalibratio import { getDateForSavedSessionTimeout } from '@/helpers/session-helper'; import { isMobileDevice } from '@/helpers/useragent-helper'; import issPageValues from '@/router/router-constants/issPage-values'; -import CoverageStatuses from '@/constants/coverage-statuses'; import { getNonFalseValuesOfPropertyInArrayOfObjects, sortArrayOfObjectsByPropertyValue } from '@/helpers/object-helper'; const storeId = 'main'; @@ -85,7 +84,10 @@ export const getDefaultState = () => ({ lastName: null }, isBigTruck: null, - canSafeliteService: null + canSafeliteService: null, + skipVINLookup: null, + skipPartQuestions: null, + vinRequired: null }, damage: { isRepair: null, @@ -768,7 +770,7 @@ export const useMainStore = defineStore({ this.updateDeductible(r.data); return r; } catch (e) { - this.updateCoverageStatus(CoverageStatuses.PENDING); + this.updateCoverageStatus(coverageStatuses.PENDING); } }, @@ -814,9 +816,7 @@ export const useMainStore = defineStore({ }, // PartsOrQuestions API Actions - async getPartsOrQuestions() { - this.resetPartsAndDependencies(); - + getPartsOrQuestionsPayload() { const { vehicle } = this; const { damage } = this; const { order } = this; @@ -828,17 +828,22 @@ export const useMainStore = defineStore({ // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); + return { + carId, + glassPieces: glassArrayForPayload, + zip: zipCode, + vin, + oemEndorsementFlag: this.hasOemEndorsement + }; + }, + async getPartsOrQuestions() { + this.resetPartsAndDependencies(); + const payload = this.getPartsOrQuestionsPayload(); const response = await globalMethods.callHttpClient({ method: endpoints.GetPartsOrQuestions.method, endpoint: endpoints.GetPartsOrQuestions.url, - payload: { - carId, - glassPieces: glassArrayForPayload, - zip: zipCode, - vin, - oemEndorsementFlag: this.hasOemEndorsement - } + payload: payload }); // Flatten location and name properties @@ -846,7 +851,21 @@ export const useMainStore = defineStore({ return response; }, + async getPartsOrQuestionsVinRequired() { + this.resetPartsAndDependencies(); + const payload = this.getPartsOrQuestionsPayload(); + const response = await globalMethods.callHttpClient({ + method: endpoints.GetPartsOrQuestionsVinRequired.method, + endpoint: endpoints.GetPartsOrQuestionsVinRequired.url, + payload: payload + }); + + // Flatten location and name properties + response.data.partsOrQuestions = convertGlassPieceNamingFromApi(response.data.partsOrQuestions); + + return response; + }, async getParts() { const { vehicle } = this.order; const { damage } = this.order; @@ -2087,6 +2106,9 @@ export const useMainStore = defineStore({ this.order.vehicle.imageColor = vehicle.imageVifColor; this.order.vehicle.isBigTruck = vehicle.isBigTruck; this.order.vehicle.canSafeliteService = vehicle.canSafeliteService; + this.order.vehicle.skipVINLookup = vehicle.skipVINLookup; + this.order.vehicle.skipPartQuestions = vehicle.skipPartQuestions; + this.order.vehicle.vinRequired = vehicle.vinRequired; this.updateSupportingItems(null); this.updateVaps(null); @@ -2221,6 +2243,9 @@ export const useMainStore = defineStore({ this.order.vehicle.registration.lastName = null; this.order.vehicle.isBigTruck = null; this.order.vehicle.canSafeliteService = null; + this.order.vehicle.skipVINLookup = null; + this.order.vehicle.skipPartQuestions = null; + this.order.vehicle.vinRequired = null; if (this.isBailout && this.bailoutCode === bailoutCode.HeavyTruckVehicle) { this.resetBailout(); @@ -3164,7 +3189,7 @@ export const useMainStore = defineStore({ return JSON.parse(window.sessionStorage.getItem(webStorageConstants.SUBMITTED_ORDER)); }, - async createSubmittedOrder(submitType) { + async createSubmittedOrder(pSubmitType) { if (this.hasSubmittedOrder()) { return; } @@ -3175,16 +3200,27 @@ export const useMainStore = defineStore({ submittedOrder.isUnverified = this.isUnverified; submittedOrder.isVerified = this.isVerified; - submittedOrder.submitType = submitType; + submittedOrder.submitType = pSubmitType; submittedOrder.payment.isPayInAdvance = this.isPayInAdvance; submittedOrder.hasRecalibrationPart = hasRecalibrationPart; // set to sessionStorage window.sessionStorage.setItem(webStorageConstants.SUBMITTED_ORDER, JSON.stringify(submittedOrder)); + let persistedBailout = null; + if (pSubmitType === submitType.BAILOUT) { + persistedBailout = this.pageData(issPageValues.BAILOUT_PAGE); + } // clear vuex this.resetState(); + if (persistedBailout) { + this.updatePageData({ + page: issPageValues.BAILOUT_PAGE, + data: persistedBailout + }); + } + // delete cookie deleteISSCookie();