Merge branch 'develop' into feature/CASH-2032

This commit is contained in:
kpatel8hs4io 2026-02-17 00:25:19 -05:00 committed by GitHub
commit 5549b6befd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 2366 additions and 3549 deletions

View file

@ -13,6 +13,8 @@ const environmentVariables = {
['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx", ['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30, ['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "", ['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
}; };
const keysOnly = Object.keys(environmentVariables); const keysOnly = Object.keys(environmentVariables);

View file

@ -10,7 +10,6 @@ module.exports = {
}, },
transformIgnorePatterns: ["'/node_modules/(?!vee-validate)"], transformIgnorePatterns: ["'/node_modules/(?!vee-validate)"],
moduleFileExtensions: ["js", "vue"], moduleFileExtensions: ["js", "vue"],
modulePathIgnorePatterns: ["vin-lookup"],
collectCoverageFrom: [ collectCoverageFrom: [
"src/**/*.{js,vue}", "src/**/*.{js,vue}",
"!src/main.js", "!src/main.js",
@ -33,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
@ -45,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

@ -545,7 +545,6 @@
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} }, { path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
{ path: '/parts/wipers/CR00046193/43015', method: 'GET' }, { path: '/parts/wipers/CR00046193/43015', method: 'GET' },
{ path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' }, { path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' },
{ path: '/parts/service-package-discount', method: 'POST', body: {"glassPieces":[{"location":"Windshield","name":"Single"}],"damageType":"Replace","AppName":"FixMyGlass"} },
{ path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' }, { path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' },
] ]
}, },

View file

