Merge branch 'develop' into feature/CASH-1947

This commit is contained in:
Chris Redelinghuys 2026-02-17 08:48:25 -05:00
commit ca4f6323c2
31 changed files with 676 additions and 2945 deletions

View file

@ -32,11 +32,7 @@ module.exports = {
"!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development "!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development "!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development "!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/schedule/**/*.vue", // Temp test exclusion while in development
"!src/digital-components/date-picker/**/*.vue", // Temp test exclusion while in development "!src/digital-components/date-picker/**/*.vue", // Temp test exclusion while in development
"!src/**/*-june-2025.vue", // Exclude these temporary files for CASH-845 project
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development "!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
"!src/experiment-components/*.vue", "!src/experiment-components/*.vue",
// END // END
@ -44,7 +40,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 64, statements: 68,
}, },
}, },
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-US"> <html lang="en-US">
<head> <head>
<meta name="robots" content="noindex" />
<script> <script>
window.dataLayer = [{}]; window.dataLayer = [{}];
</script> </script>

View file

@ -48,6 +48,10 @@ export const pageProgressMapper = {
percent: 30, percent: 30,
step: 1, step: 1,
}, },
"policy-info": {
percent: 55,
step: 2,
},
quote: { quote: {
percent: 60, percent: 60,
step: 2, step: 2,

View file

@ -173,4 +173,29 @@ export const quotePageDiscountTable = [
packageLevel: packageNames.TIER_THREE, packageLevel: packageNames.TIER_THREE,
isInsurance: true, isInsurance: true,
}, },
// Replace - Curve Tempered
{
name: "Curve Tempered Glass Only",
experimentCode: "Show_GlassOnly_Replace_CT_Discount",
partNumber: "GL CT CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS CT GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Curve Tempered Standard",
experimentCode: "Show_Standard_Replace_CT_Discount",
partNumber: "STD CT CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD CT GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Curve Tempered Premium",
experimentCode: "Show_Premium_Replace_CT_Discount",
partNumber: "PRM CT CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM CT GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
]; ];

View file

