diff --git a/jest.config.js b/jest.config.js index 10af0ccf0..7b59ea2c1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,6 @@ module.exports = { }, transformIgnorePatterns: ["'/node_modules/(?!vee-validate)"], moduleFileExtensions: ["js", "vue"], - modulePathIgnorePatterns: ["vin-lookup"], collectCoverageFrom: [ "src/**/*.{js,vue}", "!src/main.js", diff --git a/src/constants/experiments.js b/src/constants/experiments.js index 1d689d727..f81484058 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -36,6 +36,9 @@ const experimentSettings = { SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays", SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays", SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays", + SHOW_MOBILE_FIRST_POPUP_REPAIR: "Show_Mobile_First_Popup_Repair", + SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR", + SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR", OFFER_OEM: "OfferOem", USE_ADYEN_PAYMENT: "UseAdyenPayment", }; diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index fa8d14b62..59297ae05 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -289,7 +289,7 @@ export default { if (event.screenX === 0 && event.screenY === 0) { return; } - this.$emit("date-clicked", date); + this.$emit("date-selected", date); }, getWeekStartDate(dateString) { const date = convertDateStringToDate(dateString); @@ -332,9 +332,9 @@ export default { let preSelectedDateMonthEnd = this.getMonthEnd( preSelectedDate.replace("-mobile", "") ); - let weekIncludesPreSelectedMonthEnd = false; + let monthEndsByThisWeek = false; let i = 0; - while (!weekIncludesPreSelectedMonthEnd) { + while (!monthEndsByThisWeek && i < 52) { if (i > 0) { weekStartDate = this.getNextWeekSunday(weekEndDate); weekEndDate = this.getWeekEndDate(weekStartDate); @@ -346,7 +346,12 @@ export default { preSelectedDateMonthEnd === weekEndDate ) { weekEndDate = preSelectedDateMonthEnd; - weekIncludesPreSelectedMonthEnd = true; + monthEndsByThisWeek = true; + } else if (preSelectedDateMonthEnd < weekEndDate) { + // Additional case: if month ends before this week (but not necessarily during it), + // still cut the loop here, but don't truncate the week. + + monthEndsByThisWeek = true; } } weeks.push({ diff --git a/src/digital-components/sierra-webchat/sierra-webchat.vue b/src/digital-components/sierra-webchat/sierra-webchat.vue index 162367a62..2ba74b33a 100644 --- a/src/digital-components/sierra-webchat/sierra-webchat.vue +++ b/src/digital-components/sierra-webchat/sierra-webchat.vue @@ -138,10 +138,7 @@ export default { } // Pass data to Salesforce chat if available const transfer = event.detail; - if ( - window.embeddedservice_bootstrap && - typeof window.embeddedservice_bootstrap.bootstrapEmbeddedService === "function" - ) { + if (window.embeddedservice_bootstrap && window.embeddedservice_bootstrap.prechatAPI) { if ( transfer && transfer.data && diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 9d5f21785..cddb2189c 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -246,7 +246,7 @@ export default { console.log(`Price = ${this.amountDue}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`); - const requestBody = this.adyenInitRequestInfo; + const requestBody = this.getAdyenInitRequestInfo(); console.log(`Calling with:`); console.log(requestBody); @@ -294,6 +294,15 @@ export default { console.log(checkout); + const expiryTime = new Date(checkout.options.expiresAt); + const expiryInterval = expiryTime.getTime() - new Date().getTime(); + + const handleTimeout = () => { + this.resetAdyenDropin(); + }; + + const timeout = setTimeout(handleTimeout, expiryInterval); + const configuration = { paymentMethodsConfiguration: { ideal: { @@ -496,7 +505,7 @@ export default { await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE); this.$router.navigateWithoutSaving( - this.navigationScenarios.CLICKED_FORWARD, + this.navigationScenarios.CLICKED_PAY_LATER, this.pageName ); } catch (error) { @@ -505,14 +514,26 @@ export default { return; } }, - }, - computed: { - piaType() { - return this.$store.getters.order.payment.piaType; + async resetAdyenDropin() { + this?.dropinComponent?.unmount(); + await this.initializeAdyen(); }, - adyenInitRequestInfo() { + // Now a Method so it is always freshly called and not cached. + getIdempotencyKey() { + const currentDateTime = new Date(); + const currentHour = currentDateTime.getUTCHours(); + const currentDate = currentDateTime.getUTCDate(); + const id = this?.$store?.getters?.order?.referralCorrelationId; + const system = this.sourceSystem; + const total = this.adyenPriceTotal; + + return `${id}-${system}-${currentDate}-${currentHour}-${total}`; + }, + + // Now a Method so it is always freshly called and not cached. + getAdyenInitRequestInfo() { return { sourceSystem: this.sourceSystem, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, @@ -529,9 +550,15 @@ export default { IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: this.idempotencyKey, + idempotencyKey: this.getIdempotencyKey(), }; }, + }, + + computed: { + piaType() { + return this.$store.getters.order.payment.piaType; + }, workOrderNumberLastSixDigits() { const workOrderNumber = this.$store.getters.order.workOrderNumber ?? ""; @@ -588,10 +615,6 @@ export default { }; }, - idempotencyKey() { - return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`; - }, - sourceSystem() { return "FMG-2.0"; }, @@ -601,7 +624,7 @@ export default { }, adyenPriceTotal() { - return this.amountDue * 100; + return Math.round(this.amountDue * 100); }, // Cart info diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e8ce97d63..c4d566ae7 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -28,6 +28,7 @@ cmsWidgetName="ScheduleYourServiceWidget" id="schedule-your-service" /> -
+
0; + } else { + return this.selectableDatesInshop?.days?.length > 0; + } + }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -1261,8 +1278,6 @@ export default { async initializeDatePicker() { this.isShowMobileFirstAppt && this.showLoadingModal(); - this.selectedDate = null; - const includeMobileTimeSlots = this.isServiceableMobile; const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff; const datePickerInitialData = await this.$refs.datePicker.loadInitialData({ @@ -1637,12 +1652,12 @@ export default { handleWaitListRequested(value) { this.waitListRequested = value; }, - handleDateClicked(date) { - // do something to mark this as upcharge day or not... - if (date.isPricingByDayUpchargeDay) { - this.includePricingByDayUpcharge = true; - } else { - this.includePricingByDayUpcharge = false; + handleDateSelected(date) { + const previousDate = this.selectedDate; + // check if date actually changed + if (previousDate !== date.dateString) { + this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); + this.updateFooterButtonText(this.selectedTimeSlotInfo); } // TODO: REMOVE AS PART OF CASH-1634 @@ -1669,17 +1684,7 @@ export default { updateTimeSlot(timeSlotObj) { if (!timeSlotObj?.routeCode) { this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; - this.selectedTimeSlotInfo = { - timeSlot: { - date: null, - routeCode: null, - startTime: null, - endTime: null, - jobMaxMinutes: null, - jobMinMinutes: null, - }, - isPremiumAppointment: null, - }; + this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); return; } const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find( @@ -1801,6 +1806,9 @@ export default { }, handleAppointmentTypeChange(newAppointmentType) { this.updateFooterButtonText(); + // if time appointment type changes, clear any selected time slot + this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); + if (newAppointmentType === AppointmentTypeStrings.MOBILE) { // Remember last shop selected if previous selection was inshop/dropoff if ( @@ -1809,17 +1817,6 @@ export default { AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) && this.selectedProvider ) { - this.selectedTimeSlotInfo = { - timeSlot: { - date: null, - routeCode: null, - startTime: null, - endTime: null, - jobMaxMinutes: null, - jobMinMinutes: null, - }, - isPremiumAppointment: null, - }; this.lastSelectedInshopOrDropoffProvider = this.selectedProvider; } this.appointmentType = AppointmentTypeStrings.MOBILE; @@ -1846,7 +1843,8 @@ export default { } else { this.appointmentType = null; } - this.setSelectedDateToFirstAvailable(); + this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now + this.setDisplayWaitList(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now }, setSelectedDateToFirstAvailable() { this.selectedDate = this.getFirstAvailableDate(); @@ -1888,6 +1886,20 @@ export default { const noMobileAvailableDays = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_NO_MOBILE_AVAILABLE_DAYS ); + const mobileFirstPopupRepairSetting = experimentMixin.methods.getSettingValue( + experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPAIR + ); + const mobileFirstPopupReplaceWithoutMsrSetting = + experimentMixin.methods.getSettingValue( + experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR + ); + const mobileFirstPopupReplaceWithMsrSetting = + experimentMixin.methods.getSettingValue( + experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR + ); + + const order = this.$store.getters.order; + const isRepair = order.damage.isRepair; let preSelectedMobileAppointment = null; const todaysDate = getTodayDate(); @@ -1904,12 +1916,63 @@ export default { (new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24); const shouldExposeMobileFirstAppointment = () => { - return ( + const mobileFirstDaysCheck = (firstMobileAMDate && numberOfDaysToFirstMobileAMDate <= noMobileAvailableDays) || (firstMobilePMDate && - numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays) - ); + numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays); + debugLog("::: Mobile First Days Check:", mobileFirstDaysCheck); + + if ( + mobileFirstPopupRepairSetting === "false" && + mobileFirstPopupReplaceWithoutMsrSetting === "false" && + mobileFirstPopupReplaceWithMsrSetting === "false" + ) { + debugLog( + "::: Mobile First Damage Popup settings are all false, using default check." + ); + return mobileFirstDaysCheck; + } + + if (isRepair) { + if (mobileFirstPopupRepairSetting === "true") { + debugLog( + "::: Mobile First Popup Repair setting is true, showing mobile first appointment for repair." + ); + return mobileFirstDaysCheck; + } else { + debugLog( + "::: Mobile First Popup Repair setting is false, not showing mobile first appointment for repair." + ); + return false; + } + } + + if (this.isMobileStaticRecalibrationApplicable) { + if (mobileFirstPopupReplaceWithMsrSetting === "true") { + debugLog( + "::: Mobile First Popup Replace with MSR setting is true, showing mobile first appointment for replace with MSR." + ); + return mobileFirstDaysCheck; + } else { + debugLog( + "::: Mobile First Popup Replace with MSR setting is false, not showing mobile first appointment for replace with MSR." + ); + return false; + } + } + + if (mobileFirstPopupReplaceWithoutMsrSetting === "true") { + debugLog( + "::: Mobile First Popup Replace without MSR setting is true, showing mobile first appointment for replace without MSR." + ); + return mobileFirstDaysCheck; + } else { + debugLog( + "::: Mobile First Popup Replace without MSR setting is false, not showing mobile first appointment for replace without MSR." + ); + return false; + } }; if (shouldExposeMobileFirstAppointment()) { @@ -2008,35 +2071,9 @@ export default { watch: { appointmentTypeFromAppointmentTypeQuestion: { handler(newValue, oldValue) { - this.handleAppointmentTypeChange(newValue); + this.handleAppointmentTypeChange(newValue); // v-if on appointmentTypeQuestion ensures dates have been loaded by now }, }, - selectedDate(newValue, oldValue) { - // Clear time slot selection if date selected changes - const selectedDate = this.getSelectedDateFromStore(); - if (oldValue && this.appointmentType === AppointmentTypeStrings.MOBILE) { - oldValue = `${oldValue}-mobile`; - } - const hasValueChanged = newValue !== oldValue; - const isDateDifferent = (newValue || oldValue) !== selectedDate; - - if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) { - this.selectedTimeSlotInfo = { - timeSlot: { - date: null, - routeCode: null, - startTime: null, - endTime: null, - jobMaxMinutes: null, - jobMinMinutes: null, - }, - isPremiumAppointment: null, - }; - } - }, - selectedTimeSlotInfo(newValue) { - this.updateFooterButtonText(newValue); - }, }, components: { funnelHeader, diff --git a/src/layouts/schedule/time-slot-question/time-slot-question.vue b/src/layouts/schedule/time-slot-question/time-slot-question.vue index b96bdbe1a..a855ab399 100644 --- a/src/layouts/schedule/time-slot-question/time-slot-question.vue +++ b/src/layouts/schedule/time-slot-question/time-slot-question.vue @@ -1,6 +1,7 @@