@ -14,6 +14,8 @@ const GaCategories = {
EVOX: "Evox", EVOX: "Evox",
APPOINTMENT: "Appointment", APPOINTMENT: "Appointment",
SERVICE_LOCATION: "service-location", SERVICE_LOCATION: "service-location",
CONFIRMATION_CLICKED: "confirmation clicked",
RECAL_DISCLAIMER: "Recalibration disclaimer",
}; };
const GaActions = { const GaActions = {
@ -26,6 +28,9 @@ const GaActions = {
MOBILE_AVAILABLE: "mobile_available", MOBILE_AVAILABLE: "mobile_available",
SHOPS_FIRST_DISPLAYED: "shops_first_displayed", SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
MORE_LOCATIONS_CLICKED: "more_locations_clicked", MORE_LOCATIONS_CLICKED: "more_locations_clicked",
YES: "yes",
NO: "no",
ACKNOWLEDGED: "Acknowledged",
}; };
const GaLabels = { const GaLabels = {

View file

@ -32,6 +32,8 @@ const applicationConfig = {
COOKIE_NAME_LENGTH_MAX_LIMIT: 44, COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__, SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
RETURN_USER_PAGE: "/fmg/return-user", RETURN_USER_PAGE: "/fmg/return-user",
ADYEN_CLIENT_KEY: process.env.__VUE_APP_ADYEN_CLIENT_KEY__,
ADYEN_ENVIRONMENT: process.env.__VUE_APP_ADYEN_ENVIRONMENT__,
}; };
export { applicationConfig }; export { applicationConfig };

View file

@ -76,10 +76,6 @@ const endpoints = {
url: "/parts/api/v1/parts/pricing-by-day-part-number", url: "/parts/api/v1/parts/pricing-by-day-part-number",
method: "GET", method: "GET",
}, },
GetServicePackageDiscountPart: {
url: "/parts/api/v1/parts/service-package-discount",
method: "POST",
},
GetServiceabilityDetails: { GetServiceabilityDetails: {
url: "/location/api/v1/location/serviceability-details", url: "/location/api/v1/location/serviceability-details",
method: "GET", method: "GET",
@ -162,7 +158,7 @@ const endpoints = {
}, },
TaxOrderItems: { TaxOrderItems: {
url: "/price/api/v1/price/taxed-order-items", url: "/price/api/v1/price/taxed-order-items",
method: "GET", method: "POST",
}, },
LogExperimentExposureIfAssigned: { LogExperimentExposureIfAssigned: {
url: "/experiments/api/v1/experiments/log-exposure", url: "/experiments/api/v1/experiments/log-exposure",
@ -216,6 +212,10 @@ const endpoints = {
url: "/order/api/v1/order/add-donation-part", url: "/order/api/v1/order/add-donation-part",
method: "POST", method: "POST",
}, },
GetOemAfterMarketParts: {
url: "/parts/api/v1/parts/oem-after-market-parts",
method: "POST",
},
InitializeAdyenPayment: { InitializeAdyenPayment: {
url: "/payment/api/v1/payment/payment/session/initialize-session", url: "/payment/api/v1/payment/payment/session/initialize-session",
method: "POST", method: "POST",

View file

@ -12,6 +12,9 @@ const experimentSettings = {
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture", SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators", DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience", PIA_EXPERIENCE: "PIA Experience",
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
PIA_INSURANCE: "DisplayPIAInsurance", PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA", SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional", IS_EMAIL_OPTIONAL: "isEmailOptional",
@ -33,6 +36,10 @@ const experimentSettings = {
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays", SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays", SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays", 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", USE_ADYEN_PAYMENT: "UseAdyenPayment",
}; };

View file

@ -12,6 +12,7 @@ const partTypeStrings = {
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT", QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION", DONATION: "DONATION",
WINDSHIELD: "WINDSHIELD",
}; };
export { partTypeStrings }; export { partTypeStrings };

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,
@ -80,6 +84,10 @@ export const pageProgressMapper = {
percent: 98.5, percent: 98.5,
step: 4, step: 4,
}, },
"payment-adyen": {
percent: 98.5,
step: 4,
},
confirmation: { confirmation: {
percent: 100, percent: 100,
step: 4, step: 4,

View file

@ -77,4 +77,29 @@ export const quotePageDiscountTable = [
description: "PREM RCL GEOTARGET DISC", description: "PREM RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE, packageLevel: packageNames.TIER_THREE,
}, },
// 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

@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN"; const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
const RECAL_ACK_YES = "Yes";
const RouteCodeFlags = { const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF", ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF", OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
@ -31,4 +33,5 @@ export {
PRICING_BY_DAY_PART_TYPE, PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags, RouteCodeFlags,
cmsWidgetFieldMappings, cmsWidgetFieldMappings,
RECAL_ACK_YES,
}; };

View file

@ -23,6 +23,7 @@ const storeActions = {
GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu", GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts", GET_PARTS: "getParts",
GET_OEM_AFTERMARKET_PARTS: "getOemAfterMarketParts",
GET_WIPERS: "getWipers", GET_WIPERS: "getWipers",
GET_RAIN_REPEL: "getRainRepel", GET_RAIN_REPEL: "getRainRepel",
GET_SUPPORTING_ITEMS: "getSupportingItems", GET_SUPPORTING_ITEMS: "getSupportingItems",
@ -32,7 +33,6 @@ const storeActions = {
GET_MOLDING_QUESTIONS: "getMoldingQuestions", GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart", GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_PRICING_BY_DAY_PART: "getPricingByDayPart", GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots", GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots", GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
@ -111,6 +111,8 @@ const storeActions = {
SAVE_PAYPAL_TOKEN: "savePaypalToken", SAVE_PAYPAL_TOKEN: "savePaypalToken",
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount", SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn", 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", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",
CREATE_SUBMITTED_STATE: "createSubmittedState", CREATE_SUBMITTED_STATE: "createSubmittedState",

View file

@ -69,6 +69,8 @@ const storeMutations = {
LOCK_TOKEN: "updateLockToken", LOCK_TOKEN: "updateLockToken",
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", 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_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
@ -117,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();
@ -281,7 +289,7 @@ export default {
if (event.screenX === 0 && event.screenY === 0) { if (event.screenX === 0 && event.screenY === 0) {
return; return;
} }
this.$emit("date-clicked", date); this.$emit("date-selected", date);
}, },
getWeekStartDate(dateString) { getWeekStartDate(dateString) {
const date = convertDateStringToDate(dateString); const date = convertDateStringToDate(dateString);
@ -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({
@ -466,7 +479,7 @@ export default {
const response = config.getSelectableDatesCallback({ const response = config.getSelectableDatesCallback({
startDateString: initialViewStartDate, startDateString: initialViewStartDate,
endDateString: initialViewEndDate, endDateString: initialViewEndDate,
providerNumber: config.providerNumber, inshopProviderNumber: config.inshopProviderNumber,
zipCode: config.zipCode, zipCode: config.zipCode,
includeMobileTimeSlots: config.includeMobileTimeSlots, includeMobileTimeSlots: config.includeMobileTimeSlots,
includeInshopTimeSlots: config.includeInshopTimeSlots, includeInshopTimeSlots: config.includeInshopTimeSlots,
@ -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

@ -160,6 +160,10 @@ export default {
return this.staticBackdrop ? "static" : "true"; return this.staticBackdrop ? "static" : "true";
}, },
}, },
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.closeModal();
},
components: { components: {
modalButtonMain, modalButtonMain,
}, },

View file

@ -1,4 +1,9 @@
/* eslint-disable */ /* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings( function setupCustomSettings(
modifyEmbeddedSvcCallback, modifyEmbeddedSvcCallback,
@ -7,90 +12,38 @@ function setupCustomSettings(
onWebchatCloseCallback onWebchatCloseCallback
) { ) {
modifyEmbeddedSvcCallback(); modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback); window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback); window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback); window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
} }
/*
The code below is generated from salesforce but modified in the following ways:
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
* CSS is not copied in
* First <script> tag loading in the file from the CDN is not copied in
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForDev
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
Actual call to paste in:
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
*/
export function initializeSalesforceWebchatForDev( export function initializeSalesforceWebchatForDev(
modifyEmbeddedSvcCallback, modifyEmbeddedSvcCallback,
finishedLoadingCallback, finishedLoadingCallback,
onWebchatOpenCallback, onWebchatOpenCallback,
onWebchatCloseCallback onWebchatCloseCallback
) { ) {
var initESW = function (gslbBaseURL) { setupCustomSettings(
embedded_svc.settings.displayHelpButton = true; //Or false modifyEmbeddedSvcCallback,
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US' finishedLoadingCallback,
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert) onWebchatOpenCallback,
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline) onWebchatCloseCallback
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading) );
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session) // START OF SALESFORCE GENERATED CODE
// Settings for Chat // Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) { try {
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form. embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
// Returns a valid button ID.
//};
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
embedded_svc.settings.enabledFeatures = ["LiveAgent"]; embeddedservice_bootstrap.init(
embedded_svc.settings.entryFeature = "LiveAgent";
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
embedded_svc.init(
"https://safelite2--dev.sandbox.my.salesforce.com",
"https://safelite2--dev.sandbox.my.salesforce-sites.com/chat",
gslbBaseURL,
"00D6s0000008r6A", "00D6s0000008r6A",
"Sales_Central_Snap_in", "Sales_Central_TFN",
"https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477",
{ {
baseLiveAgentContentURL: scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com",
"https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content",
deploymentId: "572Wr0000017xKX",
buttonId: "573Wr000000CpgH",
baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat",
eswLiveAgentDevName:
"EmbeddedServiceLiveAgent_Parent04IWr000003yKq9MAE_1961681659a",
isOfflineSupportEnabled: false,
} }
); );
}; } catch (err) {
console.error("Error loading Embedded Messaging: ", err);
if (!window.embedded_svc) {
var s = document.createElement("script");
s.setAttribute(
"src",
"https://safelite2--dev.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js"
);
s.onload = function () {
initESW(null);
};
document.body.appendChild(s);
} else {
initESW("https://service.force.com");
} }
// END OF SALESFORCE GENERATED CODE
} }

View file

@ -1,4 +1,9 @@
/* eslint-disable */ /* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings( function setupCustomSettings(
modifyEmbeddedSvcCallback, modifyEmbeddedSvcCallback,
@ -7,91 +12,39 @@ function setupCustomSettings(
onWebchatCloseCallback onWebchatCloseCallback
) { ) {
modifyEmbeddedSvcCallback(); modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback); window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback); window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback); window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
} }
/*
The code below is generated from salesforce but modified in the following ways:
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
* CSS is not copied in
* First <script> tag loading in the file from the CDN is not copied in
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForDev
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
Actual call to paste in:
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
*/
export function initializeSalesforceWebchatForProd( export function initializeSalesforceWebchatForProd(
modifyEmbeddedSvcCallback, modifyEmbeddedSvcCallback,
finishedLoadingCallback, finishedLoadingCallback,
onWebchatOpenCallback, onWebchatOpenCallback,
onWebchatCloseCallback onWebchatCloseCallback
) { ) {
var initESW = function (gslbBaseURL) { setupCustomSettings(
let embedded_svc = window.embedded_svc; modifyEmbeddedSvcCallback,
embedded_svc.settings.displayHelpButton = true; //Or false finishedLoadingCallback,
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US' onWebchatOpenCallback,
onWebchatCloseCallback
);
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert) // START OF SALESFORCE GENERATED CODE
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline) // Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading) embeddedservice_bootstrap.init(
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
// Settings for Chat
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
// Returns a valid button ID.
//};
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
embedded_svc.settings.entryFeature = "LiveAgent";
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
embedded_svc.init(
"https://safelite2.my.salesforce.com",
"https://safelite2.my.salesforce-sites.com/chat",
gslbBaseURL,
"00D3t000004nvsT", "00D3t000004nvsT",
"Sales_Central_Snap_in", "Sales_Central_TFN",
"https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294",
{ {
baseLiveAgentContentURL: scrt2URL: "https://safelite2.my.salesforce-scrt.com",
"https://c.la13-core2.sfdc-lywfpd.salesforceliveagent.com/content",
deploymentId: "5723t0000008wWB",
buttonId: "5733t000000D90z",
baseLiveAgentURL: "https://d.la13-core2.sfdc-lywfpd.salesforceliveagent.com/chat",
eswLiveAgentDevName:
"EmbeddedServiceLiveAgent_Parent04I3t000000Cq0SEAS_188de1871ea",
isOfflineSupportEnabled: false,
} }
); );
}; } catch (err) {
console.error("Error loading Embedded Messaging: ", err);
if (!window.embedded_svc) {
var s = document.createElement("script");
s.setAttribute("src", "https://safelite2.my.salesforce.com/embeddedservice/5.0/esw.min.js");
s.onload = function () {
initESW(null);
};
document.body.appendChild(s);
} else {
initESW("https://service.force.com");
} }
// END OF SALESFORCE GENERATED CODE
} }

View file

@ -1,4 +1,9 @@
/* eslint-disable */ /* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings( function setupCustomSettings(
modifyEmbeddedSvcCallback, modifyEmbeddedSvcCallback,
@ -7,93 +12,38 @@ function setupCustomSettings(
onWebchatCloseCallback onWebchatCloseCallback
) { ) {
modifyEmbeddedSvcCallback(); modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback); window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback); window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback); window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
} }
/*
The code below is generated from salesforce but modified in the following ways:
* Only the javascript inside the second <script> tag in the provided salesforce code is copy pasted
* CSS is not copied in
* First <script> tag loading in the file from the CDN is not copied in
* Copy the entire contents of the second <script> tag inside the initializeSalesforceWebchatForQa
method and add a call to setupCustomSettings before the 'embedded_svc.init()' method is called
Actual call to paste in:
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
*/
export function initializeSalesforceWebchatForQa( export function initializeSalesforceWebchatForQa(
modifyEmbeddedSvcCallback, modifyEmbeddedSvcCallback,
finishedLoadingCallback, finishedLoadingCallback,
onWebchatOpenCallback, onWebchatOpenCallback,
onWebchatCloseCallback onWebchatCloseCallback
) { ) {
var initESW = function (gslbBaseURL) { setupCustomSettings(
embedded_svc.settings.displayHelpButton = true; //Or false modifyEmbeddedSvcCallback,
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US' finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
// START OF SALESFORCE GENERATED CODE
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert) embeddedservice_bootstrap.init(
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline) "00DTI000005x7c7",
"Sales_Central_TFN",
//embedded_svc.settings.loadingText = ''; //(Defaults to Loading) "https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884",
//embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session)
// Settings for Chat
//embedded_svc.settings.directToButtonRouting = function(prechatFormData) {
// Dynamically changes the button ID based on what the visitor enters in the pre-chat form.
// Returns a valid button ID.
//};
//embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields
//embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId
//embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us)
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
embedded_svc.settings.entryFeature = "LiveAgent";
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
embedded_svc.init(
"https://safelite2--safeliteua.sandbox.my.salesforce.com",
"https://safelite2--safeliteua.sandbox.my.salesforce-sites.com/chat",
gslbBaseURL,
"00DDJ000000HRqO",
"Sales_Central_Snap_in",
{ {
baseLiveAgentContentURL: scrt2URL: "https://safelite2--qa.sandbox.my.salesforce-scrt.com",
"https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content",
deploymentId: "572DJ00000001j3",
buttonId: "573DJ00000001ur",
baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat",
eswLiveAgentDevName:
"EmbeddedServiceLiveAgent_Parent04IDJ0000008OZk2AM_1883057c9a6",
isOfflineSupportEnabled: false,
} }
); );
}; } catch (err) {
console.error("Error loading Embedded Messaging: ", err);
if (!window.embedded_svc) {
var s = document.createElement("script");
s.setAttribute(
"src",
"https://safelite2--safeliteua.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js"
);
s.onload = function () {
initESW(null);
};
document.body.appendChild(s);
} else {
initESW("https://service.force.com");
} }
// END OF SALESFORCE GENERATED CODE
} }

View file

@ -0,0 +1,50 @@
/* eslint-disable */
// ***************************************************************
// This file wraps the Salesforce generated code.
// See the comment below for where you need to paste in the Salesforce generated code.
// You may also need to modify the script tag src located in salesforce-webchat.vue to point to the correct Salesforce instance.
// ***************************************************************
function setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback);
}
export function initializeSalesforceWebchatForSys(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
// START OF SALESFORCE GENERATED CODE
// Copy in the contents of the first <script> tag inside the initEmbeddedMessaging() method
try {
embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US'
embeddedservice_bootstrap.init(
"00DcY0000067qKz",
"Sales_Central_TFN",
"https://safelite2--sys.sandbox.my.site.com/ESWSalesCentralTFN1767797265631",
{
scrt2URL: "https://safelite2--sys.sandbox.my.salesforce-scrt.com",
}
);
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -2,10 +2,20 @@ import { shallowMount } from "@vue/test-utils";
import salesforceWebchat from "./salesforce-webchat.vue"; import salesforceWebchat from "./salesforce-webchat.vue";
import * as devHelper from "./salesforce-helper-dev"; import * as devHelper from "./salesforce-helper-dev";
import * as qaHelper from "./salesforce-helper-qa"; import * as qaHelper from "./salesforce-helper-qa";
import * as sysHelper from "./salesforce-helper-sys";
import * as prodHelper from "./salesforce-helper-prod"; import * as prodHelper from "./salesforce-helper-prod";
import { applicationConfig } from "../../constants/application-config"; import { applicationConfig } from "../../constants/application-config";
describe("salesforceWebchat.vue", () => { describe("salesforceWebchat.vue", () => {
beforeEach(() => {
window.embeddedservice_bootstrap = {
settings: {},
};
});
afterEach(() => {
delete window.embeddedservice_bootstrap;
});
it("renders correctly", () => { it("renders correctly", () => {
const wrapper = shallowMount(salesforceWebchat); const wrapper = shallowMount(salesforceWebchat);
expect(wrapper.exists()).toBe(true); expect(wrapper.exists()).toBe(true);
@ -14,7 +24,7 @@ describe("salesforceWebchat.vue", () => {
it("has the correct default data", () => { it("has the correct default data", () => {
const wrapper = shallowMount(salesforceWebchat); const wrapper = shallowMount(salesforceWebchat);
expect(wrapper.vm.$data).toEqual({ expect(wrapper.vm.$data).toEqual({
isAgentAvailable: false, isWebchatLoaded: false,
isWebchatOpen: false, isWebchatOpen: false,
}); });
}); });
@ -37,6 +47,12 @@ describe("salesforceWebchat.vue", () => {
wrapper.vm.initializeSalesforceWebchat(); wrapper.vm.initializeSalesforceWebchat();
expect(mockQa).toHaveBeenCalled(); expect(mockQa).toHaveBeenCalled();
applicationConfig.CURRENT_ENVIRONMENT = "SysTest";
wrapper = shallowMount(salesforceWebchat);
const mockSys = jest.spyOn(sysHelper, "initializeSalesforceWebchatForSys");
wrapper.vm.initializeSalesforceWebchat();
expect(mockSys).toHaveBeenCalled();
applicationConfig.CURRENT_ENVIRONMENT = "Dev"; applicationConfig.CURRENT_ENVIRONMENT = "Dev";
wrapper = shallowMount(salesforceWebchat); wrapper = shallowMount(salesforceWebchat);
const mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev"); const mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev");

View file

@ -6,6 +6,7 @@
// Supporting files // Supporting files
import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev"; import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev";
import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa"; import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa";
import { initializeSalesforceWebchatForSys } from "./salesforce-helper-sys";
import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod"; import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
import { webchatHelper } from "@/helpers/webchat-helper"; import { webchatHelper } from "@/helpers/webchat-helper";
@ -16,7 +17,7 @@ export default {
mixins: [analyticsMixin], mixins: [analyticsMixin],
data() { data() {
return { return {
isAgentAvailable: false, isWebchatLoaded: false,
isWebchatOpen: false, isWebchatOpen: false,
}; };
}, },
@ -33,32 +34,13 @@ export default {
mounted() { mounted() {
// Load in script from salesforce CDN // Load in script from salesforce CDN
const script = document.createElement("script"); const script = document.createElement("script");
script.src = "https://service.force.com/embeddedservice/5.0/esw.min.js"; script.src = this.webChatScriptSrc;
script.onload = this.initializeSalesforceWebchat; script.onload = this.initializeSalesforceWebchat;
document.body.appendChild(script); document.body.appendChild(script);
// Notify funnel-header about Salesforce chat open/close state
this._salesforceChatOpenObserver = new MutationObserver(() => {
const isOpen = !!document.querySelector(".embeddedServiceSidebar");
window.dispatchEvent(
new CustomEvent("salesforce-chat-visibility", { detail: { open: isOpen } })
);
});
this._salesforceChatOpenObserver.observe(document.body, {
childList: true,
subtree: true,
});
}, },
beforeUnmount() { beforeUnmount() {
// Clean up event listeners and observers // Clean up event listeners and observers
window.removeEventListener(
"salesforce-chat-visibility",
this._handleSalesforceChatVisibility
);
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer); window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
if (this._salesforceChatOpenObserver) {
this._salesforceChatOpenObserver.disconnect();
}
}, },
methods: { methods: {
initializeSalesforceWebchat() { initializeSalesforceWebchat() {
@ -72,7 +54,6 @@ export default {
); );
break; break;
case "QA": case "QA":
case "SysTest":
initializeSalesforceWebchatForQa( initializeSalesforceWebchatForQa(
this.modifyEmbeddedSvcCallback, this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback, this.finishedLoadingCallback,
@ -80,8 +61,17 @@ export default {
this.onWebchatCloseCallback this.onWebchatCloseCallback
); );
break; break;
case "SysTest":
initializeSalesforceWebchatForSys(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
this.onWebchatOpenCallback,
this.onWebchatCloseCallback
);
break;
case "Dev": case "Dev":
case "Localhost": case "Localhost":
default:
initializeSalesforceWebchatForDev( initializeSalesforceWebchatForDev(
this.modifyEmbeddedSvcCallback, this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback, this.finishedLoadingCallback,
@ -92,16 +82,13 @@ export default {
} }
}, },
modifyEmbeddedSvcCallback() { modifyEmbeddedSvcCallback() {
window.embedded_svc.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png"); window.embeddedservice_bootstrap.settings.hideChatButtonOnLoad = true;
window.embedded_svc.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png"); window.addEventListener("onEmbeddedMessagingConversationStarted", () => {
window.embedded_svc.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png");
window.embedded_svc.settings.displayHelpButton = false;
window.embedded_svc.addEventHandler("onChatEstablished", () => {
this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true); this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true);
}); });
}, },
finishedLoadingCallback(data) { finishedLoadingCallback(data) {
this.isAgentAvailable = data.isAgentAvailable; this.isWebchatLoaded = true;
this.updateShouldShowWebchatButton(); this.updateShouldShowWebchatButton();
}, },
onWebchatOpenCallback() { onWebchatOpenCallback() {
@ -117,16 +104,35 @@ export default {
updateShouldShowWebchatButton() { updateShouldShowWebchatButton() {
this.webchatGlobalNonpersistedState.showWebchatButton = this.webchatGlobalNonpersistedState.showWebchatButton =
!this.hideSalesforceWebchatLaunchButton && !this.hideSalesforceWebchatLaunchButton &&
this.isAgentAvailable && this.isWebchatLoaded &&
!this.isWebchatOpen; !this.isWebchatOpen;
}, },
}, },
computed: {}, computed: {
webChatScriptSrc() {
// These URLs are from the src tag on the script tag in the Salesforce generated code.
switch (applicationConfig.CURRENT_ENVIRONMENT) {
case "Prod":
return "https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294/assets/js/bootstrap.min.js";
case "QA":
return "https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884/assets/js/bootstrap.min.js";
case "SysTest":
return "https://safelite2--sys.sandbox.my.site.com/ESWSalesCentralTFN1767797265631/assets/js/bootstrap.min.js";
case "Dev":
case "Localhost":
default:
return "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477/assets/js/bootstrap.min.js";
}
},
},
components: {}, components: {},
}; };
</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 = {
@ -77,25 +69,4 @@ describe("sierraWebchat.vue", () => {
expect.objectContaining({ type: "sierra-chat-transfer" }) expect.objectContaining({ type: "sierra-chat-transfer" })
); );
}); });
it("handles sierra-chat-transfer event and starts Salesforce chat with prepopulated fields", () => {
const transfer = {
data: {
first_name: "A",
last_name: "B",
email: "a@b.com",
chat_summary: "summary",
},
};
const event = new CustomEvent("sierra-chat-transfer", { detail: transfer });
window.dispatchEvent(event);
expect(window.sierraChat.closeChatModal).toHaveBeenCalled();
expect(window.embedded_svc.settings.prepopulatedPrechatFields).toEqual({
FirstName: "A",
LastName: "B",
Email: "a@b.com",
Subject: "summary",
});
expect(window.embedded_svc.liveAgentAPI.startChat).toHaveBeenCalled();
});
}); });

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.embedded_svc &&
typeof window.embedded_svc.bootstrapEmbeddedService === "function"
) {
if ( if (
transfer && transfer &&
transfer.data && transfer.data &&
@ -149,51 +163,22 @@ export default {
transfer.data.last_name && transfer.data.last_name &&
transfer.data.email transfer.data.email
) { ) {
window.embedded_svc.settings.prepopulatedPrechatFields = { window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
ScarlettAISummary: transfer.data.chat_summary,
FirstName: transfer.data.first_name, FirstName: transfer.data.first_name,
LastName: transfer.data.last_name, LastName: transfer.data.last_name,
Email: transfer.data.email, Email: transfer.data.email,
Subject: transfer.data.chat_summary, });
};
window.embedded_svc.settings.extraPrechatFormDetails = [
{
label: "Scarlett AI Summary",
value: transfer.data.chat_summary,
transcriptFields: ["Scarlett_AI_Summary__c"],
displayToAgent: true,
},
];
if (
window.embedded_svc.liveAgentAPI &&
typeof window.embedded_svc.liveAgentAPI.startChat === "function"
) {
window.embedded_svc.liveAgentAPI.startChat();
return;
}
} }
window.embedded_svc.bootstrapEmbeddedService(); 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

@ -59,7 +59,6 @@ export default {
return { return {
globalAlertMessages: [], globalAlertMessages: [],
sierraChatOpen: false, sierraChatOpen: false,
salesforceChatOpen: false,
isSalesforceTransferInProgress: false, isSalesforceTransferInProgress: false,
isScrolled: false, isScrolled: false,
}; };
@ -80,20 +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;
}
// Hide the chat icon if Salesforce chat is open or transfer is in progress
if (this.salesforceChatOpen || this.isSalesforceTransferInProgress) {
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: {
@ -121,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;
@ -164,37 +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);
if (this.sierraChatOpen) {
this.isSalesforceTransferInProgress = false;
}
};
window.addEventListener("sierra-chat-visibility", this._handleSierraChatVisibility);
// Listen for Salesforce chat open/close events from salesforce-webchat
this._handleSalesforceChatVisibility = (event) => {
this.salesforceChatOpen = !!(event.detail && event.detail.open === true);
};
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatVisibility);
// Listen for transfer event to set transfer-in-progress flag
this._handleSierraToSalesforceTransfer = () => {
this.isSalesforceTransferInProgress = true;
};
window.addEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
// Reset transfer flag when Salesforce chat opens
this._handleSalesforceChatOpened = (event) => {
if (event.detail && event.detail.open === true) {
this.isSalesforceTransferInProgress = false;
this.salesforceChatOpen = true;
} else {
this.salesforceChatOpen = false;
}
};
window.addEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
// Add scroll listener for header shadow effect // Add scroll listener for header shadow effect
this._handleScroll = () => { this._handleScroll = () => {
@ -203,14 +154,7 @@ 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(
"salesforce-chat-visibility",
this._handleSalesforceChatVisibility
);
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer); window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
window.removeEventListener("salesforce-chat-visibility", this._handleSalesforceChatOpened);
window.removeEventListener("scroll", this._handleScroll); window.removeEventListener("scroll", this._handleScroll);
}, },
}; };

