diff --git a/environment-variables.js b/environment-variables.js index 55ffdf54e..e3e7ba2cd 100644 --- a/environment-variables.js +++ b/environment-variables.js @@ -12,7 +12,6 @@ const environmentVariables = { ['__VUE_APP_MY_ACCOUNT__']: "https://myaccountdev.safelite.com/", ['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx", ['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30, - ['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "", ['__VUE_APP_ADYEN_ENVIRONMENT__']: "test", ['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", ['__VUE_APP_SESSION_EXPIRATION_INTERVAL_CHECK_MILLISECONDS__']: 60000, diff --git a/public/index.html b/public/index.html index ab8f27144..0e0518fb6 100644 --- a/public/index.html +++ b/public/index.html @@ -27,7 +27,5 @@
- - <%= process.env.__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__ %> diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 6899c2aec..b5cbde3fa 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -44,6 +44,10 @@ const errorMessages = { DAMAGE_TYPES_REQUIRED: "How damage occurred is required", ZIP_CODE_REQUIRED: "Please enter your ZIP code", ZIP_CODE_FORMAT: "Zip must be 5 digits", + DATE_OF_BIRTH_REQUIRED: "Please enter your date of birth", + LOSS_TIME_REQUIRED: "Please enter your loss time", + LOSS_LOCATION_REQUIRED: "Please select your loss location", + SUBROGATION_REQUIRED: "Please make a selection", }; export { errorMessages }; diff --git a/src/constants/insurance.js b/src/constants/insurance.js index 8f8cd80fd..1439fafcb 100644 --- a/src/constants/insurance.js +++ b/src/constants/insurance.js @@ -85,3 +85,8 @@ export const parentAccountNumbers = { export const NON_MANAGED_SHOW_CLAIM_NUMBER_PARENTS = [ { name: "KentuckyFarmBureau", value: "223499" }, ]; + +export const PARENT_ACCOUNT_NUMBERS = { + STATE_FARM: "711310", + USAA: "900040", +}; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index d2fc2b0fc..a06a37bb0 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -147,6 +147,9 @@ const storeMutations = { //Affiliate Cookies UPDATE_AFFILIATE_COOKIES: "updateAffiliateCookies", + + // Submitted state (invalidates getters that read sessionStorage submittedState) + INCREMENT_SUBMITTED_STATE_REVISION: "incrementSubmittedStateRevision", }; export { storeMutations }; diff --git a/src/digital-components/date-picker-popup/date-picker-popup.vue b/src/digital-components/date-picker-popup/date-picker-popup.vue index 7f3e109ac..ea24197bc 100644 --- a/src/digital-components/date-picker-popup/date-picker-popup.vue +++ b/src/digital-components/date-picker-popup/date-picker-popup.vue @@ -26,7 +26,8 @@ :min-date="minDate" :max-date="maxDate" :auto-apply="autoApply" - :text-input="textInput" + :text-input="resolvedTextInput" + :hide-input-icon="textInputOnly" :prevent-min-max-navigation="preventMinMaxNavigation" :teleport="true" :aria-labels="{ input: questionText }" @@ -107,6 +108,12 @@ export default { type: Boolean, default: false, }, + // Force manual entry: text input enabled, calendar popup never opens + // and the calendar icon is hidden. + textInputOnly: { + type: Boolean, + default: false, + }, preventMinMaxNavigation: { type: Boolean, default: false, @@ -155,7 +162,7 @@ export default { questionText() { if (!this.cmsWidgetName) return ""; const text = this.getCmsContent - ? this.getCmsContent(this.cmsWidgetName, "BodyText") + ? this.getCmsContent(this.cmsWidgetName, "QuestionText") : ""; return this.addOptionalText ? `${text} (optional)` : text; }, @@ -193,6 +200,15 @@ export default { clearable: this.clearable, }; }, + // Forwarded to VueDatePicker's `text-input` prop. When `textInputOnly` + // is set, force text input on AND tell the picker never to open its + // menu (so focus/click on the input is a no-op visually). + resolvedTextInput() { + if (this.textInputOnly) { + return { enabled: true, openMenu: false }; + } + return this.textInput; + }, }, methods: { onDateChange(newValue) { diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index faa28376d..5d48dc5ae 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -907,7 +907,7 @@ export default { return cartItem; }, isCashItacNoComp() { - return !this.isInsurance || this.isITAC || this.isNoComp; + return !this.isInsurance || this.isItac || this.isNoComp; }, mobileFeeCartItemName() { return this.getCmsContent("MobileServiceTextWidget", "Text"); @@ -971,7 +971,7 @@ export default { const msrFeeLineItem = this.getMsrFeeLineItem; const shouldCreateMsrFeeCartItem = this.isMSRFeeApplicable && - (!this.isInsurance || !this.IsMSRFeeCoveredByInsurance) && + (this.isCashItacNoComp || !this.IsMSRFeeCoveredByInsurance) && msrFeeLineItem && this.msrFeeAmount > 0; @@ -1007,9 +1007,10 @@ export default { return this.getCmsContent("RecycleTextBlock", "Text"); }, msrToolTipBodyText() { - let cmsContentText = this.isInsurance - ? this.getCmsContent("MSRModal", "BodyText") - : this.getCmsContent("MSRModal", "BodyText2"); + let cmsContentText = + this.isInsurance && !this.isItac && !this.isNoComp + ? this.getCmsContent("MSRModal", "BodyText") + : this.getCmsContent("MSRModal", "BodyText2"); if (this.isInsurance && cmsContentText) { cmsContentText = cmsContentText.replaceAll( "{custom:mobileFee}", diff --git a/src/fmg-components/funnel-header/funnel-header.vue b/src/fmg-components/funnel-header/funnel-header.vue index 7bfa3eef6..ac6e73bde 100644 --- a/src/fmg-components/funnel-header/funnel-header.vue +++ b/src/fmg-components/funnel-header/funnel-header.vue @@ -5,8 +5,17 @@
@@ -69,6 +78,10 @@ export default { type: Boolean, default: false, }, + overrideImageSrc: { + type: String, + default: "", + }, }, setup() { const { webchatGlobalNonpersistedState, launchWebchat } = webchatHelper(); @@ -76,7 +89,7 @@ export default { }, computed: { imageSrc() { - return this.getCmsContent(this.cmsWidgetName, "LogoImage"); + return this.overrideImageSrc || this.getCmsContent(this.cmsWidgetName, "LogoImage"); }, shouldShowWebchatButton() { return this.webchatGlobalNonpersistedState.showWebchatButton; @@ -199,6 +212,13 @@ export default { @include media-breakpoint-up(md) { width: 165px; } + + &--override { + width: 200px; + @include media-breakpoint-up(md) { + width: 250px; + } + } } .webchat { diff --git a/src/layouts/duplicate-check/duplicate-check.spec.js b/src/layouts/duplicate-check/duplicate-check.spec.js index e28a749e6..3f5d6cca2 100644 --- a/src/layouts/duplicate-check/duplicate-check.spec.js +++ b/src/layouts/duplicate-check/duplicate-check.spec.js @@ -55,6 +55,15 @@ function setupMocks() { fetchCmsContentForPage.mockResolvedValue(mockCmsContent); settleAllPromises.mockResolvedValue({ cmsContent: mockCmsContent }); + const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, fieldName) => { + return mockCmsContent?.[widgetName]?.[fieldName] ?? ""; + }), + setCmsContent: jest.fn(), + }, + }; + const mountOptions = getMountOptions({ route: { name: "duplicate-check", query: {}, params: {} }, router: { @@ -62,6 +71,7 @@ function setupMocks() { navigateWithSaving: jest.fn(), navigateWithoutSaving: jest.fn(), }, + mixins: [mockMixin], }); const wrapper = shallowMount(duplicateCheck, mountOptions); diff --git a/src/layouts/duplicate-check/duplicate-check.vue b/src/layouts/duplicate-check/duplicate-check.vue index aa1f80bb9..6cbc56e5b 100644 --- a/src/layouts/duplicate-check/duplicate-check.vue +++ b/src/layouts/duplicate-check/duplicate-check.vue @@ -5,9 +5,40 @@
- + + + + + + + + + + + ({ + buttonLabel: answer.Text, + value: answer.Name, + })); + }, + }, methods: { backButtonAction() { @@ -76,3 +136,30 @@ export default { }, }; + diff --git a/src/layouts/insurance-company/insurance-company.vue b/src/layouts/insurance-company/insurance-company.vue index c97a8589a..480cf505f 100644 --- a/src/layouts/insurance-company/insurance-company.vue +++ b/src/layouts/insurance-company/insurance-company.vue @@ -182,7 +182,7 @@ export default { await this.dispatchStoreAction( this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER, - this.selectedAccount.parentAccountNumber.toString(), + Number(this.selectedAccount.parentAccountNumber).toString(), false ); diff --git a/src/layouts/policy-info/more-policy-questions/more-policy-questions.vue b/src/layouts/policy-info/more-policy-questions/more-policy-questions.vue new file mode 100644 index 000000000..0345efcda --- /dev/null +++ b/src/layouts/policy-info/more-policy-questions/more-policy-questions.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/layouts/policy-info/policy-info.spec.js b/src/layouts/policy-info/policy-info.spec.js index b3251154a..74649d20b 100644 --- a/src/layouts/policy-info/policy-info.spec.js +++ b/src/layouts/policy-info/policy-info.spec.js @@ -55,6 +55,15 @@ function setupMocks() { fetchCmsContentForPage.mockResolvedValue(mockCmsContent); settleAllPromises.mockResolvedValue({ cmsContent: mockCmsContent }); + const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, fieldName) => { + return mockCmsContent?.[widgetName]?.[fieldName] ?? ""; + }), + setCmsContent: jest.fn(), + }, + }; + const mountOptions = getMountOptions({ route: { name: "policy-info", query: {}, params: {} }, router: { @@ -62,6 +71,7 @@ function setupMocks() { navigateWithSaving: jest.fn(), navigateWithoutSaving: jest.fn(), }, + mixins: [mockMixin], }); const wrapper = shallowMount(policyInfo, mountOptions); diff --git a/src/layouts/policy-info/policy-info.vue b/src/layouts/policy-info/policy-info.vue index ceef462a0..0918a7536 100644 --- a/src/layouts/policy-info/policy-info.vue +++ b/src/layouts/policy-info/policy-info.vue @@ -1,19 +1,206 @@ @@ -76,6 +77,9 @@ export default { setServiceabilityDetails(serviceabilityDetails) { this.$emit("updated-serviceability", serviceabilityDetails); }, + setMobileFeePart(mobileFeePart) { + this.$emit("updated-mobile-fee-part", mobileFeePart); + }, }, }; diff --git a/src/layouts/service-location/shop-question/shop-question-popup.vue b/src/layouts/service-location/shop-question/shop-question-popup.vue index 430c347e1..c66f4c79a 100644 --- a/src/layouts/service-location/shop-question/shop-question-popup.vue +++ b/src/layouts/service-location/shop-question/shop-question-popup.vue @@ -107,6 +107,7 @@ import { getServiceabilityDetails, getClosestApplicableShops, getShopProviderData, + getPricedMobileFeePart, } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); @@ -338,6 +339,14 @@ export default { ); this.$emit("updated-serviceability", serviceabilityDetails.data); + + // retrieve mobile fee part + const mobileFeePart = await getPricedMobileFeePart( + this.zipCodeData.zipCode, + this.pageName + ); + + this.$emit("updated-mobile-fee-part", mobileFeePart); } this.closeModal(); diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 1c85df8b6..b33ede8ae 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -404,6 +404,13 @@ export default { payload.orderNumber = ""; } + // Referral Sequence Number + if (order.referralSequenceNumber) { + payload.referralSequenceNumber = order.referralSequenceNumber; + } else { + payload.referralSequenceNumber = ""; + } + // Pricing // Only fire for completed orders? diff --git a/src/mixins/analytics-mixin.spec.js b/src/mixins/analytics-mixin.spec.js index 650403935..ee7c39e3c 100644 --- a/src/mixins/analytics-mixin.spec.js +++ b/src/mixins/analytics-mixin.spec.js @@ -423,6 +423,7 @@ describe("analyticsMixin.js", () => { }, workOrderNumber: "01820-111111", workOrderId: "222222222222", + referralSequenceNumber: "1234567", }; store.getters.isRecalibrationOnOrder = true; store.getters.isRecalibrationOnSubmittedState = false; @@ -667,6 +668,14 @@ describe("analyticsMixin.js", () => { expect(glassString).toMatch(/(\w+\/\w+)?(,\w+\/\w+)*/); expect(promoString).toMatch(/(\w+)?(,\w+)*/); }); + + test("Pushes referralSequenceNumber when present on order", () => { + window.dataLayer = []; + + analyticsMixin.methods.pushOrderToDataLayer(); + + expect(window.dataLayer[0].referralSequenceNumber).toBe("1234567"); + }); }); test("Obj is not null after action prepended", () => { diff --git a/src/store/index.js b/src/store/index.js index 6cbe8e17a..509c34093 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -175,11 +175,18 @@ const getDefaultState = () => { additionalAuthFlag: null, isNoComp: false, insuranceCompanyName: null, + phoneNumber: null, + phoneExtension: null, streetAddress: null, apartment: null, city: null, state: null, zipCode: null, + dateOfBirth: null, + lossTime: null, + lossLocation: null, + lastName: null, + subrogation: null, }, schedule: { date: null, @@ -225,6 +232,7 @@ const getDefaultState = () => { expiryTime: null, idempotencyKey: null, }, + submittedStateRevision: 0, }; }; @@ -342,6 +350,12 @@ export const mutations = { state.order.payment.accountName = accountName; state.order.policy.insuranceCompanyName = accountName; }, + updatePhoneNumber(state, phoneNumber) { + state.order.policy.phoneNumber = phoneNumber; + }, + updatePhoneExtension(state, phoneExtension) { + state.order.policy.phoneExtension = phoneExtension; + }, updateStreetAddress(state, streetAddress) { state.order.policy.streetAddress = streetAddress; }, @@ -551,6 +565,15 @@ export const mutations = { state.order.policy.state = insuranceDetails.state; state.order.policy.zipCode = insuranceDetails.zipCode; state.order.policy.claimNumber = insuranceDetails.claimNumber; + state.order.policy.phoneNumber = insuranceDetails.phoneNumber; + state.order.policy.phoneExtension = insuranceDetails.phoneExtension; + state.order.policy.morePolicyQuestions = insuranceDetails.morePolicyQuestions; + state.order.policy.dateOfBirth = insuranceDetails.dateOfBirth; + state.order.policy.lossTime = insuranceDetails.lossTime; + state.order.policy.lastName = insuranceDetails.lastName; + state.order.policy.lossLocation = insuranceDetails.lossLocation; + state.order.policy.subrogation = insuranceDetails.subrogation; + state.order.policy.streetAddress = insuranceDetails.streetAddress; } }, updateDamageDetails(state, damageDetails) { @@ -745,6 +768,11 @@ export const mutations = { resetState(state) { Object.assign(state, getDefaultState()); }, + // Bumps when submittedState sessionStorage is created, updated, or cleared so getters + // that read sessionStorage (coverageIsVerified, etc.) invalidate their Vuex cache. + incrementSubmittedStateRevision(state) { + state.submittedStateRevision = (state.submittedStateRevision ?? 0) + 1; + }, resetSaveSessionPromise(state) { state.applicationUser.saveSessionPromise = null; }, @@ -1004,6 +1032,7 @@ export const getters = { return !!nonWindshieldItems?.length; }, coverageIsVerified: (state) => { + state.submittedStateRevision; let order; if (window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) !== null) { order = JSON.parse( @@ -1036,6 +1065,7 @@ export const getters = { return getHasRecalibrationPart(state); }, isRecalibrationOnSubmittedState: (state) => { + state.submittedStateRevision; if (window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) !== null) { return getHasRecalibrationPart({ order: JSON.parse( @@ -1046,6 +1076,7 @@ export const getters = { return false; }, shouldHideRecalibration: (state) => { + state.submittedStateRevision; if (window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) !== null) { state = JSON.parse( window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) @@ -3696,6 +3727,8 @@ export const actions = { //restore affiliate cookies context.commit(storeMutations.UPDATE_AFFILIATE_COOKIES, affiliateCookies); + + context.commit(storeMutations.INCREMENT_SUBMITTED_STATE_REVISION); }, addDonationToSubmittedState(context, donationAmount) { @@ -3725,11 +3758,15 @@ export const actions = { sessionStorageKeyConstants.SUBMITTED_STATE, JSON.stringify(submittedState) ); + + context.commit(storeMutations.INCREMENT_SUBMITTED_STATE_REVISION); }, resetSubmittedState(context) { // clear from local storage window.sessionStorage.removeItem(sessionStorageKeyConstants.SUBMITTED_STATE); + + context.commit(storeMutations.INCREMENT_SUBMITTED_STATE_REVISION); }, resetExternalParameterState(context) { if (context.getters.isExternalParameter) { diff --git a/src/store/store.spec.js b/src/store/store.spec.js index f8c41cbe4..ec2a757ca 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -1,7 +1,9 @@ import globalMethods from "@/global-methods"; +import store from "@/store"; import { mutations, state, actions, getters } from "@/store"; import { storeMutations } from "@/constants/store-mutations"; import { storeActions } from "@/constants/store-actions"; +import { sessionStorageKeyConstants } from "@/constants/session-storage"; import { experimentTriggers } from "@/constants/experiments"; import { routeData } from "@/router/constants/routes"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; @@ -363,6 +365,14 @@ describe("Mutations", () => { expect(state.order.customer.phoneNumber).toEqual("555-555-5555"); expect(state.order.customer.isSmsOptIn).toEqual(true); }); + + it("incrementSubmittedStateRevision, should increment submittedStateRevision", () => { + const storeState = { submittedStateRevision: 0 }; + + mutations.incrementSubmittedStateRevision(storeState); + + expect(storeState.submittedStateRevision).toEqual(1); + }); }); describe("Actions", () => { @@ -3610,6 +3620,85 @@ describe("Actions", () => { }); }); +describe("submittedStateRevision", () => { + beforeEach(() => { + mutations.resetState(store.state); + window.sessionStorage.removeItem(sessionStorageKeyConstants.SUBMITTED_STATE); + }); + + it("resetSubmittedState action commits INCREMENT_SUBMITTED_STATE_REVISION", async () => { + const context = { commit: jest.fn() }; + + await actions.resetSubmittedState(context); + + expect(context.commit).toHaveBeenCalledWith( + storeMutations.INCREMENT_SUBMITTED_STATE_REVISION + ); + }); + + it("createSubmittedState action commits INCREMENT_SUBMITTED_STATE_REVISION at end", async () => { + const context = { + commit: jest.fn(), + state: { + order: { + payment: { insuranceCoverage: { isVerified: false } }, + policy: { currentDeductible: 0 }, + lineItems: {}, + }, + applicationUser: { + experiments: [], + affiliateCookies: [], + }, + }, + }; + + await actions.createSubmittedState(context); + + expect(context.commit).toHaveBeenCalledWith( + storeMutations.INCREMENT_SUBMITTED_STATE_REVISION + ); + }); + + it("addDonationToSubmittedState action commits INCREMENT_SUBMITTED_STATE_REVISION", async () => { + window.sessionStorage.setItem( + sessionStorageKeyConstants.SUBMITTED_STATE, + JSON.stringify({ + order: { + lineItems: { supportingItems: [] }, + }, + applicationUser: {}, + }) + ); + + const context = { commit: jest.fn() }; + + await actions.addDonationToSubmittedState(context, 5); + + expect(context.commit).toHaveBeenCalledWith( + storeMutations.INCREMENT_SUBMITTED_STATE_REVISION + ); + }); + + it("coverageIsVerified re-evaluates after resetSubmittedState clears sessionStorage", () => { + window.sessionStorage.setItem( + sessionStorageKeyConstants.SUBMITTED_STATE, + JSON.stringify({ + order: { + payment: { insuranceCoverage: { isVerified: true } }, + policy: { currentDeductible: 500 }, + }, + applicationUser: {}, + }) + ); + + expect(store.getters.coverageIsVerified).toBe(true); + + store.dispatch(storeActions.RESET_SUBMITTED_STATE); + + expect(store.getters.coverageIsVerified).toBe(false); + }); +}); + describe("Getters", () => { it("Vehicle getter, should return vehicle data", () => { // Arrange diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index 293f29d73..df71f9800 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -6,9 +6,10 @@
- - {{ buttonLabel }} - + {{ buttonLabelSubCopy }}