@ -119,6 +119,7 @@ const storeMutations = {
UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE: "updateExternalParameterServicePackage", UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE: "updateExternalParameterServicePackage",
UPDATE_EXTERNAL_PARAMETER_SOURCE: "updateExternalParameterSource", UPDATE_EXTERNAL_PARAMETER_SOURCE: "updateExternalParameterSource",
UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER: "updateExternalParameterPhoneNumber", UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER: "updateExternalParameterPhoneNumber",
UPDATE_EXTERNAL_PARAMETER_QS_STASH: "updateExternalParameterQsStash",
//RESET EXTERNAL_PARAMETER MUTATIONS //RESET EXTERNAL_PARAMETER MUTATIONS
RESET_EXTERNAL_PARAMETER_VEHICLE_STATE: "resetExternalParameterVehicleState", RESET_EXTERNAL_PARAMETER_VEHICLE_STATE: "resetExternalParameterVehicleState",

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,18 @@
<template> <template>
<div <div
class="date-picker text-center" class="date-picker text-center"
:class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass}`"> :class="`${calendarViewDirection} calendar-2025 ${showPricingByDayClass} ${loadingStatus === 'reinitial' ? 'loading-reinitial' : ''}`">
<fieldset id="date-picker-fieldset" ref="datePickerFieldset"> <fieldset id="date-picker-fieldset" ref="datePickerFieldset">
<legend class="sr-only">Select a day and time</legend> <legend class="sr-only">Select a day and time</legend>
<div <div
v-for="month in months" v-for="(month, i) in months"
:key="`${month.monthLabel}-${month.yearNum?.toString()}`" :key="`${month.monthLabel}-${month.yearNum?.toString()}`"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`" :id="`${month.monthLabel}-${month.yearNum?.toString()}`"
class="calendar-grid-container" class="calendar-grid-container month"
:class="[ :class="[
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '', hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
month.monthClass, month.monthClass,
i === 0 ? 'first-month-shown' : '',
]"> ]">
<div class="month-year body-small d-flex align-items-center"> <div class="month-year body-small d-flex align-items-center">
{{ month.monthLabel }} {{ month.yearNum?.toString() }} {{ month.monthLabel }} {{ month.yearNum?.toString() }}
@ -114,7 +115,8 @@
</div> </div>
</div> </div>
<loader <loader
:class="[!isLoading ? 'date-picker-hidden' : '']" class="date-picker-loader"
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
loaderColor="blue" loaderColor="blue"
loaderPosition="center" /> loaderPosition="center" />
<div class="row form-test-error" id="date-of-month-error"> <div class="row form-test-error" id="date-of-month-error">
@ -125,7 +127,7 @@
type="button" type="button"
class="btn btn-link view-more-dates" class="btn btn-link view-more-dates"
id="viewMoreDates" id="viewMoreDates"
:disabled="isLoading" :disabled="loadingStatus !== 'none'"
@click="showAnotherMonth"> @click="showAnotherMonth">
View more dates View more dates
</button> </button>
@ -160,7 +162,6 @@ export default {
name: "datePicker", name: "datePicker",
data() { data() {
return { return {
isLoading: true,
months: null, months: null,
disableViewMoreDatesButton: false, disableViewMoreDatesButton: false,
hideSomeDaysForInitialView: null, hideSomeDaysForInitialView: null,
@ -169,7 +170,10 @@ export default {
}; };
}, },
props: { props: {
customComponentId: String, customComponentId: {
type: String,
default: "",
},
selectableDatesSetting: { selectableDatesSetting: {
type: String, type: String,
validator(value) { validator(value) {
@ -200,6 +204,10 @@ export default {
pricingByDayUpcharge: Number, pricingByDayUpcharge: Number,
isPricingByDayExperiment: Boolean, isPricingByDayExperiment: Boolean,
isMobileSelected: Boolean, isMobileSelected: Boolean,
loadingStatus: {
type: String,
default: "none",
},
}, },
setup(props) { setup(props) {
const uuid = uuidv4(); const uuid = uuidv4();
@ -324,9 +332,9 @@ export default {
let preSelectedDateMonthEnd = this.getMonthEnd( let preSelectedDateMonthEnd = this.getMonthEnd(
preSelectedDate.replace("-mobile", "") preSelectedDate.replace("-mobile", "")
); );
let weekIncludesPreSelectedMonthEnd = false; let monthEndsByThisWeek = false;
let i = 0; let i = 0;
while (!weekIncludesPreSelectedMonthEnd) { while (!monthEndsByThisWeek && i < 52) {
if (i > 0) { if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate); weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate); weekEndDate = this.getWeekEndDate(weekStartDate);
@ -338,7 +346,12 @@ export default {
preSelectedDateMonthEnd === weekEndDate preSelectedDateMonthEnd === weekEndDate
) { ) {
weekEndDate = preSelectedDateMonthEnd; 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({ weeks.push({
@ -489,7 +502,6 @@ export default {
}); });
}, },
async setCalendarData(config = {}) { async setCalendarData(config = {}) {
this.isLoading = true;
this.selectableDatesInshop = []; this.selectableDatesInshop = [];
this.selectableDatesMobile = []; this.selectableDatesMobile = [];
this.months = []; this.months = [];
@ -541,7 +553,6 @@ export default {
// } // }
} }
this.months = months; this.months = months;
this.isLoading = false;
if (config.preSelectedDate) { if (config.preSelectedDate) {
this.$nextTick(() => { this.$nextTick(() => {
@ -729,7 +740,6 @@ export default {
}, },
async showAnotherMonth() { async showAnotherMonth() {
if (!this.months) return; if (!this.months) return;
this.isLoading = true;
let monthToShow; let monthToShow;
let monthStartDate; let monthStartDate;
@ -774,7 +784,6 @@ export default {
monthToShowDates[monthToShowDates.length - 1].inputValue monthToShowDates[monthToShowDates.length - 1].inputValue
); );
this.isLoading = false;
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", ""); monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
this.scrollToElement(monthToShow.monthString); this.scrollToElement(monthToShow.monthString);
@ -787,7 +796,6 @@ export default {
}, },
async updateSelectableDates(monthStart, monthEnd) { async updateSelectableDates(monthStart, monthEnd) {
const moreSelectableDates = await this.getMoreDatesCallback(monthStart, monthEnd); const moreSelectableDates = await this.getMoreDatesCallback(monthStart, monthEnd);
moreSelectableDates.inshopTimeSlotsData.days.forEach((selectableDate) => { moreSelectableDates.inshopTimeSlotsData.days.forEach((selectableDate) => {
const index = this.selectableDatesInshop.findIndex( const index = this.selectableDatesInshop.findIndex(
(dateObj) => dateObj.date === selectableDate.date (dateObj) => dateObj.date === selectableDate.date
@ -846,12 +854,6 @@ export default {
}, },
}, },
watch: { watch: {
isLoading(newValue) {
// if done loading dates, then set to first available date
if (newValue === false && this.firstAvailableSelectableDate) {
this.selectedDate = this.firstAvailableSelectableDate;
}
},
modelValue(newValue) { modelValue(newValue) {
this.resetField({ this.resetField({
value: newValue, value: newValue,
@ -872,9 +874,14 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.date-picker-hidden { .date-picker-loader {
opacity: 0; opacity: 0;
max-height: 0; max-height: 0;
&.show-loader {
opacity: 1;
max-height: none;
}
} }
.date-picker { .date-picker {
position: relative; position: relative;
@ -903,12 +910,12 @@ export default {
height: 1.5rem; height: 1.5rem;
} }
} }
.calendar-grid-container { .month {
margin: 0 auto 1rem auto; margin: 0 auto 1rem auto;
position: relative; position: relative;
transition: transition:
height ease 2s, height ease 1s,
opacity ease 2s; max-height ease 1s;
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
justify-content: center; justify-content: center;
@ -1157,7 +1164,7 @@ export default {
} }
.past { .past {
.calendar-grid-container { .month {
.month-year { .month-year {
grid-area: 1 / 1 / 2 / 6; grid-area: 1 / 1 / 2 / 6;
} }
@ -1214,7 +1221,7 @@ export default {
} }
// new calendar styling overrides // new calendar styling overrides
&.calendar-2025 { &.calendar-2025 {
.calendar-grid-container .grid-item { .month .grid-item {
margin: 0 0 0.15rem 0; margin: 0 0 0.15rem 0;
} }
.month-year { .month-year {
@ -1316,6 +1323,33 @@ export default {
} }
} }
} }
&.loading-reinitial {
.month {
opacity: 0;
max-height: 0;
}
.month.first-month-shown {
opacity: 1;
max-height: none;
.grid-item.radio-wrapper.selectable-day {
input[type="radio"]:checked + label,
label {
background-color: $white;
border: none;
color: initial;
cursor: default;
&:hover {
border: none;
}
span {
font-family: UrbanistRegular;
}
}
}
}
}
} }
:deep(.timeslots .button-inner-wrapper) { :deep(.timeslots .button-inner-wrapper) {
display: flex; display: flex;

View file

@ -41,9 +41,6 @@ export default {
beforeUnmount() { beforeUnmount() {
// Clean up event listeners and observers // Clean up event listeners and observers
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer); window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
if (this._salesforceChatOpenObserver) {
this._salesforceChatOpenObserver.disconnect();
}
}, },
methods: { methods: {
initializeSalesforceWebchat() { initializeSalesforceWebchat() {
@ -133,6 +130,9 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.embedded-messaging > .embeddedMessagingFrame[class~="isMaximized"] {
z-index: 9999 !important;
}
.dockableContainer { .dockableContainer {
&.showDockableContainer { &.showDockableContainer {
color: $gray-600; color: $gray-600;

View file

@ -59,14 +59,6 @@ describe("sierraWebchat.vue", () => {
expect(window.sierraChat.closeChatModal).toHaveBeenCalled(); expect(window.sierraChat.closeChatModal).toHaveBeenCalled();
}); });
it("dispatches sierra-chat-closed on handleSierraOnClose", () => {
const eventSpy = jest.spyOn(window, "dispatchEvent");
wrapper.vm.createSierraConfig().onClose();
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({ type: "sierra-chat-closed" })
);
});
it("dispatches sierra-chat-transfer on handleSierraOnTransfer", () => { it("dispatches sierra-chat-transfer on handleSierraOnTransfer", () => {
const eventSpy = jest.spyOn(window, "dispatchEvent"); const eventSpy = jest.spyOn(window, "dispatchEvent");
const transfer = { const transfer = {

View file

@ -7,10 +7,20 @@ import analyticsMixin from "@/mixins/analytics-mixin";
import store from "@/store"; import store from "@/store";
import { routeData } from "@/router/constants/routes"; import { routeData } from "@/router/constants/routes";
import { deepClone } from "@/helpers/object-helper"; import { deepClone } from "@/helpers/object-helper";
import { webchatHelper } from "@/helpers/webchat-helper";
export default { export default {
name: "sierraWebchat", name: "sierraWebchat",
mixins: [analyticsMixin], mixins: [analyticsMixin],
data() {
return {
transferInProgress: false,
};
},
setup() {
const { webchatGlobalNonpersistedState, launchWebchat } = webchatHelper();
return { webchatGlobalNonpersistedState, launchWebchat };
},
methods: { methods: {
openSierraChatModal() { openSierraChatModal() {
const sierra = window.sierraChat || window.sierra || window.SierraChat; const sierra = window.sierraChat || window.sierra || window.SierraChat;
@ -35,11 +45,18 @@ export default {
display: "corner", display: "corner",
onLoad: () => this.openSierraChatModal(), onLoad: () => this.openSierraChatModal(),
onOpen: () => {}, onOpen: () => {},
onClose: () => window.dispatchEvent(new CustomEvent("sierra-chat-closed")), onClose: () => {
onTransfer: (transfer) => if (!this.transferInProgress) {
this.webchatGlobalNonpersistedState.showWebchatButton = true;
this.transferInProgress = false;
}
},
onTransfer: (transfer) => {
this.transferInProgress = true;
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("sierra-chat-transfer", { detail: transfer }) new CustomEvent("sierra-chat-transfer", { detail: transfer })
), );
},
}; };
}, },
getSierraContext() { getSierraContext() {
@ -138,10 +155,7 @@ export default {
} }
// Pass data to Salesforce chat if available // Pass data to Salesforce chat if available
const transfer = event.detail; const transfer = event.detail;
if ( if (window.embeddedservice_bootstrap && window.embeddedservice_bootstrap.prechatAPI) {
window.embeddedservice_bootstrap &&
typeof window.embeddedservice_bootstrap.bootstrapEmbeddedService === "function"
) {
if ( if (
transfer && transfer &&
transfer.data && transfer.data &&
@ -155,37 +169,16 @@ export default {
LastName: transfer.data.last_name, LastName: transfer.data.last_name,
Email: transfer.data.email, Email: transfer.data.email,
}); });
if (
window.embeddedservice_bootstrap.utilAPI &&
typeof window.embeddedservice_bootstrap.utilAPI.launchChat === "function"
) {
window.embeddedservice_bootstrap.utilAPI.launchChat();
return;
}
} }
window.embeddedservice_bootstrap.utilAPI.launchChat(); this.webchatGlobalNonpersistedState.shouldLaunchSierra = false;
this.launchWebchat();
} }
}; };
window.addEventListener("sierra-chat-transfer", this._handleSierraTransfer); window.addEventListener("sierra-chat-transfer", this._handleSierraTransfer);
// Notify funnel-header about Sierra chat open/close state
this._sierraChatContainerObserver = new MutationObserver(() => {
const isOpen = !!document.querySelector("[data-sierra-chat-container]");
window.dispatchEvent(
new CustomEvent("sierra-chat-visibility", { detail: { open: isOpen } })
);
});
this._sierraChatContainerObserver.observe(document.body, {
childList: true,
subtree: true,
});
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener("launch-sierra-webchat", this.triggerSierraChat); window.removeEventListener("launch-sierra-webchat", this.triggerSierraChat);
window.removeEventListener("sierra-chat-transfer", this._handleSierraTransfer); window.removeEventListener("sierra-chat-transfer", this._handleSierraTransfer);
if (this._sierraChatContainerObserver) {
this._sierraChatContainerObserver.disconnect();
}
}, },
}; };
</script> </script>

View file

@ -79,16 +79,7 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "LogoImage"); return this.getCmsContent(this.cmsWidgetName, "LogoImage");
}, },
shouldShowWebchatButton() { shouldShowWebchatButton() {
// Hide the chat icon if Sierra chat is open return this.webchatGlobalNonpersistedState.showWebchatButton;
if (this.sierraChatOpen) {
return false;
}
// Show if either Sierra is enabled or Salesforce button is available and not hidden by page
return (
(this.webchatGlobalNonpersistedState.shouldLaunchSierra ||
this.webchatGlobalNonpersistedState.showWebchatButton) &&
!this.shouldHideWebchatButtonOnPage
);
}, },
}, },
methods: { methods: {
@ -116,10 +107,6 @@ export default {
webchatClicked(event) { webchatClicked(event) {
event.preventDefault(); event.preventDefault();
this.launchWebchat(); this.launchWebchat();
// Only set sierraChatOpen if Sierra experiment is active
if (this.webchatGlobalNonpersistedState.shouldLaunchSierra) {
this.sierraChatOpen = true;
}
}, },
syncSierraExperimentFlag() { syncSierraExperimentFlag() {
const experiments = store.getters.applicationUser.experiments; const experiments = store.getters.applicationUser.experiments;
@ -159,11 +146,6 @@ export default {
unknownAlertEvent.displayAlert = true; unknownAlertEvent.displayAlert = true;
this.globalAlertMessages.push(unknownAlertEvent); this.globalAlertMessages.push(unknownAlertEvent);
} }
// Listen for Sierra chat open/close events from sierra-webchat
this._handleSierraChatVisibility = (event) => {
this.sierraChatOpen = !!(event.detail && event.detail.open);
};
window.addEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
// Add scroll listener for header shadow effect // Add scroll listener for header shadow effect
this._handleScroll = () => { this._handleScroll = () => {
@ -172,8 +154,6 @@ export default {
window.addEventListener("scroll", this._handleScroll); window.addEventListener("scroll", this._handleScroll);
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener("sierra-chat-closed", this._handleSierraChatClosed);
window.removeEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer); window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
window.removeEventListener("scroll", this._handleScroll); window.removeEventListener("scroll", this._handleScroll);
}, },

View file

@ -25,7 +25,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/, /^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT errorMessages.EMAIL_ADDRESS_FORMAT
) )
); );

View file

@ -8,6 +8,7 @@ const webchatGlobalNonpersistedState = reactive({
export const webchatHelper = () => { export const webchatHelper = () => {
function launchWebchat() { function launchWebchat() {
webchatGlobalNonpersistedState.showWebchatButton = false;
if (webchatGlobalNonpersistedState.shouldLaunchSierra) { if (webchatGlobalNonpersistedState.shouldLaunchSierra) {
window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat
} else { } else {

View file

@ -91,7 +91,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/, /^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT errorMessages.EMAIL_ADDRESS_FORMAT
) )
); );

View file

@ -70,7 +70,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule( defineRule(
"email-address-format", "email-address-format",
regex( regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/, /^([a-zA-Z0-9_.+]+)@([a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT errorMessages.EMAIL_ADDRESS_FORMAT
) )
); );

View file

@ -246,7 +246,7 @@ export default {
console.log(`Price = ${this.amountDue}`); console.log(`Price = ${this.amountDue}`);
console.log(`Adyen Price = ${this.adyenPriceTotal}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`);
const requestBody = this.adyenInitRequestInfo; const requestBody = this.getAdyenInitRequestInfo();
console.log(`Calling with:`); console.log(`Calling with:`);
console.log(requestBody); console.log(requestBody);
@ -294,6 +294,15 @@ export default {
console.log(checkout); 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 = { const configuration = {
paymentMethodsConfiguration: { paymentMethodsConfiguration: {
ideal: { ideal: {
@ -505,14 +514,26 @@ export default {
return; return;
} }
}, },
},
computed: { async resetAdyenDropin() {
piaType() { this?.dropinComponent?.unmount();
return this.$store.getters.order.payment.piaType; 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 { return {
sourceSystem: this.sourceSystem, sourceSystem: this.sourceSystem,
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
@ -529,9 +550,15 @@ export default {
IP: "127.0.0.1", // TODO IP: "127.0.0.1", // TODO
firstName: this.$store.getters.order.customer.firstName, firstName: this.$store.getters.order.customer.firstName,
lastName: this.$store.getters.order.customer.lastName, lastName: this.$store.getters.order.customer.lastName,
idempotencyKey: this.idempotencyKey, idempotencyKey: this.getIdempotencyKey(),
}; };
}, },
},
computed: {
piaType() {
return this.$store.getters.order.payment.piaType;
},
workOrderNumberLastSixDigits() { workOrderNumberLastSixDigits() {
const workOrderNumber = this.$store.getters.order.workOrderNumber ?? ""; const workOrderNumber = this.$store.getters.order.workOrderNumber ?? "";
@ -588,10 +615,6 @@ export default {
}; };
}, },
idempotencyKey() {
return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`;
},
sourceSystem() { sourceSystem() {
return "FMG-2.0"; return "FMG-2.0";
}, },
@ -601,7 +624,7 @@ export default {
}, },
adyenPriceTotal() { adyenPriceTotal() {
return this.amountDue * 100; return Math.round(this.amountDue * 100);
}, },
// Cart info // Cart info

View file

@ -745,10 +745,16 @@ export default {
} }
}, },
updatePaymentMethod(paymentMethod) { updatePaymentMethod(paymentMethod) {
// if the payment method is not set yet, then set it based on what HOP sends us debugLog(
`updatePaymentMethod(${paymentMethod}) and existing store.getters.order.payment.piaType = ${store?.getters?.order?.payment?.piaType}`
);
// if the payment method is not set yet, then set it based on what HOP sends
// or if its not already set to AfterPay, always set it based on what HOP sends
if ( if (
store.getters.order.payment.piaType == paymentMethods.PAY_NOW || store.getters.order.payment.piaType == paymentMethods.PAY_NOW ||
store.getters.order.payment.piaType == paymentMethods.NONE store.getters.order.payment.piaType == paymentMethods.NONE ||
store.getters.order.payment.piaType != paymentMethods.AFTERPAY
) { ) {
console.log(new Date() + " UPDATE PaymentMethod from HOP:" + paymentMethod); console.log(new Date() + " UPDATE PaymentMethod from HOP:" + paymentMethod);
this.dispatchStoreAction( this.dispatchStoreAction(

View file

@ -0,0 +1,70 @@
import policyInfo from "@/layouts/policy-info/policy-info.vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
// 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("policy-info.vue", () => {
describe("component rendering", () => {
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 navbar component", () => {
const { wrapper } = setupMocks();
expect(wrapper.findComponent({ name: "navbar" }).exists()).toBe(true);
});
test("renders Form component", () => {
const { wrapper } = setupMocks();
expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true);
});
});
describe("methods", () => {
test("arePagePrerequisitesValid returns true", () => {
const { wrapper } = setupMocks();
expect(wrapper.vm.arePagePrerequisitesValid()).toBe(true);
});
});
});
function setupMocks() {
const mockCmsContent = {
FunnelHeaderWidget: { Text: "Header" },
FunnelSubHeaderWidget: { Text: "Subheader" },
FunnelFooterWidget: { Text: "Footer" },
};
fetchCmsContentForPage.mockResolvedValue(mockCmsContent);
settleAllPromises.mockResolvedValue({ cmsContent: mockCmsContent });
const mountOptions = getMountOptions({
route: { name: "policy-info", query: {}, params: {} },
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
});
const wrapper = shallowMount(policyInfo, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,72 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container">
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
<!--
... Page code goes here.
-->
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
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 { Form } from "vee-validate";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
export default {
name: "policy-info",
mixins: [],
data() {
return {};
},
components: {
Form,
funnelHeader,
navbar,
funnelSubHeader,
},
// Ensure CMS content is fetched during route navigation without depending on `to`/`from`
async beforeRouteEnter(to, from, next) {
const pageName = to.name;
const cmsContentPromise = fetchCmsContentForPage(pageName);
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
methods: {
async backButtonAction() {
// Stub, for navigating back via nav-bar.
},
async forwardButtonAction() {
// Stub, for navigating forward via nav-bar
},
arePagePrerequisitesValid() {
return true;
},
},
};
</script>

View file

@ -72,10 +72,10 @@ export function isDropOffRouteCode(routeCode) {
); );
} }
export function getTodayDate(routeCode) { export function getTodayDate() {
return new Date(); return new Date();
} }
export function getTodayDateString(routeCode) { export function getTodayDateString() {
return convertDateToDateString(getTodayDate()); return convertDateToDateString(getTodayDate());
} }

View file

@ -0,0 +1,183 @@
// Components
import schedule from "@/layouts/schedule/schedule.vue";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { routeData } from "@/router/constants/routes";
import store from "@/store";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
const storeMocked = {
getters: {
payment: { isInsurance: false },
order: {
payment: { isInsurance: false, insuranceCoverage: { isVerified: false } },
referralNumber: "",
serviceLocation: {
address: "",
address2: "",
city: "",
state: "",
zipCode: "00000",
zipCodeCtu: "000",
appointmentType: null,
provider: {},
isVehicleProtected: false,
},
schedule: {
date: "",
routeCode: "",
startTime: "",
endTime: "",
jobMinMinutes: null,
jobMaxMinutes: null,
},
policy: {
isItac: false,
isNoComp: false,
},
damage: { isRepair: false },
lineItems: { glassParts: [] },
},
vehicle: {
cardId: "",
},
lineItems: {
supportingItems: [],
},
applicationUser: {
experiments: [],
},
isMobileAppointment: false,
},
};
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn(),
}));
describe("schedule.vue", () => {
describe("navigation", () => {
test("backButtonAction => non-insurance: navigateWithoutSaving called", () => {
// Arrange
const { wrapper, mocks } = setupMocks();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalledWith(
mocks.navigationScenarios.CLICKED_BACK,
routeData.SCHEDULE.name
);
});
test("backButtonAction => insurance verified: navigateToHeritageFunnel called", () => {
// Arrange
store.getters.order.payment.isInsurance = true;
store.getters.order.payment.insuranceCoverage = { isVerified: true };
store.getters.order.referralNumber = "";
const { wrapper } = setupMocks();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(navigateToHeritageFunnel).toHaveBeenCalledWith({
shouldSaveSession: false,
pageNameToLog: "schedule",
navType: "back",
});
});
});
describe("computed: isForwardActionDisabled", () => {
test("should be true when routeCode missing or appointmentType not set", async () => {
const { wrapper } = setupMocks();
await wrapper.setData({
selectedTimeSlotInfo: { timeSlot: { routeCode: null } },
appointmentType: null,
});
expect(wrapper.vm.isForwardActionDisabled).toBe(true);
});
test("should be false when routeCode present and appointmentType set", async () => {
const { wrapper } = setupMocks();
await wrapper.setData({
selectedTimeSlotInfo: { timeSlot: { routeCode: "RC123" } },
appointmentType: "SomeType",
});
expect(wrapper.vm.isForwardActionDisabled).toBe(false);
});
});
describe("computed: selectedRouteCodeData", () => {
test("should return null when routeCode not set", async () => {
const { wrapper } = setupMocks();
await wrapper.setData({
selectedTimeSlotInfo: { timeSlot: { routeCode: null } },
});
expect(wrapper.vm.selectedRouteCodeData).toBeNull();
});
test("should return routeCode and premium flag when set", async () => {
const { wrapper } = setupMocks();
await wrapper.setData({
selectedTimeSlotInfo: {
timeSlot: { routeCode: "RC999" },
isPremiumAppointment: true,
},
});
expect(wrapper.vm.selectedRouteCodeData).toEqual({
routeCode: "RC999",
isPremiumAppointment: true,
});
});
});
describe("forwardButtonAction (early exit path)", () => {
test("shows recal acknowledgement popup and does not navigate when required", async () => {
const { wrapper } = setupMocks();
// Force the recal-ack modal path
wrapper.vm.showRecalAcknowledgementModal = jest.fn(() => true);
await wrapper.setData({ isRecalAcknowledgedForScheduling: null });
await wrapper.vm.forwardButtonAction();
expect(wrapper.vm.showRecalAcknowledgementPopup).toBe(true);
expect(wrapper.vm.$router.navigateWithSaving).not.toHaveBeenCalled();
});
});
});
function setupMocks(mountOptionsMockData = {}) {
const route = { name: "schedule" };
const defaultMountOptions = {
route: route,
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
store: { ...storeMocked },
};
const mockBaseMixin = {
methods: {
getTierOnePackagePrice: jest.fn(),
filterOutFees: jest.fn(),
getCmsContent: jest.fn(),
},
};
const baseMountOptions = getMountOptions({
...defaultMountOptions,
...mountOptionsMockData,
});
baseMountOptions.global.mixins = [mockBaseMixin];
baseMountOptions.global.mocks.pageName = route.name;
const wrapper = shallowMount(schedule, baseMountOptions);
wrapper.getCmsContent = jest.fn();
return { wrapper, mocks: baseMountOptions.global?.mocks || {} };
}

View file

@ -98,15 +98,11 @@
linkWidgetName="ServiceZipLinkWidget" linkWidgetName="ServiceZipLinkWidget"
modalWidgetName="ServiceZipModalWidget" /> modalWidgetName="ServiceZipModalWidget" />
</div> </div>
<div <div class="row your-shop-location" v-if="showShopLocationComponent">
class="row your-shop-location"
v-if="appointmentType && appointmentType !== appointmentTypeStrings.MOBILE">
<div class="col-12 col-md-10 col-lg-8 col-xl-7"> <div class="col-12 col-md-10 col-lg-8 col-xl-7">
<div> <div>
<shopLocation <shopLocation
v-if=" v-if="showShopLocationComponent"
appointmentType && appointmentType !== appointmentTypeStrings.MOBILE
"
:selectedShop="selectedShopAnswer" :selectedShop="selectedShopAnswer"
:selectedProviderNumberFromParent="selectedProvider?.providerNumber" :selectedProviderNumberFromParent="selectedProvider?.providerNumber"
:shopProviderDataFromParent="shopProviderData" :shopProviderDataFromParent="shopProviderData"
@ -153,7 +149,8 @@
:pricingByDayBasePrice="pricingByDayBasePrice" :pricingByDayBasePrice="pricingByDayBasePrice"
:pricingByDayUpcharge="pricingByDayUpcharge" :pricingByDayUpcharge="pricingByDayUpcharge"
:showPricingByDay="showPricingByDay" :showPricingByDay="showPricingByDay"
:isPricingByDayExperiment="isPricingByDayExperiment" /> :isPricingByDayExperiment="isPricingByDayExperiment"
:loadingStatus="calendarLoadingStatus" />
<timeSlotQuestion <timeSlotQuestion
ref="timeSlotModalQuestion" ref="timeSlotModalQuestion"
v-if="showTimeSlotQuestion" v-if="showTimeSlotQuestion"
@ -172,7 +169,8 @@
:timeSlotsForSelectedDate="timeSlotsForSelectedDate" :timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:isSameDay="isSameDay" :isSameDay="isSameDay"
:selectedRouteCodeData="selectedRouteCodeData" :selectedRouteCodeData="selectedRouteCodeData"
@waitListRequested="handleWaitListRequested" /> @waitListRequested="handleWaitListRequested"
:lazyLoadAmountToLoad="10" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="navbar" ref="navbar"
@ -445,15 +443,14 @@ export default {
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(), selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
waitListRequested: null, waitListRequested: null,
displayWaitList: null,
pricingByDayUpchargeLineItem: null, pricingByDayUpchargeLineItem: null,
includePricingByDayUpcharge: null, includePricingByDayUpcharge: null,
isPricingByDayExperiment: null, isPricingByDayExperiment: null,
pricingByDayBasePrice: null, pricingByDayBasePrice: null,
pricingByDayUpcharge: null, pricingByDayUpcharge: null,
showPricingByDay: null, showPricingByDay: null,
selectableDatesInshop: [], selectableDatesInshop: {},
selectableDatesMobile: [], selectableDatesMobile: {},
preSelectedDate: null, preSelectedDate: null,
streetAddress: this.getServiceAddressFromStore(), streetAddress: this.getServiceAddressFromStore(),
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(), apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
@ -486,6 +483,7 @@ export default {
experimentSettings.SHOW_MOBILE_FIRST_APPT, experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true" "true"
), ),
calendarLoadingStatus: "none",
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -596,6 +594,8 @@ export default {
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.showLoadingModal();
vm.calendarLoadingStatus = "initial";
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.mobilePremiumAppointmentFee = pricedPremiumFee; vm.mobilePremiumAppointmentFee = pricedPremiumFee;
@ -616,6 +616,9 @@ export default {
); );
vm.initializeDatePicker().then(() => { vm.initializeDatePicker().then(() => {
vm.showMobileFirstModal(); vm.showMobileFirstModal();
vm.hideLoadingModal();
vm.calendarLoadingStatus = "none";
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
}); });
}); });
}, },
@ -632,6 +635,20 @@ export default {
this.handleZipCodeChange(newValue); this.handleZipCodeChange(newValue);
}, },
}, },
showShopLocationComponent() {
// If we get no inshop or mobile data, then show the shoplocation component so the user is not
// looking at a half blank screen with no way to try another zip/location.
if (
(this.selectableDatesInshop === undefined &&
this.selectableDatesMobile === undefined) ||
(this.selectableDatesInshop?.days?.length === 0 &&
this.selectableDatesMobile?.days?.length === 0) ||
(this.appointmentType && this.appointmentType !== AppointmentTypeStrings.MOBILE)
) {
return true;
}
return false;
},
isMobileSelected() { isMobileSelected() {
return ( return (
this.appointmentType == AppointmentTypeStrings.MOBILE || this.appointmentType == AppointmentTypeStrings.MOBILE ||
@ -887,6 +904,53 @@ export default {
? this.$refs.mobileFirstModal?.getIsModalOpen() ? this.$refs.mobileFirstModal?.getIsModalOpen()
: false; : false;
}, },
displayWaitList() {
if (!this.appointmentType) return false;
const firstMobileDateString =
Array.isArray(this.selectableDatesMobile.days) &&
this.selectableDatesMobile.days.length > 0
? this.selectableDatesMobile.days[0].date
: null;
const firstInshopDateString =
Array.isArray(this.selectableDatesInshop.days) &&
this.selectableDatesInshop.days.length > 0
? this.selectableDatesInshop.days[0].date
: null;
const dateString =
this.isMobileSelected && firstMobileDateString
? firstMobileDateString
: firstInshopDateString;
if (
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_WAITLIST,
"true"
) &&
dateString
) {
const [year, month, day] = dateString.split("-").map(Number);
const targetDate = new Date(year, month - 1, day);
const currentDate = getTodayDate();
const futureDate = new Date(currentDate);
const experimentThresholdDays = experimentMixin.methods.hasSetting(
experimentSettings.WAITLIST_THRESHOLD_DAYS
)
? parseInt(
experimentMixin.methods.getSettingValue(
experimentSettings.WAITLIST_THRESHOLD_DAYS
)
)
: 0;
futureDate.setDate(currentDate.getDate() + experimentThresholdDays);
if (targetDate >= futureDate) {
return true;
} else {
return false;
}
}
return false;
},
hasSelectableDatesLoaded() { hasSelectableDatesLoaded() {
if (this.isMobileSelected) { if (this.isMobileSelected) {
return this.selectableDatesMobile?.days?.length > 0; return this.selectableDatesMobile?.days?.length > 0;
@ -1155,9 +1219,10 @@ export default {
); );
} }
}, },
async onShopSelected(shopQuestionPopUpData) { onShopSelected(shopQuestionPopUpData) {
this.resetWaitlist(); this.resetWaitlist();
this.preSelectedDate = null; this.preSelectedDate = null;
this.calendarLoadingStatus = "reinitial";
this.shopProviderData = shopQuestionPopUpData.shopProviderData; this.shopProviderData = shopQuestionPopUpData.shopProviderData;
const oldProvider = this.selectedProvider; const oldProvider = this.selectedProvider;
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => { const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
@ -1177,11 +1242,16 @@ export default {
this.updateSelectedProvider(selectedProvider); this.updateSelectedProvider(selectedProvider);
} }
} }
await this.initializeDatePicker(); this.initializeDatePicker().then(() => {
this.setSelectedDateToFirstAvailable(); this.showMobileFirstModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
});
}, },
async getMoreScheduleData(startDate, endDate) { async getMoreScheduleData(startDate, endDate) {
// called only when "View more dates" is clicked; not on initial page load // called only when "View more dates" is clicked; not on initial page load
this.calendarLoadingStatus = "more";
const moreShopTimeSlots = await getScheduleApiResponse({ const moreShopTimeSlots = await getScheduleApiResponse({
startDateString: startDate, startDateString: startDate,
endDateString: endDate, endDateString: endDate,
@ -1193,6 +1263,7 @@ export default {
includeMobileTimeSlots: this.isServiceableMobile, includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff, includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
}); });
this.calendarLoadingStatus = "none";
// ADD API CALL RESULTS TO EXISTING DATE DATA // ADD API CALL RESULTS TO EXISTING DATE DATA
this.selectableDatesInshop.days = this.selectableDatesInshop.days.concat( this.selectableDatesInshop.days = this.selectableDatesInshop.days.concat(
@ -1231,7 +1302,6 @@ export default {
await datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData; await datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData;
this.selectableDatesMobile = this.selectableDatesMobile =
await datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData; await datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData;
this.setDisplayWaitList();
if (this.preSelectedDate) { if (this.preSelectedDate) {
this.selectedDate = this.preSelectedDate; this.selectedDate = this.preSelectedDate;
@ -1243,11 +1313,8 @@ export default {
) { ) {
await this.$nextTick(); await this.$nextTick();
await this.$refs.datePicker.showAnotherMonth(); await this.$refs.datePicker.showAnotherMonth();
this.setDisplayWaitList();
} }
} }
this.isShowMobileFirstAppt && this.hideLoadingModal();
}, },
getIsRecalAcknowledgedForSchedulingFromStore() { getIsRecalAcknowledgedForSchedulingFromStore() {
return store.getters.order.isRecalAcknowledgedForScheduling; return store.getters.order.isRecalAcknowledgedForScheduling;
@ -1261,8 +1328,8 @@ export default {
if (isMobileSelected === undefined) { if (isMobileSelected === undefined) {
isMobileSelected = this.appointmentType === AppointmentTypeStrings.MOBILE; isMobileSelected = this.appointmentType === AppointmentTypeStrings.MOBILE;
} }
let storedDate = store.getters.order.schedule.date; let storedDate = store.getters.order.schedule?.date;
if (isMobileSelected) storedDate += "-mobile"; if (isMobileSelected && storedDate) storedDate += "-mobile";
return storedDate; return storedDate;
}, },
getSelectedTimeSlotInfo() { getSelectedTimeSlotInfo() {
@ -1517,39 +1584,6 @@ export default {
} }
this.showLoadingModal(); this.showLoadingModal();
}, },
setDisplayWaitList() {
const dateString = this.isMobileSelected
? this.selectableDatesMobile?.days[0]?.date
: this.selectableDatesInshop?.days[0]?.date;
if (
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_WAITLIST,
"true"
) &&
dateString
) {
const [year, month, day] = dateString.split("-").map(Number);
const targetDate = new Date(year, month - 1, day);
const currentDate = getTodayDate();
const futureDate = new Date(currentDate);
const experimentThresholdDays = experimentMixin.methods.hasSetting(
experimentSettings.WAITLIST_THRESHOLD_DAYS
)
? parseInt(
experimentMixin.methods.getSettingValue(
experimentSettings.WAITLIST_THRESHOLD_DAYS
)
)
: 0;
futureDate.setDate(currentDate.getDate() + experimentThresholdDays);
if (targetDate >= futureDate) {
this.displayWaitList = true;
} else {
this.displayWaitList = false;
}
}
},
updateSupportingItems() { updateSupportingItems() {
const supportingItems = this.getSupportingItems(); const supportingItems = this.getSupportingItems();
@ -1758,6 +1792,7 @@ export default {
} }
this.resetMobileLocation(); this.resetMobileLocation();
this.calendarLoadingStatus = "initial";
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => { getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
this.shopProviderData = result.data; this.shopProviderData = result.data;
@ -1768,6 +1803,9 @@ export default {
this.selectedDate = null; this.selectedDate = null;
this.initializeDatePicker().then(() => { this.initializeDatePicker().then(() => {
this.showMobileFirstModal(); this.showMobileFirstModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
}); });
}); });
}, },
@ -1820,7 +1858,6 @@ export default {
this.appointmentType = null; this.appointmentType = null;
} }
this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now 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() { setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate(); this.selectedDate = this.getFirstAvailableDate();

View file

@ -36,7 +36,10 @@
isInshopOrDropOffAppointment ? 'is-inshop-or-drop-off-appointment' : '', isInshopOrDropOffAppointment ? 'is-inshop-or-drop-off-appointment' : '',
]" ]"
:answers="availableTimeSlots" :answers="availableTimeSlots"
:lazyLoad="{ isLazyLoad: !isMobileAppointment, amountToLoad: 10 }" :lazyLoad="{
isLazyLoad: isLazyLoad,
amountToLoad: lazyLoadAmountToLoad || 10,
}"
groupName="chooseTimeSlot" groupName="chooseTimeSlot"
textPosition="text-center" textPosition="text-center"
questionText="Available times:" questionText="Available times:"
@ -134,6 +137,7 @@ export default {
isSameDay: Boolean, isSameDay: Boolean,
displayWaitList: Boolean, displayWaitList: Boolean,
selectedRouteCodeData: Object, selectedRouteCodeData: Object,
lazyLoadAmountToLoad: Number,
}, },
data() { data() {
return { return {
@ -396,6 +400,26 @@ export default {
!this.isDropOffAppointmentAvailable !this.isDropOffAppointmentAvailable
); );
}, },
isLazyLoad() {
if (this.availableTimeSlots?.length < this.lazyLoadAmountToLoad + 1) return false;
if (this.isMobileAppointment) return false;
if (this.selectedAnswerForTimeSlots && !this.isSelectedInshopTimeSlotShowing)
return false;
return true;
},
availableTimeSlotsForInshop() {
return this.getAvailableTimeSlotsForInshop(this.timeSlotsForSelectedDate?.timeSlots);
},
isSelectedInshopTimeSlotShowing() {
if (!this.selectedAnswerForTimeSlots) return false;
const idx = this.availableTimeSlotsForInshop?.findIndex((timeSlot) => {
return timeSlot.value === this.selectedAnswerForTimeSlots;
});
if (idx > -1 && idx < this.lazyLoadAmountToLoad) {
return true;
}
return false;
},
}, },
methods: { methods: {
updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) { updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) {
@ -453,6 +477,7 @@ export default {
// } // }
// }, // },
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) { getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
if (!timeSlotsForSelectedDate) return;
return timeSlotsForSelectedDate return timeSlotsForSelectedDate
.filter((timeSlot) => { .filter((timeSlot) => {
return !isDropOffRouteCode(timeSlot.id); return !isDropOffRouteCode(timeSlot.id);

View file

@ -93,6 +93,7 @@ import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { debugLog } from "@/helpers/debug-log-helper";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
@ -144,6 +145,7 @@ export default {
if (store.getters.externalParameterState?.isExternalParameter) { if (store.getters.externalParameterState?.isExternalParameter) {
await nextTick(); await nextTick();
const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm);
debugLog(`**** vehicle-damage is form valid: ${isValid} ****`);
if ( if (
store.getters.vehicle?.vehicleSubType === "MOTOR HOME" && store.getters.vehicle?.vehicleSubType === "MOTOR HOME" &&
store.getters.externalParameterDamage.damageType?.toUpperCase() === store.getters.externalParameterDamage.damageType?.toUpperCase() ===
@ -183,7 +185,10 @@ export default {
const selectedGlassToReplace = const selectedGlassToReplace =
resultMap.damageOptions.windshieldOptions.availableReplacementOptions; resultMap.damageOptions.windshieldOptions.availableReplacementOptions;
if (selectedGlassToReplace == damageLocationsSelected.SINGLE) { if (
selectedGlassToReplace == damageLocationsSelected.SINGLE &&
store.getters.externalParameterDamage.damageType
) {
vm.selectedWindshieldOptions.selectedWindshieldReplaceOptions.push( vm.selectedWindshieldOptions.selectedWindshieldReplaceOptions.push(
damageLocationsSelected.SINGLE damageLocationsSelected.SINGLE
); );
@ -251,6 +256,8 @@ export default {
if (store.getters.vehicle.carId) { if (store.getters.vehicle.carId) {
return true; return true;
} }
debugLog("**** Vehicle Page Prereq invalid ****");
debugLog("store.getters.vehicle.carId:", store.getters.vehicle?.carId, true);
return false; return false;
}, },
attachCustomEvents() { attachCustomEvents() {

View file

@ -31,7 +31,6 @@ import { applicationConfig } from "../constants/application-config";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routeData } from "@/router/constants/routes"; import { routeData } from "@/router/constants/routes";
import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { Variables } from "../constants/analytics";
import { containsRecalParts, getRecalPartNumbers } from "@/helpers/recal-helper"; import { containsRecalParts, getRecalPartNumbers } from "@/helpers/recal-helper";
import { getAmountDue, getSubTotal, getSalesTax } from "@/helpers/pricing-helper.js"; import { getAmountDue, getSubTotal, getSalesTax } from "@/helpers/pricing-helper.js";
import { partTypeStrings } from "@/constants/part-type-strings"; import { partTypeStrings } from "@/constants/part-type-strings";
@ -248,9 +247,25 @@ export default {
} }
var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`; var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`;
var currentPageName = getPageNameFromRouter();
// add query strings to the page name for debugging. on the vehicle page, if from an external link, pull it from the stash
if (currentPageName === "vehicle") {
if (!window.location.search) {
if (store.getters.externalParameterState?.qsStash) {
currentPageName += `${store.getters.externalParameterState.qsStash}`;
}
} else {
currentPageName += `${window.location.search}`;
}
} else {
if (window.location.search) {
currentPageName += `${window.location.search}`;
}
}
var sessionData = {}; var sessionData = {};
sessionData.currentPage = getPageNameFromRouter(); sessionData.currentPage = currentPageName;
sessionData.sid = getSessionIdValue(); sessionData.sid = getSessionIdValue();
sessionData.deviceId = getDeviceIdValue(); sessionData.deviceId = getDeviceIdValue();
sessionData.fmgSessionId = applicationUser?.savedSessionId; sessionData.fmgSessionId = applicationUser?.savedSessionId;
@ -952,9 +967,6 @@ export default {
ValueToLogTypes() { ValueToLogTypes() {
return ValueToLogTypes; return ValueToLogTypes;
}, },
Variables() {
return Variables;
},
}, },
}; };

View file

@ -51,6 +51,10 @@ export const routeData = {
name: "quote", name: "quote",
path: "/quote", path: "/quote",
}, },
POLICY_INFO: {
name: "policy-info",
path: "/policy-info",
},
INSURANCE_COMPANY: { INSURANCE_COMPANY: {
name: "insurance-company", name: "insurance-company",
path: "/insurance-company", path: "/insurance-company",

View file

@ -429,6 +429,10 @@ const routingTable = function () {
}, },
], ],
}, },
{
pageName: routeData.POLICY_INFO.name,
maps: [],
},
{ {
pageName: routeData.INSURANCE_COMPANY.name, pageName: routeData.INSURANCE_COMPANY.name,
maps: [ maps: [

View file

@ -8,6 +8,7 @@ import { storeActions } from "@/constants/store-actions";
import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { externalParameterStatus } from "@/constants/external-parameters"; import { externalParameterStatus } from "@/constants/external-parameters";
import { debugLog } from "@/helpers/debug-log-helper";
export async function initializeFromQueryStrings() { export async function initializeFromQueryStrings() {
// Get advertiser info // Get advertiser info
@ -26,12 +27,13 @@ function setupAdvertisersAndAffiliates() {
} }
function updateExternalParameterState() { function updateExternalParameterState() {
debugLog("--- updateExternalParameterState ---");
const externalParameterYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR); const externalParameterYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR);
const externalParameterMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE); const externalParameterMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE);
const externalParameterModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL); const externalParameterModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL);
const externalParameterStyle = getQuerystringParameter(queryStrings.VEHICLE_STYLE); const externalParameterStyle = getQuerystringParameter(queryStrings.VEHICLE_STYLE);
const externalParameterDamage = getQuerystringParameter(queryStrings.VEHICLE_DAMAGE); const externalParameterDamage = getQuerystringParameter(queryStrings.VEHICLE_DAMAGE);
const externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP); let externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP);
const externalParameterEmail = getQuerystringParameter(queryStrings.EMAIL); const externalParameterEmail = getQuerystringParameter(queryStrings.EMAIL);
const externalParameterIsInsurance = getQuerystringParameter(queryStrings.IS_INSURANCE); const externalParameterIsInsurance = getQuerystringParameter(queryStrings.IS_INSURANCE);
const externalParameterVinSelection = getQuerystringParameter(queryStrings.VIN_SELECTION); const externalParameterVinSelection = getQuerystringParameter(queryStrings.VIN_SELECTION);
@ -39,6 +41,27 @@ function updateExternalParameterState() {
const externalParameterNumberOfChips = getQuerystringParameter(queryStrings.NUMBER_OF_CHIPS); const externalParameterNumberOfChips = getQuerystringParameter(queryStrings.NUMBER_OF_CHIPS);
const externalParameterSource = getQuerystringParameter(queryStrings.EXPERIMENTS); const externalParameterSource = getQuerystringParameter(queryStrings.EXPERIMENTS);
const externalParameterPhoneNumber = getQuerystringParameter(queryStrings.PHONE_NUMBER); const externalParameterPhoneNumber = getQuerystringParameter(queryStrings.PHONE_NUMBER);
if (!externalParameterZipCode) {
externalParameterZipCode = getQuerystringParameter(queryStrings.ZIP_CODE);
}
if (externalParameterYear || externalParameterZipCode) {
debugLog("externalParameterYear:", externalParameterYear);
debugLog("externalParameterMake:", externalParameterMake);
debugLog("externalParameterModel:", externalParameterModel);
debugLog("externalParameterStyle:", externalParameterStyle);
debugLog("externalParameterDamage:", externalParameterDamage);
debugLog("externalParameterZipCode:", externalParameterZipCode);
debugLog("externalParameterEmail:", externalParameterEmail);
debugLog("externalParameterIsInsurance:", externalParameterIsInsurance);
debugLog("externalParameterVinSelection:", externalParameterVinSelection);
debugLog("externalParameterServicePackage:", externalParameterServicePackage);
debugLog("externalParameterNumberOfChips:", externalParameterNumberOfChips);
debugLog("externalParameterSource:", externalParameterSource);
debugLog("externalPhoneNumber:", externalParameterPhoneNumber);
}
if ( if (
externalParameterYear && externalParameterYear &&
externalParameterMake && externalParameterMake &&
@ -51,6 +74,7 @@ function updateExternalParameterState() {
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MAKE, externalParameterMake); store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MAKE, externalParameterMake);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MODEL, externalParameterModel); store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MODEL, externalParameterModel);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_STYLE, externalParameterStyle); store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_STYLE, externalParameterStyle);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_QS_STASH, window.location.search);
} }
if ( if (
externalParameterDamage && externalParameterDamage &&
@ -100,6 +124,7 @@ function updateExternalParameterState() {
externalParameterPhoneNumber externalParameterPhoneNumber
); );
} }
debugLog("--- updateExternalParameterState END ---");
} }
// if there is an existing external parameter state then they have already been through from an external source(LeadGen) and // if there is an existing external parameter state then they have already been through from an external source(LeadGen) and

View file

@ -41,6 +41,7 @@ export const routes = [
createRoute(routeData.CONFIRMATION), createRoute(routeData.CONFIRMATION),
createRoute(routeData.RETURN_USER), createRoute(routeData.RETURN_USER),
createRoute(routeData.MOBILE_DETAILS), createRoute(routeData.MOBILE_DETAILS),
createRoute(routeData.POLICY_INFO),
// Virtual pages (resolve to a non-virtual page.) // Virtual pages (resolve to a non-virtual page.)
createVirtualRoute(routeData.LANDING, landingBeforeEnter), createVirtualRoute(routeData.LANDING, landingBeforeEnter),

View file

@ -516,6 +516,10 @@ export const mutations = {
externalParameterState.vehicle.style = style; externalParameterState.vehicle.style = style;
saveExternalParameterState(externalParameterState); saveExternalParameterState(externalParameterState);
}, },
updateExternalParameterQsStash(state, qsStash) {
externalParameterState.qsStash = qsStash;
saveExternalParameterState(externalParameterState);
},
updateExternalParameterIsRepair(state, isRepair) { updateExternalParameterIsRepair(state, isRepair) {
externalParameterState.vehicleDamage.isRepair = isRepair; externalParameterState.vehicleDamage.isRepair = isRepair;
saveExternalParameterState(externalParameterState); saveExternalParameterState(externalParameterState);
@ -4142,6 +4146,7 @@ function createExternalParameterDefaultState() {
// create default externalParameter state // create default externalParameter state
const externalParameterDefaultState = { const externalParameterDefaultState = {
isExternalParameter: externalParameterStatus.NOT_SET, isExternalParameter: externalParameterStatus.NOT_SET,
qsStash: null,
vehicle: { vehicle: {
year: null, year: null,
make: null, make: null,