View file

@ -57,10 +57,6 @@ export default {
modalWidgetName: String, modalWidgetName: String,
pageName: String, pageName: String,
}, },
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal?.closeModal();
},
computed: { computed: {
buttonText() { buttonText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText"); return this.getCmsContent(this.modalWidgetName, "SubheaderText");

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

@ -54,10 +54,6 @@ export default {
mounted() { mounted() {
if (this.showSaveProgressPopup) this.modal.openModal(); if (this.showSaveProgressPopup) this.modal.openModal();
}, },
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal.closeModal();
},
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
props: { props: {
modelValue: Object, modelValue: Object,

View file

@ -4,6 +4,7 @@ import globalMethods from "@/global-methods";
import { endpoints } from "@/constants/endpoints"; import { endpoints } from "@/constants/endpoints";
import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { paymentMethods } from "@/constants/payment-method-constants"; import { paymentMethods } from "@/constants/payment-method-constants";
import { applicationConfig } from "../constants/application-config";
export async function getNewSession(sessionConfig) {} export async function getNewSession(sessionConfig) {}
@ -42,8 +43,8 @@ export async function createAdyenCheckout({
id: id, id: id,
sessionData: sessionData, sessionData: sessionData,
}, },
clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", clientKey: applicationConfig.ADYEN_CLIENT_KEY,
environment: "test", environment: applicationConfig.ADYEN_ENVIRONMENT,
locale: "en_US", locale: "en_US",
countryCode: "US", countryCode: "US",
showPayButton: showPayButton, showPayButton: showPayButton,
@ -83,6 +84,21 @@ export function mapAdyenToFmgPaymentMethod(adyenMethod) {
return match ?? paymentMethods.CREDIT_CARD; return match ?? paymentMethods.CREDIT_CARD;
} }
export function mapFmgToAdyenPaymentMethod(fmgMethod) {
const paymentMethodMap = {
[paymentMethods.CREDIT_CARD]: "scheme",
[paymentMethods.AFTERPAY]: "afterpaytouch",
[paymentMethods.PAYPAL]: "paypal",
[paymentMethods.APPLE_PAY]: "applepay",
[paymentMethods.PAY_NOW]: null,
[paymentMethods.LATER]: null,
};
const match = paymentMethodMap[fmgMethod];
return match;
}
export function generateCcToken(adyenSessionInfo) { export function generateCcToken(adyenSessionInfo) {
const order = store.getters.order; const order = store.getters.order;
const locationInfo = getLocationInfo(order); const locationInfo = getLocationInfo(order);

View file

@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION]; 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) { export function isRecalPartOrHasChildRecalPart(lineItem) {
if (lineItem.childParts && lineItem.childParts.length > 0) { if (lineItem.childParts && lineItem.childParts.length > 0) {
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts); return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);

View file

@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
return package_names[discountPackage] || null; return package_names[discountPackage] || null;
} }
export function getPackageLabel(packageName) {
switch (packageName) {
case packageNames.TIER_ONE:
return externalParamPackageLabels.GLASS_ONLY;
case packageNames.TIER_TWO:
return externalParamPackageLabels.STANDARD;
case packageNames.TIER_THREE:
return externalParamPackageLabels.PREMIUM;
default:
return null;
}
}
function maxTier(tierA, tierB) { function maxTier(tierA, tierB) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) { if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return packageNames.TIER_THREE; return packageNames.TIER_THREE;

View file

@ -8,10 +8,11 @@ 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 {
window.embedded_svc.bootstrapEmbeddedService(); // launches the salesForce prechat form window.embeddedservice_bootstrap.utilAPI.launchChat(); // launches the salesForce prechat form
} }
} }

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

@ -164,6 +164,8 @@ export default {
// await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it // await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it
await flushSaveSessionQueue(); await flushSaveSessionQueue();
await this.removeQPDLineItemIfExists();
await this.dispatchStoreAction( await this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER, this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
this.parentAccountNumber.toString(), this.parentAccountNumber.toString(),
@ -193,6 +195,17 @@ export default {
handlePageShow() { handlePageShow() {
window.location.reload(); window.location.reload();
}, },
async removeQPDLineItemIfExists() {
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.supportingItems = baseMixin.methods.filterOutQuotePageDiscountPart(
lineItems.supportingItems
);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
lineItems.supportingItems,
false
);
},
}, },
unmounted() { unmounted() {
window.removeEventListener("pageshow", this.handlePageShow); window.removeEventListener("pageshow", this.handlePageShow);

View file

@ -25,6 +25,11 @@
cmsWidgetName="PleaseProvideAddressWidget" cmsWidgetName="PleaseProvideAddressWidget"
class="provide-address-header" /> class="provide-address-header" />
<textBlock <textBlock
v-if="isMSRFeeApplicable()"
cmsWidgetName="VehicleKeysMSRMessageWidget"
class="keys-message" />
<textBlock
v-else
cmsWidgetName="VehicleKeysMessageWidget" cmsWidgetName="VehicleKeysMessageWidget"
class="keys-message" /> class="keys-message" />
<mobileAddressQuestions <mobileAddressQuestions
@ -143,6 +148,9 @@ export default {
getIsVehicleProtectedFromStore() { getIsVehicleProtectedFromStore() {
return store.getters.order.serviceLocation.isVehicleProtected; return store.getters.order.serviceLocation.isVehicleProtected;
}, },
isMSRFeeApplicable() {
return store.getters.order.isMSRFeeApplicable;
},
async forwardButtonAction() { async forwardButtonAction() {
await this.dispatchStoreAction( await this.dispatchStoreAction(
this.storeActions.SAVE_MOBILE_DETAILS, this.storeActions.SAVE_MOBILE_DETAILS,

View file

@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
//Assert //Assert
expect(spy).toHaveBeenNthCalledWith( expect(spy).toHaveBeenNthCalledWith(
1, 1,
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
false,
false
);
expect(spy).toHaveBeenNthCalledWith(
2,
"savePartQuestionAnswers", "savePartQuestionAnswers",
[ [
{ {

View file

@ -188,6 +188,8 @@ export default {
} }
}); });
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// save to vuex store as order.damage.partQuestionAnswers (array) // save to vuex store as order.damage.partQuestionAnswers (array)
// used in GET_PARTS call following this one // used in GET_PARTS call following this one
await this.dispatchStoreAction( await this.dispatchStoreAction(

View file

@ -1,18 +1,48 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="container-fluid"> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="row justify-content-center"> <div class="container">
<div class="col-md-6"> <div class="row">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <div class="col-12">
</div> <div>
</div> <alert
<div class="row justify-content-center"> ref="piaErrorAlert"
<div class="col-md-6 col-xl-4"> class="my-4"
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" /> v-if="hasPaymentFailureError"
<!-- cmsWidgetName="PIAErrorAlertWidget"
... Page code goes here. alertClass="alert-danger"
--> @textLinkClicked="paymentFailedPayLater"
<div id="adyen-container"></div> v-bind:isDismissible="false" />
</div>
<div id="card-container">
<div id="payment-container">
<funnelSubHeader class="py-5" cmsWidgetName="FunnelSubHeaderWidget" />
<div id="adyen-container"></div>
</div>
<div id="cart-container">
<cart
ref="cart"
:damage="damageInfo"
:availableVaps="[]"
:allowItemRemoval="false"
v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal"
:isInsurance="isInsurance"
:insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs"
:isItac="isItac"
:isNoComp="isNoComp"
:isCollapsible="false" />
<textBlock
class="payment-disclaimer"
marginTopSizeOverride="0"
cmsWidgetName="PaymentDisclaimerWidget" />
</div>
</div>
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="navbar" ref="navbar"
@ -29,6 +59,8 @@ import Form from "vee-validate";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; 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 { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
@ -42,9 +74,16 @@ import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"
import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { createAdyenCheckout } from "@/helpers/adyen-helper";
import { Dropin } from "@adyen/adyen-web/auto"; import { Dropin } from "@adyen/adyen-web/auto";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants";
import "@adyen/adyen-web/styles/adyen.css"; import "@adyen/adyen-web/styles/adyen.css";
import { mapAdyenToFmgPaymentMethod } from "../../helpers/adyen-helper"; import { mapAdyenToFmgPaymentMethod, mapFmgToAdyenPaymentMethod } from "../../helpers/adyen-helper";
import { showFmgLoadingModal } from "../../helpers/loading-modal-helper";
import cart from "@/fmg-components/cart/cart";
import { coverageStatus } from "@/constants/insurance";
import { deepClone } from "@/helpers/object-helper";
import store from "@/store";
import { debugLog } from "@/helpers/debug-log-helper.js";
export default { export default {
name: "payment-adyen", name: "payment-adyen",
@ -52,6 +91,8 @@ export default {
data() { data() {
return { return {
sessionId: String, sessionId: String,
dropinComponent: null,
hasPaymentFailureError: false,
}; };
}, },
@ -69,9 +110,15 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
const hasExistingPiaError = to.query?.piaFailure;
// Hydrate page with results // Hydrate page with results
next(async (vm) => { next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
if (hasExistingPiaError) {
vm.hasPaymentFailureError = true;
}
}); });
}, },
@ -89,18 +136,117 @@ export default {
}, },
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
// Stub, for checking if application is capable of entering this page // Service Location
// And/or, is all information required for this page present? const serviceLocation = store.getters.order.serviceLocation;
// To prevent sequence-breaking from the end-user. const mobileReqs = !!(
serviceLocation.address &&
serviceLocation.city &&
serviceLocation.state &&
serviceLocation.zipCode
);
return true; const providerLocation = serviceLocation.provider.address;
const dropOffInshopReqs = !!(
providerLocation.streetAddress &&
providerLocation.city &&
providerLocation.state &&
providerLocation.zipCode
);
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
const serviceLocationReqs =
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
// Insurance
const isInsuranceSet = store.getters.order.payment.isInsurance !== null;
// Schedule
const schedule = store.getters.order.schedule;
const scheduleReqs = !!(
schedule.date &&
schedule.startTime &&
schedule.endTime &&
schedule.jobMaxMinutes &&
schedule.jobMinMinutes
);
// Customer
const customer = store.getters.order.customer;
const customerReqs = !!(
customer.firstName &&
customer.lastName &&
customer.phoneNumber &&
customer.emailAddress
);
const paymentMethodReqs =
store.getters.order.payment.isPia !== null &&
(store.getters.order.payment.isPia || !!store.getters.order.payment.piaType);
const preReqResult =
serviceLocationReqs &&
isInsuranceSet &&
scheduleReqs &&
customerReqs &&
paymentMethodReqs;
// prettier-ignore
{
debugLog("--- payment-adyen.vue pagePrereqs start ---", null, !preReqResult);
debugLog("mobileReqs:", mobileReqs, !preReqResult);
debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult);
debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult);
debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult);
debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult);
debugLog("", null, !preReqResult);
debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult);
debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult);
debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult);
debugLog("providerLocation.city:", providerLocation.city, !preReqResult);
debugLog("providerLocation.state:", providerLocation.state, !preReqResult);
debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult);
debugLog("", null, !preReqResult);
debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult);
debugLog("", null, !preReqResult);
debugLog("scheduleReqs:", scheduleReqs, !preReqResult);
debugLog("schedule.date:", schedule.date, !preReqResult);
debugLog("schedule.startTime:", schedule.startTime, !preReqResult);
debugLog("schedule.endTime:", schedule.endTime, !preReqResult);
debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult);
debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult);
debugLog("", null, !preReqResult);
debugLog("customerReqs:", customerReqs, !preReqResult);
debugLog("customer.firstName:", customer.firstName, !preReqResult);
debugLog("customer.lastName:", customer.lastName, !preReqResult);
debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult);
debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult);
debugLog("", null, !preReqResult);
debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult);
debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult);
debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult);
debugLog("--- payment-adyen.vue pagePrereqs end ---", null, !preReqResult);
}
return preReqResult;
}, },
async initializeAdyen() { async initializeAdyen() {
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);
@ -148,8 +294,16 @@ export default {
console.log(checkout); console.log(checkout);
window.checkout = checkout; const expiryTime = new Date(checkout.options.expiresAt);
const dropin = new Dropin(checkout, { const expiryInterval = expiryTime.getTime() - new Date().getTime();
const handleTimeout = () => {
this.resetAdyenDropin();
};
const timeout = setTimeout(handleTimeout, expiryInterval);
const configuration = {
paymentMethodsConfiguration: { paymentMethodsConfiguration: {
ideal: { ideal: {
showImage: true, showImage: true,
@ -159,10 +313,10 @@ export default {
value: this.adyenPriceTotal, value: this.adyenPriceTotal,
currency: "USD", currency: "USD",
}, },
environment: "test", // Change this to "live" when you're ready to accept live PayPal payments environment: applicationConfig.ADYEN_ENVIRONMENT,
countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production. countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production.
blockPayPalVenmoButton: false, blockPayPalVenmoButton: true,
//blockPayPalPayLaterButton: true blockPayPalPayLaterButton: true,
}, },
card: { card: {
hasHolderName: true, hasHolderName: true,
@ -171,7 +325,21 @@ export default {
name: "Credit or debit card", name: "Credit or debit card",
}, },
}, },
}).mount("#adyen-container"); };
const adyenPaymentType = mapFmgToAdyenPaymentMethod(this.piaType);
if (adyenPaymentType) {
configuration.openPaymentMethod = {
type: adyenPaymentType,
};
}
const dropin = new Dropin(checkout, configuration);
this.dropinComponent = dropin;
dropin.mount("#adyen-container");
}, },
async handleCompletedPayment(result) { async handleCompletedPayment(result) {
@ -189,6 +357,8 @@ export default {
console.log(`Get session payload =`); console.log(`Get session payload =`);
console.log(paymentSessionRequest); console.log(paymentSessionRequest);
showFmgLoadingModal(true);
const response = await globalMethods.callHttpClient({ const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method, method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url, endpoint: endpoints.GetAdyenSessionResult.url,
@ -233,7 +403,20 @@ export default {
false false
); );
await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); if (paymentMethod === paymentMethods.PAYPAL) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PAYPAL_TOKEN,
responseJson?.storedToken,
false
);
} else {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_CCTOKEN,
ccToken,
false
);
}
await baseMixin.methods.dispatchStoreAction( await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
this.amountDue, this.amountDue,
@ -246,33 +429,33 @@ export default {
console.log(`Result =`); console.log(`Result =`);
console.log(result); console.log(result);
// Fetch session info const wasPaymentCancelled = result?.resultCode === "Cancelled";
const paymentSessionRequest = {
zipCodeCtu: this.locationInfo.zipCodeCtu,
workOrderNumber: this.workOrderNumberLastSixDigits,
sourceSystem: this.sourceSystem,
sessionId: this.sessionId,
sessionResult: result?.sessionResult,
};
console.log(`Get session payload =`); if (!wasPaymentCancelled) {
console.log(paymentSessionRequest); const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`;
try { await global.$logger.logError(stringToLog);
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: paymentSessionRequest,
logApiCall: true,
pageNameToLog: "payment-adyen",
});
console.log(response); this.hasPaymentFailureError = true;
} catch (error) {
console.log(`Error getting session info after failure.`);
} }
this.dropinComponent?.update();
},
async handleError(error) {
console.log(error);
const wasPaymentCancelled = error?.name === "CANCEL";
if (!wasPaymentCancelled) {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
await global.$logger.logError(stringToLog);
this.hasPaymentFailureError = true;
}
this.dropinComponent?.update();
}, },
handleError(error) {},
async saveAndSubmitWorkOrder() { async saveAndSubmitWorkOrder() {
// Work order submission after successful payment. // Work order submission after successful payment.
@ -285,15 +468,12 @@ export default {
} catch (error) { } catch (error) {
console.log(`error: response from submit work order: ${error.message}`); console.log(`error: response from submit work order: ${error.message}`);
// navigate to error page this.$router.navigateWithoutSaving(
const errorPayload = { this.navigationScenarios.PIA_ERROR,
cause: `Error while submitting work order after Adyen payment.`, this.pageName
currentPage: this.pageName, );
sessionId: this.sessionId,
idempotencyKey: this.idempotencyKey,
};
this.$router.handleSoftError(errorPayload); return;
} }
// clear order // clear order
@ -303,10 +483,57 @@ export default {
this.pageName this.pageName
); );
}, },
},
computed: { async paymentFailedPayLater() {
adyenInitRequestInfo() { console.log(`Doing final work order submission...`);
showFmgLoadingModal(true);
await this.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethods.PAY_LATER,
false
);
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
try {
await submitWorkOrder({
pageNameToLog: this.pageName,
submitAfterSave: true,
});
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_PAY_LATER,
this.pageName
);
} catch (error) {
console.log(`Final submission failed after failed payment.`);
this.$router.handleSoftError();
return;
}
},
async resetAdyenDropin() {
this?.dropinComponent?.unmount();
await this.initializeAdyen();
},
// 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,
@ -323,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 ?? "";
@ -382,10 +615,6 @@ export default {
}; };
}, },
idempotencyKey() {
return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`;
},
sourceSystem() { sourceSystem() {
return "FMG-2.0"; return "FMG-2.0";
}, },
@ -395,7 +624,41 @@ export default {
}, },
adyenPriceTotal() { adyenPriceTotal() {
return this.amountDue * 100; return Math.round(this.amountDue * 100);
},
// Cart info
damageInfo() {
return this.$store.getters.damage;
},
isInsurance() {
return this.$store.getters.payment.isInsurance;
},
isNoComp() {
return this.$store.getters.policy.isNoComp;
},
isItac() {
return this.$store.getters.policy.isItac;
},
showInsuranceCoverageAs() {
if (!this.isInsurance) {
return null;
}
if (this.isNoComp || this.isItac) {
return coverageStatus.NOCOMP;
} else {
return this.$store.getters.payment.insuranceCoverage.coverageStatus;
}
},
currentDeductible() {
return this.$store.getters.policy.currentDeductible;
},
insuranceCompanyName() {
return this.$store.getters.policy.insuranceCompanyName;
},
lineItems() {
return deepClone(this.$store.getters.lineItems);
}, },
}, },
@ -408,6 +671,39 @@ export default {
funnelHeader, funnelHeader,
funnelSubHeader, funnelSubHeader,
navbar, navbar,
cart,
alert,
textBlock,
}, },
}; };
</script> </script>
<style lang="scss">
#card-container {
display: flex;
& > #payment-container {
flex-basis: 55%;
margin-right: 9%;
}
& > #cart-container {
flex-basis: 36%;
margin-top: 2rem;
}
@include media-breakpoint-down(lg) {
flex-direction: column;
& > #payment-container {
flex-basis: auto;
margin-right: 0;
}
& > #cart-container {
flex-basis: auto;
margin: 0 0.5rem;
}
}
.payment-disclaimer p {
font-size: 0.875rem;
line-height: 1.2;
}
}
</style>

