diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 00088baa7..e156de11b 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -15,6 +15,7 @@ const GaCategories = { APPOINTMENT: "Appointment", SERVICE_LOCATION: "service-location", CONFIRMATION_CLICKED: "confirmation clicked", + RECAL_DISCLAIMER: "Recalibration disclaimer", }; const GaActions = { @@ -29,6 +30,7 @@ const GaActions = { MORE_LOCATIONS_CLICKED: "more_locations_clicked", YES: "yes", NO: "no", + ACKNOWLEDGED: "Acknowledged", }; const GaLabels = { diff --git a/src/constants/experiments.js b/src/constants/experiments.js index a2a648a65..1d689d727 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -12,6 +12,9 @@ const experimentSettings = { SUPPRESS_VIN_CAPTURE: "SuppressVinCapture", DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators", PIA_EXPERIENCE: "PIA Experience", + CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers", + CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2", + CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate", PIA_INSURANCE: "DisplayPIAInsurance", SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA", IS_EMAIL_OPTIONAL: "isEmailOptional", diff --git a/src/constants/schedule-constants.js b/src/constants/schedule-constants.js index 2b626df18..2b6171075 100644 --- a/src/constants/schedule-constants.js +++ b/src/constants/schedule-constants.js @@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD"; const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN"; +const RECAL_ACK_YES = "Yes"; + const RouteCodeFlags = { ALL_DAY_DROP_OFF: "ALL DAY DROP OFF", OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF", @@ -31,4 +33,5 @@ export { PRICING_BY_DAY_PART_TYPE, RouteCodeFlags, cmsWidgetFieldMappings, + RECAL_ACK_YES, }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 2fc0c88eb..3afd4df24 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -111,6 +111,7 @@ const storeActions = { SAVE_PAYPAL_TOKEN: "savePaypalToken", SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount", SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn", + SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling", SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 5167581e0..700f527c2 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -69,6 +69,7 @@ const storeMutations = { LOCK_TOKEN: "updateLockToken", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", + UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling", UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", diff --git a/src/helpers/recal-helper.js b/src/helpers/recal-helper.js index 2794bbcf6..2a3281185 100644 --- a/src/helpers/recal-helper.js +++ b/src/helpers/recal-helper.js @@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings"; const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION]; +export function anyPartWithRequiresRecalFlag(lineItems) { + if (!lineItems) { + return false; + } + + return lineItems.glassParts?.some((li) => li.requiresRecalibration === true); +} + export function isRecalPartOrHasChildRecalPart(lineItem) { if (lineItem.childParts && lineItem.childParts.length > 0) { return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts); diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 42b7d03fb..9d5f21785 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -36,6 +36,10 @@ :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> + @@ -56,6 +60,7 @@ import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import navbar from "@/fmg-components/nav-bar/nav-bar"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import alert from "@/ux-components/alert/alert"; +import textBlock from "@/digital-components/text-block/text-block"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; @@ -645,6 +650,7 @@ export default { navbar, cart, alert, + textBlock, }, }; @@ -671,5 +677,10 @@ export default { margin: 0 0.5rem; } } + + .payment-disclaimer p { + font-size: 0.875rem; + line-height: 1.2; + } } diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 0fa02e80a..2a6d1d9ed 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -810,11 +810,51 @@ export default { const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE); if (this.isInsurance) { - return piaInsurance === "true" && this.currentDeductible > 0; + return ( + (piaInsurance === "true" || this.isConditionalPIAEnabled) && + this.currentDeductible > 0 + ); } else { - return piaExperience === "PIA Optional" || piaExperience === "PIA Required"; + if (piaExperience?.includes("PIA Disabled")) { + return this.isConditionalPIAEnabled; + } else { + return piaExperience === "PIA Optional" || piaExperience === "PIA Required"; + } } }, + isConditionalPIAEnabled() { + const conditionalProviderNumbers = this.getSettingValue( + experimentSettings.CONDITIONAL_PROVIDER_NUMBERS + ); + const conditionalProviderNumbers2 = this.getSettingValue( + experimentSettings.CONDITIONAL_PROVIDER_NUMBERS_2 + ); + const conditionalProviderShutoffDate = this.getSettingValue( + experimentSettings.CONDITIONAL_PROVIDER_SHUTOFF_DATE + ); + + const allConditionalProviderNumbers = [ + conditionalProviderNumbers, + conditionalProviderNumbers2, + ] + .filter(Boolean) + .join(","); + const providerNumber = + this.$store.getters.order.serviceLocation.provider.providerNumber; + const appointmentDate = new Date(this.$store.getters.order.schedule.date); + const shutoffDate = new Date(conditionalProviderShutoffDate); + if (allConditionalProviderNumbers && conditionalProviderShutoffDate) { + const providerNumbersArray = allConditionalProviderNumbers + .split(",") + .map((num) => num.trim()); + if (!providerNumbersArray.includes(providerNumber)) { + return false; + } else { + return appointmentDate <= shutoffDate; + } + } + return false; + }, paymentMethod() { if (!this.isPiaEnabled) { return paymentMethods.LATER; diff --git a/src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue b/src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue new file mode 100644 index 000000000..e1c823cfe --- /dev/null +++ b/src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue @@ -0,0 +1,223 @@ +// This modal is used to acknowledge the recal disclaimer during scheduling. // It will trigger when +there is a glass part with requiresRecalibration = true // but there is no recalibration part in +lineItems. This mostly occurs when // a zip code that does mobile only is selected but the vehicle +requires // recalibration. + + + + diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 87b542b80..f7c1ead18 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -6,6 +6,12 @@ modalWidgetName="MobileFirstModalWidget" ref="mobileFirstModal" @confirm-appointment="updateTimeSlotandNavigateForward" /> +
@@ -190,6 +196,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location"; import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue"; import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue"; import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue"; +import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue"; import shopQuestion from "@/layouts/service-location/shop-question/shop-question"; import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup"; @@ -216,6 +223,7 @@ import { RouteCodeFlags, PREMIUM_FEE_PART_TYPE, PRICING_BY_DAY_PART_TYPE, + RECAL_ACK_YES, } from "@/constants/schedule-constants"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; @@ -242,6 +250,7 @@ import { isDropOffRouteCode, getTodayDate, } from "@/layouts/schedule/helpers/schedule-helper"; +import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper"; import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants"; import { required } from "@/helpers/validation-rules"; @@ -430,6 +439,7 @@ export default { name: "schedule", data() { return { + isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(), selectedDate: this.getSelectedDateFromStore(), selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(), mobilePremiumAppointmentFee: null, @@ -1233,6 +1243,9 @@ export default { this.isShowMobileFirstAppt && this.hideLoadingModal(); }, + getIsRecalAcknowledgedForSchedulingFromStore() { + return store.getters.order.isRecalAcknowledgedForScheduling; + }, getScheduleApiResponse, getServiceZipCtuCodeFromStore() { return store.getters.order.serviceLocation.zipCodeCtu; @@ -1351,7 +1364,25 @@ export default { hideLoadingModal() { this.$refs.loadingModal.hideModal(); }, + async closeRecalAcknowledgementPopup() { + this.showRecalAcknowledgementPopup = false; + }, async forwardButtonAction() { + const showRecalAcknowledgementModal = this.showRecalAcknowledgementModal(); + if ( + showRecalAcknowledgementModal && + this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES + ) { + this.showRecalAcknowledgementPopup = true; + return; + } + + this.dispatchStoreAction( + storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING, + this.isRecalAcknowledgedForScheduling, + false + ); + this.navigatingForward = true; var ctu = this.zipCodeCtu; if (this.appointmentType === AppointmentTypeStrings.MOBILE) { @@ -1675,7 +1706,27 @@ export default { this.selectedProvider = this.shopProviderData.shopProviders[0]; } }, - handleZipCodeChange(newZipCode) { + async handleZipCodeChange(newZipCode) { + // if we were showing the recal ack modal and now are changing the zip, we need to navigate + // to service-zip so we can get call parts again to add the recal part if needed + if (this.shouldShowRecalAckModal()) { + await baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_SERVICE_ZIP_CODE_INFO, + { + zipCode: newZipCode.zipCode, + state: newZipCode.state, + zipCodeCtu: newZipCode.zipCodeCtu, + }, + false + ); + + this.$router.navigateWithSaving( + this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK, + this.pageName + ); + return; + } + this.resetMobileLocation(); getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => { @@ -1752,6 +1803,32 @@ export default { setSelectedDateToFirstAvailable() { this.selectedDate = this.getFirstAvailableDate(); }, + showRecalAcknowledgementModal() { + if (this.shouldShowRecalAckModal()) { + this.$refs.navbar.removeLoader(); + this.$refs.recalAckModal.openModal(); + return true; + } + + this.closeRecalAcknowledgementPopup(); + return false; + }, + shouldShowRecalAckModal() { + // if any of the glass parts contain an item with the requiresRecalibration flag set to true + // and the order does not contain a recalibration part, show the recalibration acknowledgement modal + const order = this.$store.getters.order; + const containsRecalPart = containsRecalParts(order.lineItems); + const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(order.lineItems); + + if ( + !containsRecalPart && + hasPartWithRecalRequirement && + this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES + ) { + return true; + } + return false; + }, async showMobileFirstModal() { if (!this.selectedRouteCodeData?.routeCode) { const pmMobileDays = experimentMixin.methods.getSettingValue( @@ -1875,6 +1952,10 @@ export default { this.updateTimeSlot(timeSlotObj); this.forwardButtonAction(); }, + updateRecalAcknowledgedAndNavigateForward(isAcknowledged) { + this.isRecalAcknowledgedForScheduling = isAcknowledged; + this.forwardButtonAction(); + }, }, watch: { appointmentTypeFromAppointmentTypeQuestion: { @@ -1920,7 +2001,7 @@ export default { textBlock, timeSlotQuestion, mobileFirstModal, - + recalAckModal, alert, serviceZipModalQuestion, appointmentTypeQuestion, diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index c2247d59b..0d0ee836f 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -57,6 +57,7 @@ const navigationScenarios = { // Schedule CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION", + ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK", // Payment CLICKED_INSURANCE: "CLICKED_INSURANCE", diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 48cd49b3f..3b237d747 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -486,6 +486,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE, destinationPageData: routeData.MOBILE_DETAILS, }, + { + scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK, + destinationPageData: routeData.SERVICE_ZIP, + }, ], }, { diff --git a/src/store/index.js b/src/store/index.js index 8faf39f73..e9b379b56 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -190,6 +190,7 @@ const getDefaultState = () => { lockToken: null, settledTenderAmount: 0, isRecalAckOptIn: false, + isRecalAcknowledgedForScheduling: "", isMSRFeeApplicable: false, }, applicationUser: { @@ -343,6 +344,9 @@ export const mutations = { updateIsRecalAckOptIn(state, isRecalAckOptIn) { state.order.isRecalAckOptIn = isRecalAckOptIn; }, + updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) { + state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling; + }, updateIsOemGlassSelected(state, isOemGlassSelected) { state.order.damage.installOemGlass = isOemGlassSelected; }, @@ -2965,6 +2969,13 @@ export const actions = { context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn); }, + saveIsRecalAcknowledgedForScheduling(context, isRecalAcknowledgedForScheduling) { + context.commit( + storeMutations.UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING, + isRecalAcknowledgedForScheduling + ); + }, + saveIsOemGlassSelected(context, isOemGlassSelected) { context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected); }, diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 0c275ad51..76035c6ca 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -27,6 +27,8 @@ $red-600: #ac160b; $red-700: #840900; $red-800: #5b0600; $red-900: #330300; +$red-1000: #a80019; +$red-1100: #791710; // Greens $green-100: #e3f2ea;