View file

@ -810,11 +810,51 @@ export default {
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE); const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
if (this.isInsurance) { if (this.isInsurance) {
return piaInsurance === "true" && this.currentDeductible > 0; return (
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
this.currentDeductible > 0
);
} else { } 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() { paymentMethod() {
if (!this.isPiaEnabled) { if (!this.isPiaEnabled) {
return paymentMethods.LATER; return paymentMethods.LATER;

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

@ -253,9 +253,10 @@ export default {
height: 40px; height: 40px;
} }
h3 { h3 {
font-family: UrbanistSemibold; font-family: UrbanistBold;
font-size: 1rem; font-size: 1rem;
margin: 0 0 0 0.5rem; margin: 0 0 0 0.5rem;
color: $black;
} }
} }
p { p {

View file

@ -0,0 +1,167 @@
<template>
<div
class="oem-glass-question-container py-5 mt-3 mb-6"
:class="[isExpanded ? 'expanded' : '']">
<div class="oem-glass-question-header" @click="toggleIsExpanded">
{{ headerText }}
</div>
<div class="oem-glass-question-body">
<div class="oem-glass-question-copy" v-html="bodyText"></div>
<div class="oem-glass-question-option">
<checkboxQuestion
class="mt-3 ms-1"
cmsWidgetName="OemGlassInputQuestionWidget"
v-model="installOemGlass" />
</div>
</div>
</div>
</template>
<script>
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
export default {
name: "oem-part-question",
props: {
cmsWidgetName: String,
modelValue: Boolean,
oemGlassPartPrice: Number,
aftermarketGlassPartPrice: Number,
},
data() {
return {
isExpanded: false,
};
},
computed: {
headerText() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
bodyText() {
let priceDifference = this.oemGlassPartPrice - this.aftermarketGlassPartPrice;
let bodyText = this.getCmsContent(this.cmsWidgetName, "BodyText");
bodyText = bodyText.replace(
"{custom:OEMGLASSPRICE}",
"<span id='price-dif'>$" + priceDifference.toFixed(2) + "</span>"
);
return bodyText;
},
footerText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
installOemGlass: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
toggleIsExpanded() {
this.setIsExpanded(!this.isExpanded);
},
setIsExpanded(newVal) {
this.isExpanded = newVal;
},
},
components: {
checkboxQuestion,
},
};
</script>
<style lang="scss">
.oem-glass-question-container {
border-top: 1px solid gray;
@include media-breakpoint-up(lg) {
margin: 0 2rem;
}
.oem-glass-question-header {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
color: $blue;
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
span {
flex-grow: 1;
color: $black;
}
&::after {
content: "";
transition: all 0.5s ease;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 9'%3E%3Cpath d='M7.99282 0.117702C7.75716 0.116049 7.53044 0.207797 7.3623 0.37286L0.255553 7.46878C0.0862976 7.63796 -0.00878906 7.86742 -0.00878906 8.10667C-0.00878906 8.34593 0.0862976 8.57539 0.255553 8.74457C0.424808 8.91374 0.654368 9.00879 0.893731 9.00879C1.13309 9.00879 1.36265 8.91374 1.53191 8.74457L7.99282 2.27378L14.4614 8.74457C14.6307 8.91205 14.8595 9.00547 15.0977 9.00428C15.3359 9.00308 15.5638 8.90737 15.7314 8.73819C15.8989 8.56901 15.9924 8.34022 15.9912 8.10216C15.99 7.8641 15.8942 7.63627 15.725 7.46878L8.6259 0.377963C8.45765 0.21088 8.22999 0.117289 7.99282 0.117702Z' fill='%230070d1'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center center;
color: $blue;
width: 16px;
height: 16px;
display: inline-flex;
position: relative;
margin-left: 0.5rem;
transform: rotate(180deg);
}
}
.oem-glass-question-body {
max-height: 0;
transition: all 350ms ease-in;
overflow: hidden;
visibility: hidden;
.oem-glass-question-option {
margin-top: 0.5em;
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
.oem-glass-question-strikethrough-price {
font-size: smaller;
text-decoration: line-through;
}
.oem-glass-question-free-price {
display: inline-block;
font-weight: bolder;
border-radius: 1em;
background-color: $green-300;
padding: 0 0.5em;
margin-inline-start: 0.5em;
}
}
}
&.expanded {
.oem-glass-question-header {
&::after {
transform: unset;
}
}
.oem-glass-question-body {
visibility: visible;
max-height: none;
padding-top: 0.5em;
margin-top: 0.5em;
.oem-glass-question-copy {
#price-dif {
font-weight: $font-weight-700;
color: $black;
}
}
.oem-glass-question-option {
.small {
font-weight: $font-weight-700;
color: $black;
}
}
}
}
}
</style>

View file

@ -118,6 +118,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) { if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
return false; return false;
} }
if (settingName === mockExperimentSettings.OFFER_OEM) {
return false;
}
return "test"; return "test";
}, },
}, },

View file

@ -33,6 +33,7 @@
@vapsItemsSelected="vapsItemsSelectedAction" @vapsItemsSelected="vapsItemsSelectedAction"
@quotePageDiscountSelected="quotePageDiscountSelectedAction" @quotePageDiscountSelected="quotePageDiscountSelectedAction"
@currentNumberOfPackages="currentNumberOfPackagesAction" @currentNumberOfPackages="currentNumberOfPackagesAction"
@servicePackageSelected="servicePackageSelectedAction"
:servicePackage="servicePackage" :servicePackage="servicePackage"
:activePromos="lineItems.promos" :activePromos="lineItems.promos"
:availableQuotePageDiscounts="quoteDiscountPartsInfo" :availableQuotePageDiscounts="quoteDiscountPartsInfo"
@ -85,6 +86,13 @@
isInsuranceSelected ? isInsuranceContinueButtonText : '' isInsuranceSelected ? isInsuranceContinueButtonText : ''
" /> " />
<oemGlassQuestion
cmsWidgetName="OemGlassQuestionWidget"
v-if="offerOem && !isInsuranceSelected"
v-model="isOemGlassSelected"
:oemGlassPartPrice="oemGlassPartPrice"
:aftermarketGlassPartPrice="aftermarketGlassPartPrice" />
<saveProgressModalQuestion <saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget" modalWidgetName="SaveProgressModalWidget"
modalName="SaveProgressModal" modalName="SaveProgressModal"
@ -128,6 +136,7 @@ import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue"; import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question"; import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question"; import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
import oemGlassQuestion from "./oem-glass-question/oem-glass-question.vue";
// Supporting files // Supporting files
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
@ -217,6 +226,15 @@ export default {
null, null,
"quote" "quote"
); );
const experimentAllowsOfferOem = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.OFFER_OEM,
"true"
);
const oemGlassCheckPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_OEM_AFTERMARKET_PARTS,
null,
"quote"
);
const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder; const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder;
const skipQuoteExperiment = store.getters.applicationUser.experiments.find( const skipQuoteExperiment = store.getters.applicationUser.experiments.find(
@ -290,6 +308,10 @@ export default {
resultKey: "supportingItems", resultKey: "supportingItems",
promise: supportingItemsPromise, promise: supportingItemsPromise,
}, },
{
resultKey: "oemGlassCheck",
promise: oemGlassCheckPromise,
},
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
@ -311,6 +333,18 @@ export default {
lineItems.vaps = lineItems.vaps ?? []; lineItems.vaps = lineItems.vaps ?? [];
const nullSafeGlassParts = lineItems.glassParts ?? []; const nullSafeGlassParts = lineItems.glassParts ?? [];
const oemGlassCheck = resultMap.oemGlassCheck;
const offerOem = experimentAllowsOfferOem ? oemGlassCheck?.offerOem : false;
const oemGlassPieceParts = oemGlassCheck?.glassPieceParts;
const oemGlassParts =
oemGlassPieceParts && oemGlassPieceParts.length > 0
? oemGlassPieceParts[0].parts
: null;
const isOemGlassSelected = store.getters.order.damage?.installOemGlass ?? false;
let alternateGlassPartsForOem;
if (oemGlassParts) alternateGlassPartsForOem = oemGlassParts;
const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts(); const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts();
const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({ const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({
@ -328,6 +362,18 @@ export default {
...quotePageDiscountParts, ...quotePageDiscountParts,
]; ];
let pricedAlternateGlassParts = null;
if (alternateGlassPartsForOem && alternateGlassPartsForOem.length > 0) {
pricedAlternateGlassParts = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: alternateGlassPartsForOem,
},
"quote",
false
);
}
// When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards // When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards
var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber; var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber;
var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber; var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber;
@ -389,6 +435,41 @@ export default {
vm.holdParentAccountNumber = holdParentAccountNumber; vm.holdParentAccountNumber = holdParentAccountNumber;
vm.holdBillToAccountNumber = holdBillToAccountNumber; vm.holdBillToAccountNumber = holdBillToAccountNumber;
vm.skipToInsurance = shouldSkipToInsurance; vm.skipToInsurance = shouldSkipToInsurance;
vm.offerOem = offerOem;
vm.alternateGlassPartsForOem = pricedAlternateGlassParts; // oemParts
vm.isOemGlassSelected = isOemGlassSelected;
const getOEMGlassPartPrice = () => {
let price = 0;
if (vm.alternateGlassPartsForOem) {
let oemGlassPart = vm.isOemGlassSelected
? vm.lineItems.glassParts
: vm.alternateGlassPartsForOem;
if (oemGlassPart) {
price =
oemGlassPart.find(
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
)?.sellingPrice || 0;
}
}
return price;
};
const getAftermarketGlassPartPrice = () => {
let price = 0;
if (vm.alternateGlassPartsForOem) {
let aftermarketGlassPart = vm.isOemGlassSelected
? vm.alternateGlassPartsForOem
: vm.lineItems.glassParts;
if (aftermarketGlassPart) {
price = aftermarketGlassPart.find(
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
)?.sellingPrice;
}
}
return price;
};
vm.oemGlassPartPrice = getOEMGlassPartPrice();
vm.aftermarketGlassPartPrice = getAftermarketGlassPartPrice();
if (revalidatePromoResponse) { if (revalidatePromoResponse) {
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse( const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
@ -556,6 +637,8 @@ export default {
availableLineItems: null, availableLineItems: null,
lineItems: [], lineItems: [],
addableVaps: [], addableVaps: [],
offerOem: null,
alternateGlassPartsForOem: null,
servicePackage: null, servicePackage: null,
holdParentAccountNumber: null, holdParentAccountNumber: null,
holdBillToAccountNumber: null, holdBillToAccountNumber: null,
@ -564,6 +647,8 @@ export default {
isSaveProgressComplete: null, isSaveProgressComplete: null,
packageNumber: null, packageNumber: null,
skipToInsurance: null, skipToInsurance: null,
servicePackageSelected: null,
isOemGlassSelected: false,
}; };
}, },
async mounted() { async mounted() {
@ -670,6 +755,9 @@ export default {
vapsItemsSelectedAction(vapsItemsSelected) { vapsItemsSelectedAction(vapsItemsSelected) {
this.lineItems.vaps = vapsItemsSelected; this.lineItems.vaps = vapsItemsSelected;
}, },
servicePackageSelectedAction(newValue) {
this.servicePackageSelected = newValue;
},
quotePageDiscountSelectedAction(supportingItemsSelected) { quotePageDiscountSelectedAction(supportingItemsSelected) {
this.lineItems.supportingItems = supportingItemsSelected; this.lineItems.supportingItems = supportingItemsSelected;
}, },
@ -725,6 +813,20 @@ export default {
this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems); this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems);
} }
if (
this.lineItems.glassParts &&
this.alternateGlassPartsForOem &&
this.alternateGlassPartsForOem.length > 0
) {
let isOEMGlassOnOrder = this.isOEMGlassOnOrder(this.lineItems.glassParts);
if (
(this.isOemGlassSelected && !isOEMGlassOnOrder) ||
(!this.isOemGlassSelected && isOEMGlassOnOrder)
) {
this.swapOEMGlassPartsOnOrder();
}
}
if (this.lineItems.glassParts?.length > 0) { if (this.lineItems.glassParts?.length > 0) {
this.dispatchStoreAction( this.dispatchStoreAction(
this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING, this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
@ -757,8 +859,20 @@ export default {
}, },
false false
); );
this.dispatchStoreAction(
this.storeActions.SAVE_IS_OEM_GLASS_SELECTED,
this.isOemGlassSelected,
false
);
await savePageData(this.pageName, { saveProgressPopupSkipped: true }, true); await savePageData(
this.pageName,
{
saveProgressPopupSkipped: true,
servicePackageSelected: this.servicePackageSelected,
},
true
);
const payment = this.$store.getters.payment; const payment = this.$store.getters.payment;
if (payment.isInsurance) { if (payment.isInsurance) {
@ -868,6 +982,26 @@ export default {
const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode); const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade); this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
}, },
isOEMGlassOnOrder(glassParts) {
if (glassParts && glassParts.length > 0) {
return glassParts.some((part) => part.partNumber.endsWith("OEM"));
}
return false;
},
swapOEMGlassPartsOnOrder() {
this.lineItems.glassParts.forEach((part, index) => {
if (part.partType === partTypeStrings.WINDSHIELD) {
let altPartToUse = this.alternateGlassPartsForOem.find(
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
);
if (altPartToUse) {
let childParts = this.lineItems.glassParts[index].childParts;
altPartToUse.childParts = childParts;
this.lineItems.glassParts[index] = altPartToUse;
}
}
});
},
}, },
components: { components: {
funnelHeader, funnelHeader,
@ -884,6 +1018,7 @@ export default {
recalDisclaimer, recalDisclaimer,
saveProgressModalQuestion, saveProgressModalQuestion,
saveProgressPopupQuestion, saveProgressPopupQuestion,
oemGlassQuestion,
}, },
}; };
</script> </script>

View file

@ -34,6 +34,7 @@ import {
findLineItemsWithPartType, findLineItemsWithPartType,
getPackageNameByType, getPackageNameByType,
getDiscountedPackageName, getDiscountedPackageName,
getPackageLabel,
} from "@/helpers/service-package-helper"; } from "@/helpers/service-package-helper";
import { import {
getPromosThatMatchLineItemsOnOrder, getPromosThatMatchLineItemsOnOrder,
@ -45,6 +46,7 @@ import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-h
import experimentMixin from "@/mixins/experiment-mixin.js"; import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { savePageData } from "@/router/methods/helpers/save-page-data"; import { savePageData } from "@/router/methods/helpers/save-page-data";
import { debugLog } from "@/helpers/debug-log-helper";
export default { export default {
name: "servicePackageQuestion", name: "servicePackageQuestion",
@ -84,6 +86,12 @@ export default {
if (this.activePromos?.length) this.selectDefaultPackage(); if (this.activePromos?.length) this.selectDefaultPackage();
}, },
selectedPackageName(newValue) { selectedPackageName(newValue) {
debugLog("selectedPackageName:", newValue);
const packageLabel = getPackageLabel(newValue);
debugLog("getPackageLabel:", packageLabel);
this.$emit("servicePackageSelected", `ServicePackage-${packageLabel}`);
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue); const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage); this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue); const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue);

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

@ -50,7 +50,7 @@ export default {
data() { data() {
return { return {
isModalOpen: false, isModalOpen: false,
selectedAppontment: { selectedAppointment: {
estimatedServiceMinutes: { estimatedServiceMinutes: {
minimum: null, minimum: null,
maximum: null, maximum: null,
@ -58,6 +58,7 @@ export default {
timeSlot: null, timeSlot: null,
date: null, date: null,
}, },
confirmedAppointment: false,
}; };
}, },
methods: { methods: {
@ -67,22 +68,25 @@ export default {
this.modal.openModal(); this.modal.openModal();
}, },
onModalClosed() { onModalClosed() {
this.pushEvent();
this.$emit("close-mobile-first-modal"); this.$emit("close-mobile-first-modal");
this.confirmedAppointment = false;
}, },
closeModal() { closeModal() {
this.modal.closeModal(); this.modal.closeModal();
}, },
confirmAppointment() { confirmAppointment() {
let selectedTimeSlot = { let selectedTimeSlot = {
timeSlot: this.selectedAppontment.timeSlot, timeSlot: this.selectedAppointment.timeSlot,
routeCode: this.selectedAppontment.timeSlot.id, routeCode: this.selectedAppointment.timeSlot.id,
date: this.selectedAppontment.date, date: this.selectedAppointment.date,
}; };
this.confirmedAppointment = true;
this.$emit("confirm-appointment", selectedTimeSlot); this.$emit("confirm-appointment", selectedTimeSlot);
this.modal.closeModal(); this.modal.closeModal();
}, },
setSelectedAppointment(appointment) { setSelectedAppointment(appointment) {
this.selectedAppontment = appointment; this.selectedAppointment = appointment;
}, },
getIsModalOpen() { getIsModalOpen() {
return this.isModalOpen; return this.isModalOpen;
@ -90,6 +94,21 @@ export default {
setIsModalOpen(isOpen) { setIsModalOpen(isOpen) {
this.isModalOpen = isOpen; this.isModalOpen = isOpen;
}, },
pushEvent() {
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
if (this.selectedAppointment && this.selectedAppointment.timeSlot) {
this.pushEventToGA(
this.GaCategories.CONFIRMATION_CLICKED,
gaAction,
this.selectedAppointment.timeSlot.startTime +
"-" +
this.selectedAppointment.timeSlot.endTime +
" " +
this.selectedAppointment.date,
true
);
}
},
}, },
computed: { computed: {
modalName() { modalName() {
@ -112,37 +131,37 @@ export default {
if (token.includes(INLINE_SERVICETYPE_TOKEN)) { if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
const serviceType = "<strong>" + this.getServiceType + "</strong>"; const serviceType = "<strong>" + this.getServiceType + "</strong>";
tokens.splice(tokens.indexOf(token), 1, serviceType); tokens.splice(tokens.indexOf(token), 1, serviceType);
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppontment.date) { } else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppointment.date) {
const day = const day =
"<strong>" + "<strong>" +
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", { new Date(this.selectedAppointment.date).toLocaleDateString("en-US", {
weekday: "long", weekday: "long",
timeZone: "UTC", timeZone: "UTC",
}) + }) +
"</strong>"; "</strong>";
tokens.splice(tokens.indexOf(token), 1, day); tokens.splice(tokens.indexOf(token), 1, day);
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) { } else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppointment.date) {
const date = new Date(this.selectedAppontment.date).toLocaleDateString( const date = new Date(this.selectedAppointment.date).toLocaleDateString(
"en-US", "en-US",
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" } { timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
); );
tokens.splice(tokens.indexOf(token), 1, date); tokens.splice(tokens.indexOf(token), 1, date);
} else if ( } else if (
token.includes(INLINE_TIMESLOT_TOKEN) && token.includes(INLINE_TIMESLOT_TOKEN) &&
this.selectedAppontment.timeSlot this.selectedAppointment.timeSlot
) { ) {
const timeslot = const timeslot =
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.startTime) + this.get12HourTimeFormat(this.selectedAppointment.timeSlot.startTime) +
" - " + " - " +
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.endTime); this.get12HourTimeFormat(this.selectedAppointment.timeSlot.endTime);
tokens.splice(tokens.indexOf(token), 1, timeslot); tokens.splice(tokens.indexOf(token), 1, timeslot);
} else if ( } else if (
token.includes(INLINE_SERVICE_LENGTH_TOKEN) && token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
this.selectedAppontment.estimatedServiceMinutes this.selectedAppointment.estimatedServiceMinutes
) { ) {
const inshopDurationTime = getDisplayTextForDurationLength( const inshopDurationTime = getDisplayTextForDurationLength(
this.selectedAppontment.estimatedServiceMinutes.minimum, this.selectedAppointment.estimatedServiceMinutes.minimum,
this.selectedAppontment.estimatedServiceMinutes.maximum this.selectedAppointment.estimatedServiceMinutes.maximum
); );
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime); tokens.splice(tokens.indexOf(token), 1, inshopDurationTime);
} }

View file

@ -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.
<template>
<div id="recal-ack-modal-container">
<modal
:ref="modalName"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:isFooterButtonPrimary="true"
@footer-button-event="ackRecalDisclaimer">
<p class="modal-body-inner" v-html="modalBodyText"></p>
<div>
<div class="learn-more">
<a href="javascript:;" @click="learnMore = !learnMore">{{ learnMoreLabel }}</a>
<div v-if="learnMore">{{ learnMoreText }}</div>
</div>
<buttonQuestion
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonTypeString="listButtonHorizontal"
v-model="selectedValues"
isRequired />
</div>
<template v-slot:modal-header-slot>
<span>{{ modalHeaderText }}</span>
</template>
</modal>
</div>
</template>
<script>
import modal from "@/digital-components/modal/modal";
import buttonQuestion from "@/digital-components/button-question/button-question";
import { RECAL_ACK_YES } from "@/constants/schedule-constants";
export default {
name: "recal-ack-modal",
props: {
agreementWidgetName: String,
modalWidgetName: String,
groupName: String,
},
data() {
return {
learnMore: false,
isModalOpen: true,
isRecalAcknowledged: "",
};
},
methods: {
openModal() {
this.modal.openModal();
},
onModalClosed() {
this.$emit("recal-ack-modal");
},
closeModal() {
this.modal.closeModal();
},
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
ackRecalDisclaimer() {
this.modal.closeModal();
if (this.isRecalAcknowledged === RECAL_ACK_YES) {
this.pushEventToGA(
this.GaCategories.RECAL_DISCLAIMER,
this.GaActions.ACKNOWLEDGED,
this.isRecalAcknowledged,
true
);
this.$emit("recalAcknowledged", this.isRecalAcknowledged);
}
},
},
computed: {
questionText() {
return this.getCmsContent(this.agreementWidgetName, "QuestionText");
},
answersFromCms() {
return this.getCmsContent(this.agreementWidgetName, "Answers");
},
learnMoreLabel() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
learnMoreText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
return this.getCmsContent(this.modalWidgetName, "BodyText");
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
selectedValues: {
get: function () {
return this.isRecalAcknowledged;
},
set: function (newValue) {
this.isRecalAcknowledged = this.answersFromCms.find(
(a) => a.Name === newValue
)?.Text;
},
},
},
components: { modal, buttonQuestion },
};
</script>
<style lang="scss">
#recal-ack-modal-container {
.modal-component {
@include media-breakpoint-up(md) {
.modal-dialog {
left: 0;
align-content: center;
flex-wrap: wrap;
width: 34.875rem;
transform: translate(0, 0);
.modal-content {
border-radius: $border-radius-lg;
}
}
}
.modal-dialog {
.modal-content {
.btn-close {
display: none;
}
.modal-header {
width: 75%;
padding: 0 1.25rem 0 1.25rem;
flex-direction: column;
& > span {
color: $black;
font-size: 1.25rem;
font-weight: normal;
}
.modal-title {
font-size: $font-size-20;
font-weight: $font-weight-normal;
}
}
.modal-body {
padding: 0.875rem 1.25rem 1.875rem 1.25rem;
.modal-body-inner {
ul {
list-style: none;
padding: 1rem;
background-color: #f4f4f4;
font-size: $font-size-14;
}
p {
font-size: 0.875rem;
}
}
.button-inner-wrapper > .col:first-child .list-button-horizontal {
border-bottom-left-radius: 50rem;
border-top-left-radius: 50rem;
}
.col:first-of-type .list-button-horizontal .list-button-horizontal-content,
.list-group:first-of-type
.list-button-horizontal
.list-button-horizontal-content {
border-bottom-left-radius: 50rem;
border-top-left-radius: 50rem;
}
.button-inner-wrapper > .col:last-child .list-button-horizontal {
border-bottom-right-radius: 50rem;
border-top-right-radius: 50rem;
}
.col:last-of-type .list-button-horizontal .list-button-horizontal-content,
.list-group:first-of-type
.list-button-horizontal
.list-button-horizontal-content {
border-bottom-right-radius: 50rem;
border-top-right-radius: 50rem;
}
.learn-more {
padding-bottom: 0;
font-size: 0.875rem;
a {
font-weight: 600;
}
}
.question-text {
margin-top: 0.875rem;
}
}
.modal-footer {
flex-flow: wrap-reverse;
padding-top: 0;
padding-bottom: 2rem;
}
.modal-footer .modal-footer-button:hover {
background-color: $red-1000;
border-color: $red-1100;
}
}
}
}
}
</style>

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

@ -6,6 +6,12 @@
modalWidgetName="MobileFirstModalWidget" modalWidgetName="MobileFirstModalWidget"
ref="mobileFirstModal" ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" /> @confirm-appointment="updateTimeSlotandNavigateForward" />
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
groupName="recalAckGroup"
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
ref="recalAckModal" />
<div <div
class="container page-container-grouped-styles" class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']"> :class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
@ -22,6 +28,7 @@
cmsWidgetName="ScheduleYourServiceWidget" cmsWidgetName="ScheduleYourServiceWidget"
id="schedule-your-service" /> id="schedule-your-service" />
<appointmentTypeQuestion <appointmentTypeQuestion
v-if="hasSelectableDatesLoaded"
v-model="appointmentTypeFromAppointmentTypeQuestion" v-model="appointmentTypeFromAppointmentTypeQuestion"
v-show="isAppointmentTypeDisplayed" v-show="isAppointmentTypeDisplayed"
:isServiceableMobile="isServiceableMobile" :isServiceableMobile="isServiceableMobile"
@ -91,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"
@ -133,8 +136,6 @@
:isOvernightDropoff="isOvernightDropoff" /> :isOvernightDropoff="isOvernightDropoff" />
</div> </div>
<datePicker <datePicker
:currentZip="zipCode"
:currentProviderNumber="selectedProvider?.providerNumber"
v-show="appointmentType" v-show="appointmentType"
customComponentId="dateQuestion" customComponentId="dateQuestion"
selectableDatesSetting="custom" selectableDatesSetting="custom"
@ -144,11 +145,12 @@
class="text-link-small" class="text-link-small"
:getMoreDatesCallback="getMoreScheduleData" :getMoreDatesCallback="getMoreScheduleData"
validationRules="date-required" validationRules="date-required"
@date-clicked="handleDateClicked" @date-selected="handleDateSelected"
: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"
@ -167,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"
@ -190,6 +193,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue"; 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 durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.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 shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup"; import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
@ -216,6 +220,7 @@ import {
RouteCodeFlags, RouteCodeFlags,
PREMIUM_FEE_PART_TYPE, PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE, PRICING_BY_DAY_PART_TYPE,
RECAL_ACK_YES,
} from "@/constants/schedule-constants"; } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -242,6 +247,7 @@ import {
isDropOffRouteCode, isDropOffRouteCode,
getTodayDate, getTodayDate,
} from "@/layouts/schedule/helpers/schedule-helper"; } 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 { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
@ -287,6 +293,7 @@ const getScheduleApiResponse = async ({
startDateString, startDateString,
endDateString, endDateString,
providerNumber, providerNumber,
inshopProviderNumber,
zipCode, zipCode,
includeMobileTimeSlots = true, includeMobileTimeSlots = true,
includeInshopTimeSlots = true, includeInshopTimeSlots = true,
@ -329,7 +336,7 @@ const getScheduleApiResponse = async ({
startDate: apiStartDate, startDate: apiStartDate,
endDate: apiEndDate, endDate: apiEndDate,
shopAppointmentType: "InshopOrDropoff", shopAppointmentType: "InshopOrDropoff",
providerNumber: providerNumber, providerNumber: inshopProviderNumber,
}, },
}; };
if (apiStartDate < apiEndDate) { if (apiStartDate < apiEndDate) {
@ -429,19 +436,19 @@ export default {
name: "schedule", name: "schedule",
data() { data() {
return { return {
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
selectedDate: this.getSelectedDateFromStore(), selectedDate: this.getSelectedDateFromStore(),
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(),
@ -474,6 +481,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) {
@ -584,6 +592,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;
@ -604,6 +614,9 @@ export default {
); );
vm.initializeDatePicker().then(() => { vm.initializeDatePicker().then(() => {
vm.showMobileFirstModal(); vm.showMobileFirstModal();
vm.hideLoadingModal();
vm.calendarLoadingStatus = "none";
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
}); });
}); });
}, },
@ -620,8 +633,25 @@ 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 this.appointmentType == AppointmentTypeStrings.MOBILE; return (
this.appointmentType == AppointmentTypeStrings.MOBILE ||
(this.isServiceableMobile && !this.isServiceableInshop)
);
}, },
isServiceableMobile() { isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) { if (this.isRecalibrationServiceableMobile !== null) {
@ -633,15 +663,18 @@ export default {
return this.isGlassServiceableMobile; return this.isGlassServiceableMobile;
} }
}, },
closestInshopProviderNumber() {
return this.shopProviderData?.shopProviders[0]?.providerNumber;
},
showTimeSlotQuestion() { showTimeSlotQuestion() {
if ( if (
this.selectedDate && this.selectedDate &&
this.selectedDate.includes("mobile") && this.appointmentTypeFromAppointmentTypeQuestion &&
!this.isMobileSelected this.timeSlotsForSelectedDate
) { ) {
this.setSelectedDateToFirstAvailable(); return true;
} }
return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion; return false;
}, },
isMobileStaticRecalibrationApplicable() { isMobileStaticRecalibrationApplicable() {
return ( return (
@ -869,6 +902,60 @@ 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() {
if (this.isMobileSelected) {
return this.selectableDatesMobile?.days?.length > 0;
} else {
return this.selectableDatesInshop?.days?.length > 0;
}
},
}, },
methods: { methods: {
splitCopyOnCMSPlaceHolder, splitCopyOnCMSPlaceHolder,
@ -1130,37 +1217,17 @@ export default {
); );
} }
}, },
getTimeSlotInfo() {
// Get the current date
const currentDate = getTodayDate();
// Add 10 days to the current date
currentDate.setDate(currentDate.getDate() + 10);
// Format the date as yyyy-mm-dd
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const day = String(currentDate.getDate()).padStart(2, "0");
const formattedDate = `${year}-${month}-${day}`;
return {
date: formattedDate,
endTime: "12:00",
jobMaxMinutes: "180",
jobMinMinutes: "120",
routeCode: "3357I-03357-M-I*20988*AM",
startTime: "08:00",
};
},
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) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber; return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
}); });
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider?.providerNumber;
// No zipCodeData comes back if zip was not changed // No zipCodeData comes back if zip was not changed
if (shopQuestionPopUpData.zipCodeData) { if (shopQuestionPopUpData.zipCodeData) {
@ -1169,24 +1236,32 @@ export default {
selectedProvider selectedProvider
); );
} else { } else {
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider.providerNumber;
if (didProviderNumberChange) { if (didProviderNumberChange) {
this.updateSelectedProvider(selectedProvider); this.updateSelectedProvider(selectedProvider);
this.initializeDatePicker();
} }
} }
this.initializeDatePicker().then(() => {
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,
providerNumber: this.selectedProvider?.providerNumber, providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode, zipCode: this.zipCode,
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(
@ -1202,8 +1277,6 @@ export default {
async initializeDatePicker() { async initializeDatePicker() {
this.isShowMobileFirstAppt && this.showLoadingModal(); this.isShowMobileFirstAppt && this.showLoadingModal();
this.selectedDate = null;
const includeMobileTimeSlots = this.isServiceableMobile; const includeMobileTimeSlots = this.isServiceableMobile;
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff; const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
const datePickerInitialData = await this.$refs.datePicker.loadInitialData({ const datePickerInitialData = await this.$refs.datePicker.loadInitialData({
@ -1213,6 +1286,9 @@ export default {
getSelectableDatesCallback: getScheduleApiResponse, getSelectableDatesCallback: getScheduleApiResponse,
preSelectedDate: this.preSelectedDate, preSelectedDate: this.preSelectedDate,
providerNumber: this.selectedProvider?.providerNumber, providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode, zipCode: this.zipCode,
includeMobileTimeSlots: includeMobileTimeSlots, includeMobileTimeSlots: includeMobileTimeSlots,
includeInshopTimeSlots: includeInshopTimeSlots, includeInshopTimeSlots: includeInshopTimeSlots,
@ -1224,7 +1300,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;
@ -1236,11 +1311,11 @@ 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() {
return store.getters.order.isRecalAcknowledgedForScheduling;
}, },
getScheduleApiResponse, getScheduleApiResponse,
getServiceZipCtuCodeFromStore() { getServiceZipCtuCodeFromStore() {
@ -1251,8 +1326,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() {
@ -1360,7 +1435,25 @@ export default {
hideLoadingModal() { hideLoadingModal() {
this.$refs.loadingModal.hideModal(); this.$refs.loadingModal.hideModal();
}, },
async closeRecalAcknowledgementPopup() {
this.showRecalAcknowledgementPopup = false;
},
async forwardButtonAction() { 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; this.navigatingForward = true;
var ctu = this.zipCodeCtu; var ctu = this.zipCodeCtu;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) { if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
@ -1489,39 +1582,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();
@ -1591,28 +1651,39 @@ export default {
handleWaitListRequested(value) { handleWaitListRequested(value) {
this.waitListRequested = value; this.waitListRequested = value;
}, },
handleDateClicked(date) { handleDateSelected(date) {
// do something to mark this as upcharge day or not... const previousDate = this.selectedDate;
if (date.isPricingByDayUpchargeDay) { // check if date actually changed
this.includePricingByDayUpcharge = true; if (previousDate !== date.dateString) {
} else { this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
this.includePricingByDayUpcharge = false; this.updateFooterButtonText(this.selectedTimeSlotInfo);
} }
// TODO: REMOVE AS PART OF CASH-1634
// // do something to mark this as upcharge day or not...
// if (date.isPricingByDayUpchargeDay) {
// this.includePricingByDayUpcharge = true;
// } else {
// this.includePricingByDayUpcharge = false;
// }
},
getEmptyTimeSlot() {
return {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
}, },
updateTimeSlot(timeSlotObj) { updateTimeSlot(timeSlotObj) {
if (!timeSlotObj?.routeCode) { if (!timeSlotObj?.routeCode) {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
this.selectedTimeSlotInfo = { this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
return; return;
} }
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find( const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
@ -1662,6 +1733,8 @@ export default {
this.updateSelectedProvider(); this.updateSelectedProvider();
}, },
updateSelectedProvider(newShopProvider) { updateSelectedProvider(newShopProvider) {
if (!this.appointmentTypeStrings) return;
if (newShopProvider) { if (newShopProvider) {
this.selectedProvider = newShopProvider; this.selectedProvider = newShopProvider;
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) { } else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
@ -1682,8 +1755,29 @@ export default {
this.selectedProvider = this.shopProviderData.shopProviders[0]; 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(); 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;
@ -1694,6 +1788,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();
}); });
}); });
}, },
@ -1705,10 +1802,12 @@ export default {
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase; this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
this.selectedDate = null; this.selectedDate = null;
this.selectedProvider = newProvider; this.selectedProvider = newProvider;
this.initializeDatePicker();
}, },
handleAppointmentTypeChange(newAppointmentType) { handleAppointmentTypeChange(newAppointmentType) {
this.updateFooterButtonText(); this.updateFooterButtonText();
// if time appointment type changes, clear any selected time slot
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
if (newAppointmentType === AppointmentTypeStrings.MOBILE) { if (newAppointmentType === AppointmentTypeStrings.MOBILE) {
// Remember last shop selected if previous selection was inshop/dropoff // Remember last shop selected if previous selection was inshop/dropoff
if ( if (
@ -1717,17 +1816,6 @@ export default {
AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) && AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) &&
this.selectedProvider this.selectedProvider
) { ) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
this.lastSelectedInshopOrDropoffProvider = this.selectedProvider; this.lastSelectedInshopOrDropoffProvider = this.selectedProvider;
} }
this.appointmentType = AppointmentTypeStrings.MOBILE; this.appointmentType = AppointmentTypeStrings.MOBILE;
@ -1746,16 +1834,45 @@ export default {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
} }
// make sure a selectedProvider exists // make sure a selectedProvider exists
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider); if (this.lastSelectedInshopOrDropoffProvider) {
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
} else {
this.updateSelectedProvider();
}
} else { } else {
this.appointmentType = null; this.appointmentType = null;
} }
this.setSelectedDateToFirstAvailable(); this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
this.setDisplayWaitList();
}, },
setSelectedDateToFirstAvailable() { setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate(); 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() { async showMobileFirstModal() {
if (!this.selectedRouteCodeData?.routeCode) { if (!this.selectedRouteCodeData?.routeCode) {
const pmMobileDays = experimentMixin.methods.getSettingValue( const pmMobileDays = experimentMixin.methods.getSettingValue(
@ -1767,6 +1884,20 @@ export default {
const noMobileAvailableDays = experimentMixin.methods.getSettingValue( const noMobileAvailableDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_NO_MOBILE_AVAILABLE_DAYS 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; let preSelectedMobileAppointment = null;
const todaysDate = getTodayDate(); const todaysDate = getTodayDate();
@ -1783,12 +1914,63 @@ export default {
(new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24); (new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24);
const shouldExposeMobileFirstAppointment = () => { const shouldExposeMobileFirstAppointment = () => {
return ( const mobileFirstDaysCheck =
(firstMobileAMDate && (firstMobileAMDate &&
numberOfDaysToFirstMobileAMDate <= noMobileAvailableDays) || numberOfDaysToFirstMobileAMDate <= noMobileAvailableDays) ||
(firstMobilePMDate && (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()) { if (shouldExposeMobileFirstAppointment()) {
@ -1813,23 +1995,34 @@ export default {
} }
if (this.isShowMobileFirstAppt) { if (this.isShowMobileFirstAppt) {
if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) { if (pmMobileDays == 0) {
// Selects the first available time slot
preSelectedMobileAppointment =
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
? firstMobileAMAppt
: firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate <= pmMobileDays
) {
// Selects the first available PM time slot
preSelectedMobileAppointment = firstMobilePMAppt; preSelectedMobileAppointment = firstMobilePMAppt;
} else if ( } else if (
firstMobilePMDate && firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) { ) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
preSelectedMobileAppointment = firstMobileAMAppt preSelectedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt ? firstMobileAMAppt
: null; : firstMobilePMAppt;
!preSelectedMobileAppointment &&
(preSelectedMobileAppointment = firstMobilePMAppt);
} }
this.$refs.mobileFirstModal.setSelectedAppointment( if (preSelectedMobileAppointment) {
preSelectedMobileAppointment this.$refs.mobileFirstModal.setSelectedAppointment(
); preSelectedMobileAppointment
this.$refs.mobileFirstModal.openModal(); );
this.$refs.mobileFirstModal.openModal();
}
} }
} }
} }
@ -1868,39 +2061,17 @@ export default {
this.updateTimeSlot(timeSlotObj); this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction(); this.forwardButtonAction();
}, },
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
},
}, },
watch: { watch: {
appointmentTypeFromAppointmentTypeQuestion: { appointmentTypeFromAppointmentTypeQuestion: {
handler(newValue, oldValue) { 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: { components: {
funnelHeader, funnelHeader,
@ -1913,7 +2084,7 @@ export default {
textBlock, textBlock,
timeSlotQuestion, timeSlotQuestion,
mobileFirstModal, mobileFirstModal,
recalAckModal,
alert, alert,
serviceZipModalQuestion, serviceZipModalQuestion,
appointmentTypeQuestion, appointmentTypeQuestion,

View file

@ -548,9 +548,6 @@ export default {
} }
}, },
}, },
unmounted() {
if (this.isModalOpened) this.closeModal();
},
watch: { watch: {
waitListRequested(newVal) { waitListRequested(newVal) {
if (newVal) { if (newVal) {

View file

@ -1,6 +1,7 @@
<template> <template>
<div class="time-slots-question"> <div class="time-slots-question">
<buttonQuestion <buttonQuestion
ref="chooseDropOffOrInshop"
v-if="isDropOffAppointmentAvailable" v-if="isDropOffAppointmentAvailable"
v-model="selectedAnswerForDropOffOrInshop" v-model="selectedAnswerForDropOffOrInshop"
@update:modelValue="dropOffSelectionChanged" @update:modelValue="dropOffSelectionChanged"
@ -23,8 +24,9 @@
</div> </div>
</buttonQuestion> </buttonQuestion>
<buttonQuestion <buttonQuestion
ref="buttonQuestion" ref="chooseTimeSlot"
v-if="shouldDisplayTimeSlotQuestion" v-if="displayTimeSlotQuestion"
v-model="selectedAnswerForTimeSlots"
@update:modelValue="timeSlotSelectionChanged" @update:modelValue="timeSlotSelectionChanged"
buttonTypeString="timeSlotModalListButton" buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeSlotModalListButton" :buttonTypeObject="timeSlotModalListButton"
@ -34,10 +36,12 @@
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"
v-model="selectedAnswerForTimeSlots"
questionText="Available times:" questionText="Available times:"
isRequired isRequired
validationRules="time-slot-required" /> validationRules="time-slot-required" />
@ -133,6 +137,7 @@ export default {
isSameDay: Boolean, isSameDay: Boolean,
displayWaitList: Boolean, displayWaitList: Boolean,
selectedRouteCodeData: Object, selectedRouteCodeData: Object,
lazyLoadAmountToLoad: Number,
}, },
data() { data() {
return { return {
@ -389,12 +394,32 @@ export default {
this.answersForDropOffQuestion[0].value !== PICK_A_TIME_BUTTON_VALUE this.answersForDropOffQuestion[0].value !== PICK_A_TIME_BUTTON_VALUE
); );
}, },
shouldDisplayTimeSlotQuestion() { displayTimeSlotQuestion() {
return ( return (
this.selectedAnswerForDropOffOrInshop == PICK_A_TIME_BUTTON_VALUE || this.selectedAnswerForDropOffOrInshop == PICK_A_TIME_BUTTON_VALUE ||
!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) {
@ -452,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

@ -80,6 +80,7 @@ export default {
}, },
set: function (newValue) { set: function (newValue) {
this.$emit("update:modelValue", newValue); this.$emit("update:modelValue", newValue);
this.$emit("handle-appointment-type-change", newValue);
}, },
}, },
isMobileOnly() { isMobileOnly() {

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

@ -198,6 +198,8 @@ export default {
false false
); );
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// Navigate to the next page // Navigate to the next page
this.navigateForward(matchedParts); this.navigateForward(matchedParts);
}, },

View file

@ -95,9 +95,8 @@ describe("vin-lookup.vue", () => {
it("Should call navigateForward() if the store carId does not match the vin response carId but does match previously enterted carId and forward button is clicked", async () => { it("Should call navigateForward() if the store carId does not match the vin response carId but does match previously enterted carId and forward button is clicked", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
mockOutPromises({ carId: "C11111" }); mockOutPromises({ carId: "C11111" }); //this line in above tests affects this test, so adding to here too till we figure out how to isolate the calls
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.previouslyEnteredCarId = "C11111"; wrapper.vm.previouslyEnteredCarId = "C11111";
@ -111,6 +110,7 @@ describe("vin-lookup.vue", () => {
it("Should not call navigateForward() if zip service returns a non-serviceable flag", async () => { it("Should not call navigateForward() if zip service returns a non-serviceable flag", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
mockOutPromises({ carId: "C11111" }); //this line in above tests affects this test, so adding to here too till we figure out how to isolate the calls
const zipValidationApiResponse = { const zipValidationApiResponse = {
data: { data: {
isServiceable: false, isServiceable: false,
@ -135,6 +135,8 @@ describe("vin-lookup.vue", () => {
it("Should not call navigateForward() when forward button is clicked but lookupVehicle errors out.", async () => { it("Should not call navigateForward() when forward button is clicked but lookupVehicle errors out.", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
mockOutPromises({ carId: "C11111" }); //this line in above tests affects this test, so adding to here too till we figure out how to isolate the calls
wrapper.vm.vinTouched = true; wrapper.vm.vinTouched = true;
wrapper.vm.vin = "foo"; wrapper.vm.vin = "foo";
wrapper.vm.initialVin = "!foo"; wrapper.vm.initialVin = "!foo";
@ -164,6 +166,7 @@ describe("vin-lookup.vue", () => {
isCarIdDifferent: true, isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false, isSelectedGlassAvailableForVehicle: false,
}); });
wrapper.vm.pageName = "vin-lookup";
// Act // Act
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
@ -172,9 +175,7 @@ describe("vin-lookup.vue", () => {
expect(wrapper.vm.$router.navigateWithSaving).toBeCalledTimes(1); expect(wrapper.vm.$router.navigateWithSaving).toBeCalledTimes(1);
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith( expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS, navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
wrapper.vm.$route, "vin-lookup"
expect.anything(),
expect.anything()
); );
}); });
@ -251,12 +252,14 @@ describe("vin-lookup.vue", () => {
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
// Act // Act
const vinLookup = wrapper.findComponent('[data-test="vin-lookup-component"]'); const vinLookup = wrapper.findComponent({ ref: "vinLookupQuestion" });
vinLookup.trigger("imageLookupError"); vinLookup.trigger("image-lookup-error");
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
// Assert // Assert
expect(wrapper.findAllComponents({ name: "alert" }).length).toBe(1); expect(wrapper.vm.displayVinScanFailedAlert).toBe(true);
//TODO - Fix original check for alert box but should show if displayVinScanFailedAlert is true which I'm checking
//expect(wrapper.findAllComponents({ cmsWidgetName: "AlertVinScanFailed" }).length).toBe(1);
}); });
test("should hide the AlertNoService when displayNoServiceAlert is false", async () => { test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
@ -277,6 +280,7 @@ describe("vin-lookup.vue", () => {
}); });
}); });
/*
describe("getVinFromImage", () => { describe("getVinFromImage", () => {
test("GetVinFromImage resolves with first valid VIN when any vins are returned.", async () => { test("GetVinFromImage resolves with first valid VIN when any vins are returned.", async () => {
// Arrange // Arrange
@ -289,7 +293,7 @@ describe("vin-lookup.vue", () => {
const storeMixin = { const storeMixin = {
methods: { methods: {
dispatchStoreAction: lookup, dispatchStoreActionWithLogging: lookup,
}, },
}; };
@ -354,6 +358,7 @@ describe("vin-lookup.vue", () => {
await expect(promise).rejects.toEqual("An error occurred during the lookup."); await expect(promise).rejects.toEqual("An error occurred during the lookup.");
}); });
}); });
*/
}); });
function setupMocks({ customMountOptions }) { function setupMocks({ customMountOptions }) {

View file

@ -393,21 +393,18 @@ export default {
// Check if Service Zip entered is serviceable then save the ZIP info // Check if Service Zip entered is serviceable then save the ZIP info
if (zipCodeData.isServiceable) { if (zipCodeData.isServiceable) {
//Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu //Always save the service zip info even if it was not changed;
if ( // if it didn't change it doesn't alter other values and this makes it more consistent
this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode || await this.dispatchStoreAction(
!this.$store.getters.order.serviceLocation.zipCodeCtu storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
) { {
await this.dispatchStoreAction( state: zipCodeData.state,
storeActions.SAVE_SERVICE_ZIP_CODE_INFO, zipCode: this.serviceZipCode,
{ zipCodeCtu: zipCodeData.zipCodeCtu,
state: zipCodeData.state, },
zipCode: this.serviceZipCode, false
zipCodeCtu: zipCodeData.zipCodeCtu, );
},
false
);
}
// if no value due to field being optional, blank both phone and email address // if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) { if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false); await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);

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";
@ -232,10 +231,41 @@ export default {
(part) => `${part.glassLocation}-${part.glassName}` (part) => `${part.glassLocation}-${part.glassName}`
); );
const format = (part) => `${part?.partNumber}-${part?.partType ? part.partType : ""}`;
const mapParts = (arr) => (arr ?? []).map(format);
const glassParts = mapParts(order?.lineItems?.glassParts);
const supportingItems = mapParts(order?.lineItems?.supportingItems);
const vaps = mapParts(order?.lineItems?.vaps);
const childParts = [].concat(
...(order?.lineItems?.glassParts ?? []).map((gp) => mapParts(gp?.childParts))
);
const allParts = [...glassParts, ...supportingItems, ...vaps, ...childParts];
if (applicationUser?.pageData?.quote?.servicePackageSelected) {
allParts.push(applicationUser.pageData.quote.servicePackageSelected);
}
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;
@ -251,7 +281,7 @@ export default {
? "Insurance" ? "Insurance"
: "Cash"; : "Cash";
sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace"; sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace";
sessionData.productType = glassProducts; sessionData.productType = allParts;
sessionData.eon = order?.eon; sessionData.eon = order?.eon;
sessionData.referralNumber = order?.referralNumber; sessionData.referralNumber = order?.referralNumber;
sessionData.referralSequenceNumber = order?.referralSequenceNumber; sessionData.referralSequenceNumber = order?.referralSequenceNumber;
@ -937,9 +967,6 @@ export default {
ValueToLogTypes() { ValueToLogTypes() {
return ValueToLogTypes; return ValueToLogTypes;
}, },
Variables() {
return Variables;
},
}, },
}; };

View file

@ -57,6 +57,7 @@ const navigationScenarios = {
// Schedule // Schedule
CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION", CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION",
ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK",
// Payment // Payment
CLICKED_INSURANCE: "CLICKED_INSURANCE", CLICKED_INSURANCE: "CLICKED_INSURANCE",

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: [
@ -486,6 +490,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE, scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
destinationPageData: routeData.MOBILE_DETAILS, destinationPageData: routeData.MOBILE_DETAILS,
}, },
{
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
destinationPageData: routeData.SERVICE_ZIP,
},
], ],
}, },
{ {
@ -565,6 +573,10 @@ const routingTable = function () {
piaError: "true", piaError: "true",
}, },
}, },
{
scenario: navigationScenarios.CLICKED_PAY_LATER,
destinationPageData: routeData.CONFIRMATION,
},
], ],
}, },
{ {
@ -578,6 +590,17 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD, scenario: navigationScenarios.CLICKED_FORWARD,
destinationPageData: routeData.CONFIRMATION, destinationPageData: routeData.CONFIRMATION,
}, },
{
scenario: navigationScenarios.PIA_ERROR,
destinationPageData: routeData.PAYMENT_METHOD,
params: {
piaError: "true",
},
},
{
scenario: navigationScenarios.CLICKED_PAY_LATER,
destinationPageData: routeData.CONFIRMATION,
},
], ],
}, },
{ {

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

@ -15,7 +15,30 @@ export async function adyenReturnBeforeEnter(to, from) {
const redirectResult = to?.query?.redirectResult; const redirectResult = to?.query?.redirectResult;
if (sessionId && redirectResult) { if (sessionId && redirectResult) {
const result = await finalizeAdyenPayment(sessionId, redirectResult); let result = null;
try {
result = await finalizeAdyenPayment(sessionId, redirectResult);
} catch (unexpectedResult) {
// If user cancelled payment, handle without error message
if (unexpectedResult.code === "Cancelled" || unexpectedResult.code === "CANCEL") {
return {
name: routeData.PAYMENT_ADYEN.name,
replace: true,
};
}
// Otherwise, failure scenario.
// Payment fails, so return user to payment-adyen screen to try again or pay later.
return {
name: routeData.PAYMENT_ADYEN.name,
query: {
piaFailure: true,
},
replace: true,
};
}
console.log(`Out of promise`); console.log(`Out of promise`);
console.log(result); console.log(result);
@ -29,6 +52,12 @@ export async function adyenReturnBeforeEnter(to, from) {
const amountDue = getAmountDue(store.getters.order.lineItems); const amountDue = getAmountDue(store.getters.order.lineItems);
const ccToken = generateCcToken(sessionInfo); const ccToken = generateCcToken(sessionInfo);
ccToken.authCode = "831001";
ccToken.cardType = "VS";
ccToken.lastFour = "1111";
ccToken.expMonth = "03";
ccToken.expYear = "2030";
await baseMixin.methods.dispatchStoreAction( await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE, storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethod, paymentMethod,
@ -53,7 +82,10 @@ export async function adyenReturnBeforeEnter(to, from) {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return { return {
name: routeData.ERROR.name, name: routeData.PAYMENT_METHOD.name,
query: {
piaError: "true",
},
replace: true, replace: true,
}; };
} }
@ -87,12 +119,32 @@ function finalizeAdyenPayment(sessionId, redirectResult) {
onPaymentFailed: (result, component) => { onPaymentFailed: (result, component) => {
console.log(`Payment failed`); console.log(`Payment failed`);
console.log(result); console.log(result);
reject("Payment failed from Adyen");
if (result?.resultCode !== "Cancelled") {
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`;
global.$logger.logError(stringToLog);
}
reject({
code: result?.resultCode,
message: "Payment failed from Adyen",
});
}, },
onError: (error, component) => { onError: (error, component) => {
console.log(`Error occured`); console.log(`Error occured`);
console.log(error); console.log(error);
reject("Error from Adyen");
if (error?.name !== "CANCEL") {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
global.$logger.logError(stringToLog);
}
reject({
code: error?.name,
message: "Error from Adyen",
});
}, },
}, },
options: {}, options: {},

View file

@ -27,7 +27,7 @@ export async function vehicleBeforeEnter(to, from) {
if (zipData.isValid) { if (zipData.isValid) {
store.dispatch(storeActions.SAVE_SERVICE_ZIP_CODE_INFO, { store.dispatch(storeActions.SAVE_SERVICE_ZIP_CODE_INFO, {
zipCode: newZipFromQuerystring, zipCode: newZipFromQuerystring,
state: zipData.state.state, state: zipData.state,
zipCodeCtu: zipData.zipCodeCtu, zipCodeCtu: zipData.zipCodeCtu,
}); });
} }

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

@ -122,6 +122,7 @@ const getDefaultState = () => {
capabilityQuestionAnswers: null, capabilityQuestionAnswers: null,
dateOfLoss: null, dateOfLoss: null,
damageCause: null, damageCause: null,
installOemGlass: null,
}, },
lineItems: { lineItems: {
glassParts: null, glassParts: null,
@ -189,6 +190,7 @@ const getDefaultState = () => {
lockToken: null, lockToken: null,
settledTenderAmount: 0, settledTenderAmount: 0,
isRecalAckOptIn: false, isRecalAckOptIn: false,
isRecalAcknowledgedForScheduling: "",
isMSRFeeApplicable: false, isMSRFeeApplicable: false,
}, },
applicationUser: { applicationUser: {
@ -342,6 +344,12 @@ export const mutations = {
updateIsRecalAckOptIn(state, isRecalAckOptIn) { updateIsRecalAckOptIn(state, isRecalAckOptIn) {
state.order.isRecalAckOptIn = isRecalAckOptIn; state.order.isRecalAckOptIn = isRecalAckOptIn;
}, },
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
},
updateIsOemGlassSelected(state, isOemGlassSelected) {
state.order.damage.installOemGlass = isOemGlassSelected;
},
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable; state.order.isMSRFeeApplicable = isMSRFeeApplicable;
}, },
@ -508,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);
@ -712,6 +724,8 @@ export const mutations = {
state.order.damage.capabilityQuestionAnswers = state.order.damage.capabilityQuestionAnswers =
sessionInformation.order.damage.capabilityQuestionAnswers; sessionInformation.order.damage.capabilityQuestionAnswers;
state.order.damage.installOemGlass = sessionInformation.order.damage.installOemGlass;
state.order.lineItems.glassParts = sessionInformation.order.lineItems.glassParts; state.order.lineItems.glassParts = sessionInformation.order.lineItems.glassParts;
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems; state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? []; state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? [];
@ -1765,7 +1779,8 @@ export const actions = {
const glassArray = damage.glassToReplace; const glassArray = damage.glassToReplace;
const zipCode = order.serviceLocation.zipCode; const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin; const vin = vehicle.vin;
const serviceType = order.serviceLocation?.appointmentType; const serviceType = damage.isRepair ? "Repair" : "Install";
const parentAccountNumber = context.getters.payment.parentAccountNumber;
const referralSeqNumber = order.referralSequenceNumber; const referralSeqNumber = order.referralSequenceNumber;
// create a new array to avoid mutating state // create a new array to avoid mutating state
@ -1781,6 +1796,7 @@ export const actions = {
vin: vin, vin: vin,
serviceType: serviceType, serviceType: serviceType,
referralSeqNumber: referralSeqNumber, referralSeqNumber: referralSeqNumber,
parentAccountNumber: parentAccountNumber,
}, },
logApiCall: true, logApiCall: true,
pageNameToLog: pageNameToLog, pageNameToLog: pageNameToLog,
@ -1836,6 +1852,55 @@ export const actions = {
return response; return response;
}, },
// Parts API Actions
async getOemAfterMarketParts(context, { pageNameToLog }) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const carId = vehicle.carId;
const glassArray = damage.glassToReplace;
const resultsArray = damage.partQuestionAnswers;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
const make = vehicle.make;
const serviceType = order.serviceLocation?.appointmentType;
const referralSeqNumber = order.referralSequenceNumber;
const parentAccountNumber = applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
const oemEndorsementFlag = order.damage.installOemGlass || false;
// create a new array to avoid mutating state
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
const resultsArrayForPayload = convertResultsForApi(resultsArray);
const response = await globalMethods.callHttpClient({
method: endpoints.GetOemAfterMarketParts.method,
endpoint: endpoints.GetOemAfterMarketParts.url,
payload: {
carId: carId,
glassPieces: glassArrayForPayload,
answerResults: resultsArrayForPayload,
zip: zipCode,
vin: vin,
serviceType: serviceType,
referralSeqNumber: referralSeqNumber,
make: make,
parentAccountNumber: parentAccountNumber.toString(),
oemEndorsementFlag: oemEndorsementFlag,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
});
// Flatten location and name properties
if (response?.data?.glassPieceParts) {
response.data.glassPieceParts = convertGlassPieceNamingFromApi(
response.data.glassPieceParts
);
}
return response;
},
async getWipers(context, { payload, pageNameToLog }) { async getWipers(context, { payload, pageNameToLog }) {
const carId = payload.carId; const carId = payload.carId;
const serviceZipCode = payload.serviceZipCode; const serviceZipCode = payload.serviceZipCode;
@ -1936,26 +2001,6 @@ export const actions = {
return response; return response;
}); });
}, },
getServicePackageDiscountPart(context, { pageNameToLog }) {
const damage = context.getters.damage;
const damageType = damage.isRepair ? "Repair" : "Replace";
const glassPieces = convertGlassPieceNamingForApi(damage.glassToReplace);
return globalMethods
.callHttpClient({
method: endpoints.GetServicePackageDiscountPart.method,
endpoint: endpoints.GetServicePackageDiscountPart.url,
payload: {
glassPieces: glassPieces,
damageType: damageType,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
})
.then((response) => {
return response;
});
},
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) { getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
const escapeRecalibrationType = (rt) => rt.split("&").join("%26"); const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
@ -2421,6 +2466,7 @@ export const actions = {
partQuestionAnswers: order.damage.partQuestionAnswers, partQuestionAnswers: order.damage.partQuestionAnswers,
moldingQuestionAnswers: order.damage.moldingQuestionAnswers, moldingQuestionAnswers: order.damage.moldingQuestionAnswers,
capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers, capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers,
installOemGlass: order.damage.installOemGlass,
}, },
lineItems: { lineItems: {
glassParts: lineItems.glassParts, glassParts: lineItems.glassParts,
@ -2927,6 +2973,17 @@ export const actions = {
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn); 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);
},
saveIsMSRFeeApplicable(context, isMSRFeeApplicable) { saveIsMSRFeeApplicable(context, isMSRFeeApplicable) {
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable); context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
}, },
@ -3093,40 +3150,24 @@ export const actions = {
promoCode: lineItem.promoCode ?? null, promoCode: lineItem.promoCode ?? null,
})); }));
const pricedLineItemsFormattedForRequest = const order = context.getters.order;
buildQueryStringParameterFromArrayOfComplexObjects(
lineItemsWithOnlyPriceInfo,
"lineItems"
);
let queryString = "";
if (appointmentType == "Mobile") {
queryString =
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
`&BillToAccountNumber=${billToAccountNumber}` +
`&ProviderNumber=${providerNumber}` +
`&AppointmentType=${appointmentType}` +
`&ServiceLocation.City=${serviceLocationCity}` +
`&ServiceLocation.State=${serviceLocationState}` +
`&ServiceLocation.ZipCode=${serviceLocationZipCode}` +
`&${pricedLineItemsFormattedForRequest}`;
} else {
queryString =
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
`&BillToAccountNumber=${billToAccountNumber}` +
`&ProviderNumber=${providerNumber}` +
`&AppointmentType=${appointmentType}` +
`&${pricedLineItemsFormattedForRequest}`;
}
const lineItemServerData = context.getters.order.lineItems.serverData;
if (lineItemServerData) {
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
}
const response = await globalMethods.callHttpClient({ const response = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method, method: endpoints.TaxOrderItems.method,
endpoint: `${endpoints.TaxOrderItems.url}?${queryString}`, endpoint: endpoints.TaxOrderItems.url,
payload: {
parentAccountNumber: order.payment.parentAccountNumber,
billToAccountNumber: billToAccountNumber,
providerNumber: providerNumber,
appointmentType: appointmentType,
pricedLineItems: lineItemsWithOnlyPriceInfo,
serviceLocation: {
city: appointmentType == "Mobile" ? serviceLocationCity : null,
state: appointmentType == "Mobile" ? serviceLocationState : null,
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
},
serverData: order.lineItems.serverData ? order.lineItems.serverData : "",
},
logApiCall: true, logApiCall: true,
pageNameToLog: pageNameToLog, pageNameToLog: pageNameToLog,
}); });
@ -4105,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,

View file

@ -27,6 +27,8 @@ $red-600: #ac160b;
$red-700: #840900; $red-700: #840900;
$red-800: #5b0600; $red-800: #5b0600;
$red-900: #330300; $red-900: #330300;
$red-1000: #a80019;
$red-1100: #791710;
// Greens // Greens
$green-100: #e3f2ea; $green-100: #e3f2ea;

View file

@ -15,7 +15,7 @@
<!-- prettier-ignore --> <!-- prettier-ignore -->
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{ text }}<slot name="after-text"></slot></a> <a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{ text }}<slot name="after-text"></slot></a>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">{{ text }}<slot name="after-text"></slot></a> <a v-else-if="linkType === 'text'" @click="handleClick" class="text-link" :href="href">{{ text }}<slot name="after-text"></slot></a>
</template> </template>
<script> <script>
@ -89,5 +89,8 @@ a {
&.new-window-link { &.new-window-link {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
&.text-link {
white-space: nowrap;
}
} }
</style> </style>