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_SESSION_TIMEOUT_MINUTES__']: 30,
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
};
const keysOnly = Object.keys(environmentVariables);

View file

@ -10,7 +10,6 @@ module.exports = {
},
transformIgnorePatterns: ["'/node_modules/(?!vee-validate)"],
moduleFileExtensions: ["js", "vue"],
modulePathIgnorePatterns: ["vin-lookup"],
collectCoverageFrom: [
"src/**/*.{js,vue}",
"!src/main.js",
@ -33,11 +32,7 @@ module.exports = {
"!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-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/**/*-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/experiment-components/*.vue",
// END
@ -45,7 +40,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 64,
statements: 68,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

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

View file

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

View file

@ -32,6 +32,8 @@ const applicationConfig = {
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
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 };

View file

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

View file

@ -12,6 +12,9 @@ const experimentSettings = {
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience",
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional",
@ -33,6 +36,10 @@ const experimentSettings = {
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
SHOW_MOBILE_FIRST_POPUP_REPAIR: "Show_Mobile_First_Popup_Repair",
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
OFFER_OEM: "OfferOem",
USE_ADYEN_PAYMENT: "UseAdyenPayment",
};

View file

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

View file

@ -48,6 +48,10 @@ export const pageProgressMapper = {
percent: 30,
step: 1,
},
"policy-info": {
percent: 55,
step: 2,
},
quote: {
percent: 60,
step: 2,
@ -80,6 +84,10 @@ export const pageProgressMapper = {
percent: 98.5,
step: 4,
},
"payment-adyen": {
percent: 98.5,
step: 4,
},
confirmation: {
percent: 100,
step: 4,

View file

@ -77,4 +77,29 @@ export const quotePageDiscountTable = [
description: "PREM RCL GEOTARGET DISC",
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 RECAL_ACK_YES = "Yes";
const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
@ -31,4 +33,5 @@ export {
PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags,
cmsWidgetFieldMappings,
RECAL_ACK_YES,
};

View file

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

View file

@ -69,6 +69,8 @@ const storeMutations = {
LOCK_TOKEN: "updateLockToken",
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
@ -117,6 +119,7 @@ const storeMutations = {
UPDATE_EXTERNAL_PARAMETER_SERVICE_PACKAGE: "updateExternalParameterServicePackage",
UPDATE_EXTERNAL_PARAMETER_SOURCE: "updateExternalParameterSource",
UPDATE_EXTERNAL_PARAMETER_PHONE_NUMBER: "updateExternalParameterPhoneNumber",
UPDATE_EXTERNAL_PARAMETER_QS_STASH: "updateExternalParameterQsStash",
//RESET EXTERNAL_PARAMETER MUTATIONS
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>
<div
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">
<legend class="sr-only">Select a day and time</legend>
<div
v-for="month in months"
v-for="(month, i) in months"
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
class="calendar-grid-container"
class="calendar-grid-container month"
:class="[
hideSomeDaysForInitialView ? 'partial-month-initial-view' : '',
month.monthClass,
i === 0 ? 'first-month-shown' : '',
]">
<div class="month-year body-small d-flex align-items-center">
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
@ -114,7 +115,8 @@
</div>
</div>
<loader
:class="[!isLoading ? 'date-picker-hidden' : '']"
class="date-picker-loader"
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
loaderColor="blue"
loaderPosition="center" />
<div class="row form-test-error" id="date-of-month-error">
@ -125,7 +127,7 @@
type="button"
class="btn btn-link view-more-dates"
id="viewMoreDates"
:disabled="isLoading"
:disabled="loadingStatus !== 'none'"
@click="showAnotherMonth">
View more dates
</button>
@ -160,7 +162,6 @@ export default {
name: "datePicker",
data() {
return {
isLoading: true,
months: null,
disableViewMoreDatesButton: false,
hideSomeDaysForInitialView: null,
@ -169,7 +170,10 @@ export default {
};
},
props: {
customComponentId: String,
customComponentId: {
type: String,
default: "",
},
selectableDatesSetting: {
type: String,
validator(value) {
@ -200,6 +204,10 @@ export default {
pricingByDayUpcharge: Number,
isPricingByDayExperiment: Boolean,
isMobileSelected: Boolean,
loadingStatus: {
type: String,
default: "none",
},
},
setup(props) {
const uuid = uuidv4();
@ -281,7 +289,7 @@ export default {
if (event.screenX === 0 && event.screenY === 0) {
return;
}
this.$emit("date-clicked", date);
this.$emit("date-selected", date);
},
getWeekStartDate(dateString) {
const date = convertDateStringToDate(dateString);
@ -324,9 +332,9 @@ export default {
let preSelectedDateMonthEnd = this.getMonthEnd(
preSelectedDate.replace("-mobile", "")
);
let weekIncludesPreSelectedMonthEnd = false;
let monthEndsByThisWeek = false;
let i = 0;
while (!weekIncludesPreSelectedMonthEnd) {
while (!monthEndsByThisWeek && i < 52) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
@ -338,7 +346,12 @@ export default {
preSelectedDateMonthEnd === weekEndDate
) {
weekEndDate = preSelectedDateMonthEnd;
weekIncludesPreSelectedMonthEnd = true;
monthEndsByThisWeek = true;
} else if (preSelectedDateMonthEnd < weekEndDate) {
// Additional case: if month ends before this week (but not necessarily during it),
// still cut the loop here, but don't truncate the week.
monthEndsByThisWeek = true;
}
}
weeks.push({
@ -466,7 +479,7 @@ export default {
const response = config.getSelectableDatesCallback({
startDateString: initialViewStartDate,
endDateString: initialViewEndDate,
providerNumber: config.providerNumber,
inshopProviderNumber: config.inshopProviderNumber,
zipCode: config.zipCode,
includeMobileTimeSlots: config.includeMobileTimeSlots,
includeInshopTimeSlots: config.includeInshopTimeSlots,
@ -489,7 +502,6 @@ export default {
});
},
async setCalendarData(config = {}) {
this.isLoading = true;
this.selectableDatesInshop = [];
this.selectableDatesMobile = [];
this.months = [];
@ -541,7 +553,6 @@ export default {
// }
}
this.months = months;
this.isLoading = false;
if (config.preSelectedDate) {
this.$nextTick(() => {
@ -729,7 +740,6 @@ export default {
},
async showAnotherMonth() {
if (!this.months) return;
this.isLoading = true;
let monthToShow;
let monthStartDate;
@ -774,7 +784,6 @@ export default {
monthToShowDates[monthToShowDates.length - 1].inputValue
);
this.isLoading = false;
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
monthToShow.monthClass = monthToShow.monthClass.replace(" month-hidden", "");
this.scrollToElement(monthToShow.monthString);
@ -787,7 +796,6 @@ export default {
},
async updateSelectableDates(monthStart, monthEnd) {
const moreSelectableDates = await this.getMoreDatesCallback(monthStart, monthEnd);
moreSelectableDates.inshopTimeSlotsData.days.forEach((selectableDate) => {
const index = this.selectableDatesInshop.findIndex(
(dateObj) => dateObj.date === selectableDate.date
@ -846,12 +854,6 @@ export default {
},
},
watch: {
isLoading(newValue) {
// if done loading dates, then set to first available date
if (newValue === false && this.firstAvailableSelectableDate) {
this.selectedDate = this.firstAvailableSelectableDate;
}
},
modelValue(newValue) {
this.resetField({
value: newValue,
@ -872,9 +874,14 @@ export default {
</script>
<style lang="scss" scoped>
.date-picker-hidden {
.date-picker-loader {
opacity: 0;
max-height: 0;
&.show-loader {
opacity: 1;
max-height: none;
}
}
.date-picker {
position: relative;
@ -903,12 +910,12 @@ export default {
height: 1.5rem;
}
}
.calendar-grid-container {
.month {
margin: 0 auto 1rem auto;
position: relative;
transition:
height ease 2s,
opacity ease 2s;
height ease 1s,
max-height ease 1s;
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
@ -1157,7 +1164,7 @@ export default {
}
.past {
.calendar-grid-container {
.month {
.month-year {
grid-area: 1 / 1 / 2 / 6;
}
@ -1214,7 +1221,7 @@ export default {
}
// new calendar styling overrides
&.calendar-2025 {
.calendar-grid-container .grid-item {
.month .grid-item {
margin: 0 0 0.15rem 0;
}
.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) {
display: flex;

View file

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

View file

@ -1,4 +1,9 @@
/* 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,
@ -7,90 +12,38 @@ function setupCustomSettings(
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
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(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
var initESW = function (gslbBaseURL) {
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
//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)
// 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)
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'
embedded_svc.settings.enabledFeatures = ["LiveAgent"];
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,
embeddedservice_bootstrap.init(
"00D6s0000008r6A",
"Sales_Central_Snap_in",
"Sales_Central_TFN",
"https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477",
{
baseLiveAgentContentURL:
"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,
scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com",
}
);
};
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");
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -1,4 +1,9 @@
/* 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,
@ -7,91 +12,39 @@ function setupCustomSettings(
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
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(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
var initESW = function (gslbBaseURL) {
let embedded_svc = window.embedded_svc;
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
setupCustomSettings(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
);
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
// 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.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)
// 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,
embeddedservice_bootstrap.init(
"00D3t000004nvsT",
"Sales_Central_Snap_in",
"Sales_Central_TFN",
"https://safelite2.my.site.com/ESWSalesCentralTFN1769108630294",
{
baseLiveAgentContentURL:
"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,
scrt2URL: "https://safelite2.my.salesforce-scrt.com",
}
);
};
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");
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// END OF SALESFORCE GENERATED CODE
}

View file

@ -1,4 +1,9 @@
/* 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,
@ -7,93 +12,38 @@ function setupCustomSettings(
onWebchatCloseCallback
) {
modifyEmbeddedSvcCallback();
embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback);
embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback);
embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback);
window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback);
window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback);
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(
modifyEmbeddedSvcCallback,
finishedLoadingCallback,
onWebchatOpenCallback,
onWebchatCloseCallback
) {
var initESW = function (gslbBaseURL) {
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US'
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'
//embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert)
//embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline)
//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)
// 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",
embeddedservice_bootstrap.init(
"00DTI000005x7c7",
"Sales_Central_TFN",
"https://safelite2--qa.sandbox.my.site.com/ESWSalesCentralTFN1768832680884",
{
baseLiveAgentContentURL:
"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,
scrt2URL: "https://safelite2--qa.sandbox.my.salesforce-scrt.com",
}
);
};
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");
} catch (err) {
console.error("Error loading Embedded Messaging: ", err);
}
// 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 * as devHelper from "./salesforce-helper-dev";
import * as qaHelper from "./salesforce-helper-qa";
import * as sysHelper from "./salesforce-helper-sys";
import * as prodHelper from "./salesforce-helper-prod";
import { applicationConfig } from "../../constants/application-config";
describe("salesforceWebchat.vue", () => {
beforeEach(() => {
window.embeddedservice_bootstrap = {
settings: {},
};
});
afterEach(() => {
delete window.embeddedservice_bootstrap;
});
it("renders correctly", () => {
const wrapper = shallowMount(salesforceWebchat);
expect(wrapper.exists()).toBe(true);
@ -14,7 +24,7 @@ describe("salesforceWebchat.vue", () => {
it("has the correct default data", () => {
const wrapper = shallowMount(salesforceWebchat);
expect(wrapper.vm.$data).toEqual({
isAgentAvailable: false,
isWebchatLoaded: false,
isWebchatOpen: false,
});
});
@ -37,6 +47,12 @@ describe("salesforceWebchat.vue", () => {
wrapper.vm.initializeSalesforceWebchat();
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";
wrapper = shallowMount(salesforceWebchat);
const mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev");

View file

@ -6,6 +6,7 @@
// Supporting files
import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev";
import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa";
import { initializeSalesforceWebchatForSys } from "./salesforce-helper-sys";
import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod";
import { applicationConfig } from "@/constants/application-config";
import { webchatHelper } from "@/helpers/webchat-helper";
@ -16,7 +17,7 @@ export default {
mixins: [analyticsMixin],
data() {
return {
isAgentAvailable: false,
isWebchatLoaded: false,
isWebchatOpen: false,
};
},
@ -33,32 +34,13 @@ export default {
mounted() {
// Load in script from salesforce CDN
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;
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() {
// Clean up event listeners and observers
window.removeEventListener(
"salesforce-chat-visibility",
this._handleSalesforceChatVisibility
);
window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer);
if (this._salesforceChatOpenObserver) {
this._salesforceChatOpenObserver.disconnect();
}
},
methods: {
initializeSalesforceWebchat() {
@ -72,7 +54,6 @@ export default {
);
break;
case "QA":
case "SysTest":
initializeSalesforceWebchatForQa(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
@ -80,8 +61,17 @@ export default {
this.onWebchatCloseCallback
);
break;
case "SysTest":
initializeSalesforceWebchatForSys(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
this.onWebchatOpenCallback,
this.onWebchatCloseCallback
);
break;
case "Dev":
case "Localhost":
default:
initializeSalesforceWebchatForDev(
this.modifyEmbeddedSvcCallback,
this.finishedLoadingCallback,
@ -92,16 +82,13 @@ export default {
}
},
modifyEmbeddedSvcCallback() {
window.embedded_svc.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png");
window.embedded_svc.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png");
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", () => {
window.embeddedservice_bootstrap.settings.hideChatButtonOnLoad = true;
window.addEventListener("onEmbeddedMessagingConversationStarted", () => {
this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true);
});
},
finishedLoadingCallback(data) {
this.isAgentAvailable = data.isAgentAvailable;
this.isWebchatLoaded = true;
this.updateShouldShowWebchatButton();
},
onWebchatOpenCallback() {
@ -117,16 +104,35 @@ export default {
updateShouldShowWebchatButton() {
this.webchatGlobalNonpersistedState.showWebchatButton =
!this.hideSalesforceWebchatLaunchButton &&
this.isAgentAvailable &&
this.isWebchatLoaded &&
!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: {},
};
</script>
<style lang="scss">
.embedded-messaging > .embeddedMessagingFrame[class~="isMaximized"] {
z-index: 9999 !important;
}
.dockableContainer {
&.showDockableContainer {
color: $gray-600;

View file

@ -59,14 +59,6 @@ describe("sierraWebchat.vue", () => {
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", () => {
const eventSpy = jest.spyOn(window, "dispatchEvent");
const transfer = {
@ -77,25 +69,4 @@ describe("sierraWebchat.vue", () => {
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 { routeData } from "@/router/constants/routes";
import { deepClone } from "@/helpers/object-helper";
import { webchatHelper } from "@/helpers/webchat-helper";
export default {
name: "sierraWebchat",
mixins: [analyticsMixin],
data() {
return {
transferInProgress: false,
};
},
setup() {
const { webchatGlobalNonpersistedState, launchWebchat } = webchatHelper();
return { webchatGlobalNonpersistedState, launchWebchat };
},
methods: {
openSierraChatModal() {
const sierra = window.sierraChat || window.sierra || window.SierraChat;
@ -35,11 +45,18 @@ export default {
display: "corner",
onLoad: () => this.openSierraChatModal(),
onOpen: () => {},
onClose: () => window.dispatchEvent(new CustomEvent("sierra-chat-closed")),
onTransfer: (transfer) =>
onClose: () => {
if (!this.transferInProgress) {
this.webchatGlobalNonpersistedState.showWebchatButton = true;
this.transferInProgress = false;
}
},
onTransfer: (transfer) => {
this.transferInProgress = true;
window.dispatchEvent(
new CustomEvent("sierra-chat-transfer", { detail: transfer })
),
);
},
};
},
getSierraContext() {
@ -138,10 +155,7 @@ export default {
}
// Pass data to Salesforce chat if available
const transfer = event.detail;
if (
window.embedded_svc &&
typeof window.embedded_svc.bootstrapEmbeddedService === "function"
) {
if (window.embeddedservice_bootstrap && window.embeddedservice_bootstrap.prechatAPI) {
if (
transfer &&
transfer.data &&
@ -149,51 +163,22 @@ export default {
transfer.data.last_name &&
transfer.data.email
) {
window.embedded_svc.settings.prepopulatedPrechatFields = {
window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
ScarlettAISummary: transfer.data.chat_summary,
FirstName: transfer.data.first_name,
LastName: transfer.data.last_name,
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);
// 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() {
window.removeEventListener("launch-sierra-webchat", this.triggerSierraChat);
window.removeEventListener("sierra-chat-transfer", this._handleSierraTransfer);
if (this._sierraChatContainerObserver) {
this._sierraChatContainerObserver.disconnect();
}
},
};
</script>

View file

@ -59,7 +59,6 @@ export default {
return {
globalAlertMessages: [],
sierraChatOpen: false,
salesforceChatOpen: false,
isSalesforceTransferInProgress: false,
isScrolled: false,
};
@ -80,20 +79,7 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "LogoImage");
},
shouldShowWebchatButton() {
// Hide the chat icon if Sierra chat is open
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
);
return this.webchatGlobalNonpersistedState.showWebchatButton;
},
},
methods: {
@ -121,10 +107,6 @@ export default {
webchatClicked(event) {
event.preventDefault();
this.launchWebchat();
// Only set sierraChatOpen if Sierra experiment is active
if (this.webchatGlobalNonpersistedState.shouldLaunchSierra) {
this.sierraChatOpen = true;
}
},
syncSierraExperimentFlag() {
const experiments = store.getters.applicationUser.experiments;
@ -164,37 +146,6 @@ export default {
unknownAlertEvent.displayAlert = true;
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
this._handleScroll = () => {
@ -203,14 +154,7 @@ export default {
window.addEventListener("scroll", this._handleScroll);
},
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("salesforce-chat-visibility", this._handleSalesforceChatOpened);
window.removeEventListener("scroll", this._handleScroll);
},
};

View file

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

View file

@ -25,7 +25,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
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
)
);

View file

@ -54,10 +54,6 @@ export default {
mounted() {
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
props: {
modelValue: Object,

View file

@ -4,6 +4,7 @@ import globalMethods from "@/global-methods";
import { endpoints } from "@/constants/endpoints";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { paymentMethods } from "@/constants/payment-method-constants";
import { applicationConfig } from "../constants/application-config";
export async function getNewSession(sessionConfig) {}
@ -42,8 +43,8 @@ export async function createAdyenCheckout({
id: id,
sessionData: sessionData,
},
clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
environment: "test",
clientKey: applicationConfig.ADYEN_CLIENT_KEY,
environment: applicationConfig.ADYEN_ENVIRONMENT,
locale: "en_US",
countryCode: "US",
showPayButton: showPayButton,
@ -83,6 +84,21 @@ export function mapAdyenToFmgPaymentMethod(adyenMethod) {
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) {
const order = store.getters.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];
export function anyPartWithRequiresRecalFlag(lineItems) {
if (!lineItems) {
return false;
}
return lineItems.glassParts?.some((li) => li.requiresRecalibration === true);
}
export function isRecalPartOrHasChildRecalPart(lineItem) {
if (lineItem.childParts && lineItem.childParts.length > 0) {
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);

View file

@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
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) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return packageNames.TIER_THREE;

View file

@ -8,10 +8,11 @@ const webchatGlobalNonpersistedState = reactive({
export const webchatHelper = () => {
function launchWebchat() {
webchatGlobalNonpersistedState.showWebchatButton = false;
if (webchatGlobalNonpersistedState.shouldLaunchSierra) {
window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat
} 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(
"email-address-format",
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
)
);

View file

@ -70,7 +70,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
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
)
);

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 flushSaveSessionQueue();
await this.removeQPDLineItemIfExists();
await this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
this.parentAccountNumber.toString(),
@ -193,6 +195,17 @@ export default {
handlePageShow() {
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() {
window.removeEventListener("pageshow", this.handlePageShow);

View file

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

View file

@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
//Assert
expect(spy).toHaveBeenNthCalledWith(
1,
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
false,
false
);
expect(spy).toHaveBeenNthCalledWith(
2,
"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)
// used in GET_PARTS call following this one
await this.dispatchStoreAction(

View file

@ -1,18 +1,48 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
<!--
... Page code goes here.
-->
<div id="adyen-container"></div>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container">
<div class="row">
<div class="col-12">
<div>
<alert
ref="piaErrorAlert"
class="my-4"
v-if="hasPaymentFailureError"
cmsWidgetName="PIAErrorAlertWidget"
alertClass="alert-danger"
@textLinkClicked="paymentFailedPayLater"
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
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
@ -29,6 +59,8 @@ import Form from "vee-validate";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert";
import textBlock from "@/digital-components/text-block/text-block";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
@ -42,9 +74,16 @@ import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"
import { createAdyenCheckout } from "@/helpers/adyen-helper";
import { Dropin } from "@adyen/adyen-web/auto";
import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants";
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 {
name: "payment-adyen",
@ -52,6 +91,8 @@ export default {
data() {
return {
sessionId: String,
dropinComponent: null,
hasPaymentFailureError: false,
};
},
@ -69,9 +110,15 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
const hasExistingPiaError = to.query?.piaFailure;
// Hydrate page with results
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
if (hasExistingPiaError) {
vm.hasPaymentFailureError = true;
}
});
},
@ -89,18 +136,117 @@ export default {
},
arePagePrerequisitesValid() {
// Stub, for checking if application is capable of entering this page
// And/or, is all information required for this page present?
// To prevent sequence-breaking from the end-user.
// Service Location
const serviceLocation = store.getters.order.serviceLocation;
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() {
console.log(`Price = ${this.amountDue}`);
console.log(`Adyen Price = ${this.adyenPriceTotal}`);
const requestBody = this.adyenInitRequestInfo;
const requestBody = this.getAdyenInitRequestInfo();
console.log(`Calling with:`);
console.log(requestBody);
@ -148,8 +294,16 @@ export default {
console.log(checkout);
window.checkout = checkout;
const dropin = new Dropin(checkout, {
const expiryTime = new Date(checkout.options.expiresAt);
const expiryInterval = expiryTime.getTime() - new Date().getTime();
const handleTimeout = () => {
this.resetAdyenDropin();
};
const timeout = setTimeout(handleTimeout, expiryInterval);
const configuration = {
paymentMethodsConfiguration: {
ideal: {
showImage: true,
@ -159,10 +313,10 @@ export default {
value: this.adyenPriceTotal,
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.
blockPayPalVenmoButton: false,
//blockPayPalPayLaterButton: true
blockPayPalVenmoButton: true,
blockPayPalPayLaterButton: true,
},
card: {
hasHolderName: true,
@ -171,7 +325,21 @@ export default {
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) {
@ -189,6 +357,8 @@ export default {
console.log(`Get session payload =`);
console.log(paymentSessionRequest);
showFmgLoadingModal(true);
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
@ -233,7 +403,20 @@ export default {
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(
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
this.amountDue,
@ -246,33 +429,33 @@ export default {
console.log(`Result =`);
console.log(result);
// Fetch session info
const paymentSessionRequest = {
zipCodeCtu: this.locationInfo.zipCodeCtu,
workOrderNumber: this.workOrderNumberLastSixDigits,
sourceSystem: this.sourceSystem,
sessionId: this.sessionId,
sessionResult: result?.sessionResult,
};
const wasPaymentCancelled = result?.resultCode === "Cancelled";
console.log(`Get session payload =`);
console.log(paymentSessionRequest);
if (!wasPaymentCancelled) {
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`;
try {
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: paymentSessionRequest,
logApiCall: true,
pageNameToLog: "payment-adyen",
});
await global.$logger.logError(stringToLog);
console.log(response);
} catch (error) {
console.log(`Error getting session info after failure.`);
this.hasPaymentFailureError = true;
}
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() {
// Work order submission after successful payment.
@ -285,15 +468,12 @@ export default {
} catch (error) {
console.log(`error: response from submit work order: ${error.message}`);
// navigate to error page
const errorPayload = {
cause: `Error while submitting work order after Adyen payment.`,
currentPage: this.pageName,
sessionId: this.sessionId,
idempotencyKey: this.idempotencyKey,
};
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.pageName
);
this.$router.handleSoftError(errorPayload);
return;
}
// clear order
@ -303,10 +483,57 @@ export default {
this.pageName
);
},
},
computed: {
adyenInitRequestInfo() {
async paymentFailedPayLater() {
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 {
sourceSystem: this.sourceSystem,
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
@ -323,9 +550,15 @@ export default {
IP: "127.0.0.1", // TODO
firstName: this.$store.getters.order.customer.firstName,
lastName: this.$store.getters.order.customer.lastName,
idempotencyKey: this.idempotencyKey,
idempotencyKey: this.getIdempotencyKey(),
};
},
},
computed: {
piaType() {
return this.$store.getters.order.payment.piaType;
},
workOrderNumberLastSixDigits() {
const workOrderNumber = this.$store.getters.order.workOrderNumber ?? "";
@ -382,10 +615,6 @@ export default {
};
},
idempotencyKey() {
return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`;
},
sourceSystem() {
return "FMG-2.0";
},
@ -395,7 +624,41 @@ export default {
},
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,
funnelSubHeader,
navbar,
cart,
alert,
textBlock,
},
};
</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);
if (this.isInsurance) {
return piaInsurance === "true" && this.currentDeductible > 0;
return (
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
this.currentDeductible > 0
);
} else {
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
if (piaExperience?.includes("PIA Disabled")) {
return this.isConditionalPIAEnabled;
} else {
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
}
}
},
isConditionalPIAEnabled() {
const conditionalProviderNumbers = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS
);
const conditionalProviderNumbers2 = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS_2
);
const conditionalProviderShutoffDate = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_SHUTOFF_DATE
);
const allConditionalProviderNumbers = [
conditionalProviderNumbers,
conditionalProviderNumbers2,
]
.filter(Boolean)
.join(",");
const providerNumber =
this.$store.getters.order.serviceLocation.provider.providerNumber;
const appointmentDate = new Date(this.$store.getters.order.schedule.date);
const shutoffDate = new Date(conditionalProviderShutoffDate);
if (allConditionalProviderNumbers && conditionalProviderShutoffDate) {
const providerNumbersArray = allConditionalProviderNumbers
.split(",")
.map((num) => num.trim());
if (!providerNumbersArray.includes(providerNumber)) {
return false;
} else {
return appointmentDate <= shutoffDate;
}
}
return false;
},
paymentMethod() {
if (!this.isPiaEnabled) {
return paymentMethods.LATER;

View file

@ -745,10 +745,16 @@ export default {
}
},
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 (
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);
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;
}
h3 {
font-family: UrbanistSemibold;
font-family: UrbanistBold;
font-size: 1rem;
margin: 0 0 0 0.5rem;
color: $black;
}
}
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) {
return false;
}
if (settingName === mockExperimentSettings.OFFER_OEM) {
return false;
}
return "test";
},
},

View file

@ -33,6 +33,7 @@
@vapsItemsSelected="vapsItemsSelectedAction"
@quotePageDiscountSelected="quotePageDiscountSelectedAction"
@currentNumberOfPackages="currentNumberOfPackagesAction"
@servicePackageSelected="servicePackageSelectedAction"
:servicePackage="servicePackage"
:activePromos="lineItems.promos"
:availableQuotePageDiscounts="quoteDiscountPartsInfo"
@ -85,6 +86,13 @@
isInsuranceSelected ? isInsuranceContinueButtonText : ''
" />
<oemGlassQuestion
cmsWidgetName="OemGlassQuestionWidget"
v-if="offerOem && !isInsuranceSelected"
v-model="isOemGlassSelected"
:oemGlassPartPrice="oemGlassPartPrice"
:aftermarketGlassPartPrice="aftermarketGlassPartPrice" />
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
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 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 oemGlassQuestion from "./oem-glass-question/oem-glass-question.vue";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
@ -217,6 +226,15 @@ export default {
null,
"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 skipQuoteExperiment = store.getters.applicationUser.experiments.find(
@ -290,6 +308,10 @@ export default {
resultKey: "supportingItems",
promise: supportingItemsPromise,
},
{
resultKey: "oemGlassCheck",
promise: oemGlassCheckPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
@ -311,6 +333,18 @@ export default {
lineItems.vaps = lineItems.vaps ?? [];
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 quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({
@ -328,6 +362,18 @@ export default {
...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
var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber;
var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber;
@ -389,6 +435,41 @@ export default {
vm.holdParentAccountNumber = holdParentAccountNumber;
vm.holdBillToAccountNumber = holdBillToAccountNumber;
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) {
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
@ -556,6 +637,8 @@ export default {
availableLineItems: null,
lineItems: [],
addableVaps: [],
offerOem: null,
alternateGlassPartsForOem: null,
servicePackage: null,
holdParentAccountNumber: null,
holdBillToAccountNumber: null,
@ -564,6 +647,8 @@ export default {
isSaveProgressComplete: null,
packageNumber: null,
skipToInsurance: null,
servicePackageSelected: null,
isOemGlassSelected: false,
};
},
async mounted() {
@ -670,6 +755,9 @@ export default {
vapsItemsSelectedAction(vapsItemsSelected) {
this.lineItems.vaps = vapsItemsSelected;
},
servicePackageSelectedAction(newValue) {
this.servicePackageSelected = newValue;
},
quotePageDiscountSelectedAction(supportingItemsSelected) {
this.lineItems.supportingItems = supportingItemsSelected;
},
@ -725,6 +813,20 @@ export default {
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) {
this.dispatchStoreAction(
this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
@ -757,8 +859,20 @@ export default {
},
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;
if (payment.isInsurance) {
@ -868,6 +982,26 @@ export default {
const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
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: {
funnelHeader,
@ -884,6 +1018,7 @@ export default {
recalDisclaimer,
saveProgressModalQuestion,
saveProgressPopupQuestion,
oemGlassQuestion,
},
};
</script>

View file

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

View file

@ -50,7 +50,7 @@ export default {
data() {
return {
isModalOpen: false,
selectedAppontment: {
selectedAppointment: {
estimatedServiceMinutes: {
minimum: null,
maximum: null,
@ -58,6 +58,7 @@ export default {
timeSlot: null,
date: null,
},
confirmedAppointment: false,
};
},
methods: {
@ -67,22 +68,25 @@ export default {
this.modal.openModal();
},
onModalClosed() {
this.pushEvent();
this.$emit("close-mobile-first-modal");
this.confirmedAppointment = false;
},
closeModal() {
this.modal.closeModal();
},
confirmAppointment() {
let selectedTimeSlot = {
timeSlot: this.selectedAppontment.timeSlot,
routeCode: this.selectedAppontment.timeSlot.id,
date: this.selectedAppontment.date,
timeSlot: this.selectedAppointment.timeSlot,
routeCode: this.selectedAppointment.timeSlot.id,
date: this.selectedAppointment.date,
};
this.confirmedAppointment = true;
this.$emit("confirm-appointment", selectedTimeSlot);
this.modal.closeModal();
},
setSelectedAppointment(appointment) {
this.selectedAppontment = appointment;
this.selectedAppointment = appointment;
},
getIsModalOpen() {
return this.isModalOpen;
@ -90,6 +94,21 @@ export default {
setIsModalOpen(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: {
modalName() {
@ -112,37 +131,37 @@ export default {
if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
const serviceType = "<strong>" + this.getServiceType + "</strong>";
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 =
"<strong>" +
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", {
new Date(this.selectedAppointment.date).toLocaleDateString("en-US", {
weekday: "long",
timeZone: "UTC",
}) +
"</strong>";
tokens.splice(tokens.indexOf(token), 1, day);
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) {
const date = new Date(this.selectedAppontment.date).toLocaleDateString(
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppointment.date) {
const date = new Date(this.selectedAppointment.date).toLocaleDateString(
"en-US",
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
);
tokens.splice(tokens.indexOf(token), 1, date);
} else if (
token.includes(INLINE_TIMESLOT_TOKEN) &&
this.selectedAppontment.timeSlot
this.selectedAppointment.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);
} else if (
token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
this.selectedAppontment.estimatedServiceMinutes
this.selectedAppointment.estimatedServiceMinutes
) {
const inshopDurationTime = getDisplayTextForDurationLength(
this.selectedAppontment.estimatedServiceMinutes.minimum,
this.selectedAppontment.estimatedServiceMinutes.maximum
this.selectedAppointment.estimatedServiceMinutes.minimum,
this.selectedAppointment.estimatedServiceMinutes.maximum
);
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"
ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" />
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
groupName="recalAckGroup"
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
ref="recalAckModal" />
<div
class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
@ -22,6 +28,7 @@
cmsWidgetName="ScheduleYourServiceWidget"
id="schedule-your-service" />
<appointmentTypeQuestion
v-if="hasSelectableDatesLoaded"
v-model="appointmentTypeFromAppointmentTypeQuestion"
v-show="isAppointmentTypeDisplayed"
:isServiceableMobile="isServiceableMobile"
@ -91,15 +98,11 @@
linkWidgetName="ServiceZipLinkWidget"
modalWidgetName="ServiceZipModalWidget" />
</div>
<div
class="row your-shop-location"
v-if="appointmentType && appointmentType !== appointmentTypeStrings.MOBILE">
<div class="row your-shop-location" v-if="showShopLocationComponent">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<div>
<shopLocation
v-if="
appointmentType && appointmentType !== appointmentTypeStrings.MOBILE
"
v-if="showShopLocationComponent"
:selectedShop="selectedShopAnswer"
:selectedProviderNumberFromParent="selectedProvider?.providerNumber"
:shopProviderDataFromParent="shopProviderData"
@ -133,8 +136,6 @@
:isOvernightDropoff="isOvernightDropoff" />
</div>
<datePicker
:currentZip="zipCode"
:currentProviderNumber="selectedProvider?.providerNumber"
v-show="appointmentType"
customComponentId="dateQuestion"
selectableDatesSetting="custom"
@ -144,11 +145,12 @@
class="text-link-small"
:getMoreDatesCallback="getMoreScheduleData"
validationRules="date-required"
@date-clicked="handleDateClicked"
@date-selected="handleDateSelected"
:pricingByDayBasePrice="pricingByDayBasePrice"
:pricingByDayUpcharge="pricingByDayUpcharge"
:showPricingByDay="showPricingByDay"
:isPricingByDayExperiment="isPricingByDayExperiment" />
:isPricingByDayExperiment="isPricingByDayExperiment"
:loadingStatus="calendarLoadingStatus" />
<timeSlotQuestion
ref="timeSlotModalQuestion"
v-if="showTimeSlotQuestion"
@ -167,7 +169,8 @@
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:isSameDay="isSameDay"
:selectedRouteCodeData="selectedRouteCodeData"
@waitListRequested="handleWaitListRequested" />
@waitListRequested="handleWaitListRequested"
:lazyLoadAmountToLoad="10" />
<navbar
cmsWidgetName="FunnelFooterWidget"
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 durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
@ -216,6 +220,7 @@ import {
RouteCodeFlags,
PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE,
RECAL_ACK_YES,
} from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -242,6 +247,7 @@ import {
isDropOffRouteCode,
getTodayDate,
} from "@/layouts/schedule/helpers/schedule-helper";
import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper";
import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
import { required } from "@/helpers/validation-rules";
@ -287,6 +293,7 @@ const getScheduleApiResponse = async ({
startDateString,
endDateString,
providerNumber,
inshopProviderNumber,
zipCode,
includeMobileTimeSlots = true,
includeInshopTimeSlots = true,
@ -329,7 +336,7 @@ const getScheduleApiResponse = async ({
startDate: apiStartDate,
endDate: apiEndDate,
shopAppointmentType: "InshopOrDropoff",
providerNumber: providerNumber,
providerNumber: inshopProviderNumber,
},
};
if (apiStartDate < apiEndDate) {
@ -429,19 +436,19 @@ export default {
name: "schedule",
data() {
return {
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
selectedDate: this.getSelectedDateFromStore(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
mobilePremiumAppointmentFee: null,
waitListRequested: null,
displayWaitList: null,
pricingByDayUpchargeLineItem: null,
includePricingByDayUpcharge: null,
isPricingByDayExperiment: null,
pricingByDayBasePrice: null,
pricingByDayUpcharge: null,
showPricingByDay: null,
selectableDatesInshop: [],
selectableDatesMobile: [],
selectableDatesInshop: {},
selectableDatesMobile: {},
preSelectedDate: null,
streetAddress: this.getServiceAddressFromStore(),
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
@ -474,6 +481,7 @@ export default {
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
),
calendarLoadingStatus: "none",
};
},
async beforeRouteEnter(to, from, next) {
@ -584,6 +592,8 @@ export default {
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.showLoadingModal();
vm.calendarLoadingStatus = "initial";
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.mobilePremiumAppointmentFee = pricedPremiumFee;
@ -604,6 +614,9 @@ export default {
);
vm.initializeDatePicker().then(() => {
vm.showMobileFirstModal();
vm.hideLoadingModal();
vm.calendarLoadingStatus = "none";
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
});
});
},
@ -620,8 +633,25 @@ export default {
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() {
return this.appointmentType == AppointmentTypeStrings.MOBILE;
return (
this.appointmentType == AppointmentTypeStrings.MOBILE ||
(this.isServiceableMobile && !this.isServiceableInshop)
);
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
@ -633,15 +663,18 @@ export default {
return this.isGlassServiceableMobile;
}
},
closestInshopProviderNumber() {
return this.shopProviderData?.shopProviders[0]?.providerNumber;
},
showTimeSlotQuestion() {
if (
this.selectedDate &&
this.selectedDate.includes("mobile") &&
!this.isMobileSelected
this.appointmentTypeFromAppointmentTypeQuestion &&
this.timeSlotsForSelectedDate
) {
this.setSelectedDateToFirstAvailable();
return true;
}
return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion;
return false;
},
isMobileStaticRecalibrationApplicable() {
return (
@ -869,6 +902,60 @@ export default {
? this.$refs.mobileFirstModal?.getIsModalOpen()
: 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: {
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) {
this.resetWaitlist();
this.preSelectedDate = null;
this.calendarLoadingStatus = "reinitial";
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
const oldProvider = this.selectedProvider;
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
});
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider?.providerNumber;
// No zipCodeData comes back if zip was not changed
if (shopQuestionPopUpData.zipCodeData) {
@ -1169,24 +1236,32 @@ export default {
selectedProvider
);
} else {
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider.providerNumber;
if (didProviderNumberChange) {
this.updateSelectedProvider(selectedProvider);
this.initializeDatePicker();
}
}
this.initializeDatePicker().then(() => {
this.showMobileFirstModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
});
},
async getMoreScheduleData(startDate, endDate) {
// called only when "View more dates" is clicked; not on initial page load
this.calendarLoadingStatus = "more";
const moreShopTimeSlots = await getScheduleApiResponse({
startDateString: startDate,
endDateString: endDate,
providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
});
this.calendarLoadingStatus = "none";
// ADD API CALL RESULTS TO EXISTING DATE DATA
this.selectableDatesInshop.days = this.selectableDatesInshop.days.concat(
@ -1202,8 +1277,6 @@ export default {
async initializeDatePicker() {
this.isShowMobileFirstAppt && this.showLoadingModal();
this.selectedDate = null;
const includeMobileTimeSlots = this.isServiceableMobile;
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
const datePickerInitialData = await this.$refs.datePicker.loadInitialData({
@ -1213,6 +1286,9 @@ export default {
getSelectableDatesCallback: getScheduleApiResponse,
preSelectedDate: this.preSelectedDate,
providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: includeMobileTimeSlots,
includeInshopTimeSlots: includeInshopTimeSlots,
@ -1224,7 +1300,6 @@ export default {
await datePickerInitialData.initialShopTimeSlotsResponse.inshopTimeSlotsData;
this.selectableDatesMobile =
await datePickerInitialData.initialShopTimeSlotsResponse.mobileTimeSlotsData;
this.setDisplayWaitList();
if (this.preSelectedDate) {
this.selectedDate = this.preSelectedDate;
@ -1236,11 +1311,11 @@ export default {
) {
await this.$nextTick();
await this.$refs.datePicker.showAnotherMonth();
this.setDisplayWaitList();
}
}
this.isShowMobileFirstAppt && this.hideLoadingModal();
},
getIsRecalAcknowledgedForSchedulingFromStore() {
return store.getters.order.isRecalAcknowledgedForScheduling;
},
getScheduleApiResponse,
getServiceZipCtuCodeFromStore() {
@ -1251,8 +1326,8 @@ export default {
if (isMobileSelected === undefined) {
isMobileSelected = this.appointmentType === AppointmentTypeStrings.MOBILE;
}
let storedDate = store.getters.order.schedule.date;
if (isMobileSelected) storedDate += "-mobile";
let storedDate = store.getters.order.schedule?.date;
if (isMobileSelected && storedDate) storedDate += "-mobile";
return storedDate;
},
getSelectedTimeSlotInfo() {
@ -1360,7 +1435,25 @@ export default {
hideLoadingModal() {
this.$refs.loadingModal.hideModal();
},
async closeRecalAcknowledgementPopup() {
this.showRecalAcknowledgementPopup = false;
},
async forwardButtonAction() {
const showRecalAcknowledgementModal = this.showRecalAcknowledgementModal();
if (
showRecalAcknowledgementModal &&
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
) {
this.showRecalAcknowledgementPopup = true;
return;
}
this.dispatchStoreAction(
storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
this.isRecalAcknowledgedForScheduling,
false
);
this.navigatingForward = true;
var ctu = this.zipCodeCtu;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
@ -1489,39 +1582,6 @@ export default {
}
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() {
const supportingItems = this.getSupportingItems();
@ -1591,28 +1651,39 @@ export default {
handleWaitListRequested(value) {
this.waitListRequested = value;
},
handleDateClicked(date) {
// do something to mark this as upcharge day or not...
if (date.isPricingByDayUpchargeDay) {
this.includePricingByDayUpcharge = true;
} else {
this.includePricingByDayUpcharge = false;
handleDateSelected(date) {
const previousDate = this.selectedDate;
// check if date actually changed
if (previousDate !== date.dateString) {
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
this.updateFooterButtonText(this.selectedTimeSlotInfo);
}
// TODO: REMOVE AS PART OF CASH-1634
// // 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) {
if (!timeSlotObj?.routeCode) {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
return;
}
const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find(
@ -1662,6 +1733,8 @@ export default {
this.updateSelectedProvider();
},
updateSelectedProvider(newShopProvider) {
if (!this.appointmentTypeStrings) return;
if (newShopProvider) {
this.selectedProvider = newShopProvider;
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
@ -1682,8 +1755,29 @@ export default {
this.selectedProvider = this.shopProviderData.shopProviders[0];
}
},
handleZipCodeChange(newZipCode) {
async handleZipCodeChange(newZipCode) {
// if we were showing the recal ack modal and now are changing the zip, we need to navigate
// to service-zip so we can get call parts again to add the recal part if needed
if (this.shouldShowRecalAckModal()) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
zipCode: newZipCode.zipCode,
state: newZipCode.state,
zipCodeCtu: newZipCode.zipCodeCtu,
},
false
);
this.$router.navigateWithSaving(
this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
this.pageName
);
return;
}
this.resetMobileLocation();
this.calendarLoadingStatus = "initial";
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
this.shopProviderData = result.data;
@ -1694,6 +1788,9 @@ export default {
this.selectedDate = null;
this.initializeDatePicker().then(() => {
this.showMobileFirstModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
});
});
},
@ -1705,10 +1802,12 @@ export default {
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
this.selectedDate = null;
this.selectedProvider = newProvider;
this.initializeDatePicker();
},
handleAppointmentTypeChange(newAppointmentType) {
this.updateFooterButtonText();
// if time appointment type changes, clear any selected time slot
this.selectedTimeSlotInfo = this.getEmptyTimeSlot();
if (newAppointmentType === AppointmentTypeStrings.MOBILE) {
// Remember last shop selected if previous selection was inshop/dropoff
if (
@ -1717,17 +1816,6 @@ export default {
AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) &&
this.selectedProvider
) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
this.lastSelectedInshopOrDropoffProvider = this.selectedProvider;
}
this.appointmentType = AppointmentTypeStrings.MOBILE;
@ -1746,16 +1834,45 @@ export default {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
}
// make sure a selectedProvider exists
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
if (this.lastSelectedInshopOrDropoffProvider) {
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
} else {
this.updateSelectedProvider();
}
} else {
this.appointmentType = null;
}
this.setSelectedDateToFirstAvailable();
this.setDisplayWaitList();
this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
},
setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate();
},
showRecalAcknowledgementModal() {
if (this.shouldShowRecalAckModal()) {
this.$refs.navbar.removeLoader();
this.$refs.recalAckModal.openModal();
return true;
}
this.closeRecalAcknowledgementPopup();
return false;
},
shouldShowRecalAckModal() {
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
const order = this.$store.getters.order;
const containsRecalPart = containsRecalParts(order.lineItems);
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(order.lineItems);
if (
!containsRecalPart &&
hasPartWithRecalRequirement &&
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
) {
return true;
}
return false;
},
async showMobileFirstModal() {
if (!this.selectedRouteCodeData?.routeCode) {
const pmMobileDays = experimentMixin.methods.getSettingValue(
@ -1767,6 +1884,20 @@ export default {
const noMobileAvailableDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_NO_MOBILE_AVAILABLE_DAYS
);
const mobileFirstPopupRepairSetting = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPAIR
);
const mobileFirstPopupReplaceWithoutMsrSetting =
experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR
);
const mobileFirstPopupReplaceWithMsrSetting =
experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR
);
const order = this.$store.getters.order;
const isRepair = order.damage.isRepair;
let preSelectedMobileAppointment = null;
const todaysDate = getTodayDate();
@ -1783,12 +1914,63 @@ export default {
(new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24);
const shouldExposeMobileFirstAppointment = () => {
return (
const mobileFirstDaysCheck =
(firstMobileAMDate &&
numberOfDaysToFirstMobileAMDate <= noMobileAvailableDays) ||
(firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays)
);
numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays);
debugLog("::: Mobile First Days Check:", mobileFirstDaysCheck);
if (
mobileFirstPopupRepairSetting === "false" &&
mobileFirstPopupReplaceWithoutMsrSetting === "false" &&
mobileFirstPopupReplaceWithMsrSetting === "false"
) {
debugLog(
"::: Mobile First Damage Popup settings are all false, using default check."
);
return mobileFirstDaysCheck;
}
if (isRepair) {
if (mobileFirstPopupRepairSetting === "true") {
debugLog(
"::: Mobile First Popup Repair setting is true, showing mobile first appointment for repair."
);
return mobileFirstDaysCheck;
} else {
debugLog(
"::: Mobile First Popup Repair setting is false, not showing mobile first appointment for repair."
);
return false;
}
}
if (this.isMobileStaticRecalibrationApplicable) {
if (mobileFirstPopupReplaceWithMsrSetting === "true") {
debugLog(
"::: Mobile First Popup Replace with MSR setting is true, showing mobile first appointment for replace with MSR."
);
return mobileFirstDaysCheck;
} else {
debugLog(
"::: Mobile First Popup Replace with MSR setting is false, not showing mobile first appointment for replace with MSR."
);
return false;
}
}
if (mobileFirstPopupReplaceWithoutMsrSetting === "true") {
debugLog(
"::: Mobile First Popup Replace without MSR setting is true, showing mobile first appointment for replace without MSR."
);
return mobileFirstDaysCheck;
} else {
debugLog(
"::: Mobile First Popup Replace without MSR setting is false, not showing mobile first appointment for replace without MSR."
);
return false;
}
};
if (shouldExposeMobileFirstAppointment()) {
@ -1813,23 +1995,34 @@ export default {
}
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;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
preSelectedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt
: null;
!preSelectedMobileAppointment &&
(preSelectedMobileAppointment = firstMobilePMAppt);
: firstMobilePMAppt;
}
this.$refs.mobileFirstModal.setSelectedAppointment(
preSelectedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
if (preSelectedMobileAppointment) {
this.$refs.mobileFirstModal.setSelectedAppointment(
preSelectedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
}
}
}
}
@ -1868,39 +2061,17 @@ export default {
this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction();
},
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
},
},
watch: {
appointmentTypeFromAppointmentTypeQuestion: {
handler(newValue, oldValue) {
this.handleAppointmentTypeChange(newValue);
this.handleAppointmentTypeChange(newValue); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
},
},
selectedDate(newValue, oldValue) {
// Clear time slot selection if date selected changes
const selectedDate = this.getSelectedDateFromStore();
if (oldValue && this.appointmentType === AppointmentTypeStrings.MOBILE) {
oldValue = `${oldValue}-mobile`;
}
const hasValueChanged = newValue !== oldValue;
const isDateDifferent = (newValue || oldValue) !== selectedDate;
if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
}
},
selectedTimeSlotInfo(newValue) {
this.updateFooterButtonText(newValue);
},
},
components: {
funnelHeader,
@ -1913,7 +2084,7 @@ export default {
textBlock,
timeSlotQuestion,
mobileFirstModal,
recalAckModal,
alert,
serviceZipModalQuestion,
appointmentTypeQuestion,

View file

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

View file

@ -1,6 +1,7 @@
<template>
<div class="time-slots-question">
<buttonQuestion
ref="chooseDropOffOrInshop"
v-if="isDropOffAppointmentAvailable"
v-model="selectedAnswerForDropOffOrInshop"
@update:modelValue="dropOffSelectionChanged"
@ -23,8 +24,9 @@
</div>
</buttonQuestion>
<buttonQuestion
ref="buttonQuestion"
v-if="shouldDisplayTimeSlotQuestion"
ref="chooseTimeSlot"
v-if="displayTimeSlotQuestion"
v-model="selectedAnswerForTimeSlots"
@update:modelValue="timeSlotSelectionChanged"
buttonTypeString="timeSlotModalListButton"
:buttonTypeObject="timeSlotModalListButton"
@ -34,10 +36,12 @@
isInshopOrDropOffAppointment ? 'is-inshop-or-drop-off-appointment' : '',
]"
:answers="availableTimeSlots"
:lazyLoad="{ isLazyLoad: !isMobileAppointment, amountToLoad: 10 }"
:lazyLoad="{
isLazyLoad: isLazyLoad,
amountToLoad: lazyLoadAmountToLoad || 10,
}"
groupName="chooseTimeSlot"
textPosition="text-center"
v-model="selectedAnswerForTimeSlots"
questionText="Available times:"
isRequired
validationRules="time-slot-required" />
@ -133,6 +137,7 @@ export default {
isSameDay: Boolean,
displayWaitList: Boolean,
selectedRouteCodeData: Object,
lazyLoadAmountToLoad: Number,
},
data() {
return {
@ -389,12 +394,32 @@ export default {
this.answersForDropOffQuestion[0].value !== PICK_A_TIME_BUTTON_VALUE
);
},
shouldDisplayTimeSlotQuestion() {
displayTimeSlotQuestion() {
return (
this.selectedAnswerForDropOffOrInshop == PICK_A_TIME_BUTTON_VALUE ||
!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: {
updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(selectedRouteCodeData) {
@ -452,6 +477,7 @@ export default {
// }
// },
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
if (!timeSlotsForSelectedDate) return;
return timeSlotsForSelectedDate
.filter((timeSlot) => {
return !isDropOffRouteCode(timeSlot.id);

View file

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

View file

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

View file

@ -198,6 +198,8 @@ export default {
false
);
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// Navigate to the next page
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 () => {
// Arrange
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.previouslyEnteredCarId = "C11111";
@ -111,6 +110,7 @@ describe("vin-lookup.vue", () => {
it("Should not call navigateForward() if zip service returns a non-serviceable flag", async () => {
// Arrange
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 = {
data: {
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 () => {
// Arrange
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.vin = "foo";
wrapper.vm.initialVin = "!foo";
@ -164,6 +166,7 @@ describe("vin-lookup.vue", () => {
isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false,
});
wrapper.vm.pageName = "vin-lookup";
// Act
await wrapper.vm.navigateForward();
@ -172,9 +175,7 @@ describe("vin-lookup.vue", () => {
expect(wrapper.vm.$router.navigateWithSaving).toBeCalledTimes(1);
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
wrapper.vm.$route,
expect.anything(),
expect.anything()
"vin-lookup"
);
});
@ -251,12 +252,14 @@ describe("vin-lookup.vue", () => {
const { wrapper } = setupMocks({});
// Act
const vinLookup = wrapper.findComponent('[data-test="vin-lookup-component"]');
vinLookup.trigger("imageLookupError");
const vinLookup = wrapper.findComponent({ ref: "vinLookupQuestion" });
vinLookup.trigger("image-lookup-error");
await wrapper.vm.$nextTick();
// 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 () => {
@ -277,6 +280,7 @@ describe("vin-lookup.vue", () => {
});
});
/*
describe("getVinFromImage", () => {
test("GetVinFromImage resolves with first valid VIN when any vins are returned.", async () => {
// Arrange
@ -289,7 +293,7 @@ describe("vin-lookup.vue", () => {
const storeMixin = {
methods: {
dispatchStoreAction: lookup,
dispatchStoreActionWithLogging: lookup,
},
};
@ -354,6 +358,7 @@ describe("vin-lookup.vue", () => {
await expect(promise).rejects.toEqual("An error occurred during the lookup.");
});
});
*/
});
function setupMocks({ customMountOptions }) {

View file

@ -393,21 +393,18 @@ export default {
// Check if Service Zip entered is serviceable then save the ZIP info
if (zipCodeData.isServiceable) {
//Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu
if (
this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode ||
!this.$store.getters.order.serviceLocation.zipCodeCtu
) {
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
state: zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
);
}
//Always save the service zip info even if it was not changed;
// if it didn't change it doesn't alter other values and this makes it more consistent
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
state: zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
);
// if no value due to field being optional, blank both phone and email address
if (!this.emailOrSms) {
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 { routeData } from "@/router/constants/routes";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { Variables } from "../constants/analytics";
import { containsRecalParts, getRecalPartNumbers } from "@/helpers/recal-helper";
import { getAmountDue, getSubTotal, getSalesTax } from "@/helpers/pricing-helper.js";
import { partTypeStrings } from "@/constants/part-type-strings";
@ -232,10 +231,41 @@ export default {
(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 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 = {};
sessionData.currentPage = getPageNameFromRouter();
sessionData.currentPage = currentPageName;
sessionData.sid = getSessionIdValue();
sessionData.deviceId = getDeviceIdValue();
sessionData.fmgSessionId = applicationUser?.savedSessionId;
@ -251,7 +281,7 @@ export default {
? "Insurance"
: "Cash";
sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace";
sessionData.productType = glassProducts;
sessionData.productType = allParts;
sessionData.eon = order?.eon;
sessionData.referralNumber = order?.referralNumber;
sessionData.referralSequenceNumber = order?.referralSequenceNumber;
@ -937,9 +967,6 @@ export default {
ValueToLogTypes() {
return ValueToLogTypes;
},
Variables() {
return Variables;
},
},
};

View file

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

View file

@ -51,6 +51,10 @@ export const routeData = {
name: "quote",
path: "/quote",
},
POLICY_INFO: {
name: "policy-info",
path: "/policy-info",
},
INSURANCE_COMPANY: {
name: "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,
maps: [
@ -486,6 +490,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
destinationPageData: routeData.MOBILE_DETAILS,
},
{
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
destinationPageData: routeData.SERVICE_ZIP,
},
],
},
{
@ -565,6 +573,10 @@ const routingTable = function () {
piaError: "true",
},
},
{
scenario: navigationScenarios.CLICKED_PAY_LATER,
destinationPageData: routeData.CONFIRMATION,
},
],
},
{
@ -578,6 +590,17 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD,
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 { queryStrings } from "@/constants/query-strings";
import { externalParameterStatus } from "@/constants/external-parameters";
import { debugLog } from "@/helpers/debug-log-helper";
export async function initializeFromQueryStrings() {
// Get advertiser info
@ -26,12 +27,13 @@ function setupAdvertisersAndAffiliates() {
}
function updateExternalParameterState() {
debugLog("--- updateExternalParameterState ---");
const externalParameterYear = getQuerystringParameter(queryStrings.VEHICLE_YEAR);
const externalParameterMake = getQuerystringParameter(queryStrings.VEHICLE_MAKE);
const externalParameterModel = getQuerystringParameter(queryStrings.VEHICLE_MODEL);
const externalParameterStyle = getQuerystringParameter(queryStrings.VEHICLE_STYLE);
const externalParameterDamage = getQuerystringParameter(queryStrings.VEHICLE_DAMAGE);
const externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP);
let externalParameterZipCode = getQuerystringParameter(queryStrings.SERVICE_ZIP);
const externalParameterEmail = getQuerystringParameter(queryStrings.EMAIL);
const externalParameterIsInsurance = getQuerystringParameter(queryStrings.IS_INSURANCE);
const externalParameterVinSelection = getQuerystringParameter(queryStrings.VIN_SELECTION);
@ -39,6 +41,27 @@ function updateExternalParameterState() {
const externalParameterNumberOfChips = getQuerystringParameter(queryStrings.NUMBER_OF_CHIPS);
const externalParameterSource = getQuerystringParameter(queryStrings.EXPERIMENTS);
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 (
externalParameterYear &&
externalParameterMake &&
@ -51,6 +74,7 @@ function updateExternalParameterState() {
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MAKE, externalParameterMake);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_MODEL, externalParameterModel);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_STYLE, externalParameterStyle);
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_QS_STASH, window.location.search);
}
if (
externalParameterDamage &&
@ -100,6 +124,7 @@ function updateExternalParameterState() {
externalParameterPhoneNumber
);
}
debugLog("--- updateExternalParameterState END ---");
}
// 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;
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(result);
@ -29,6 +52,12 @@ export async function adyenReturnBeforeEnter(to, from) {
const amountDue = getAmountDue(store.getters.order.lineItems);
const ccToken = generateCcToken(sessionInfo);
ccToken.authCode = "831001";
ccToken.cardType = "VS";
ccToken.lastFour = "1111";
ccToken.expMonth = "03";
ccToken.expYear = "2030";
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethod,
@ -53,7 +82,10 @@ export async function adyenReturnBeforeEnter(to, from) {
} catch (error) {
console.log(error);
return {
name: routeData.ERROR.name,
name: routeData.PAYMENT_METHOD.name,
query: {
piaError: "true",
},
replace: true,
};
}
@ -87,12 +119,32 @@ function finalizeAdyenPayment(sessionId, redirectResult) {
onPaymentFailed: (result, component) => {
console.log(`Payment failed`);
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) => {
console.log(`Error occured`);
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: {},

View file

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

View file

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

View file

@ -122,6 +122,7 @@ const getDefaultState = () => {
capabilityQuestionAnswers: null,
dateOfLoss: null,
damageCause: null,
installOemGlass: null,
},
lineItems: {
glassParts: null,
@ -189,6 +190,7 @@ const getDefaultState = () => {
lockToken: null,
settledTenderAmount: 0,
isRecalAckOptIn: false,
isRecalAcknowledgedForScheduling: "",
isMSRFeeApplicable: false,
},
applicationUser: {
@ -342,6 +344,12 @@ export const mutations = {
updateIsRecalAckOptIn(state, isRecalAckOptIn) {
state.order.isRecalAckOptIn = isRecalAckOptIn;
},
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
},
updateIsOemGlassSelected(state, isOemGlassSelected) {
state.order.damage.installOemGlass = isOemGlassSelected;
},
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable;
},
@ -508,6 +516,10 @@ export const mutations = {
externalParameterState.vehicle.style = style;
saveExternalParameterState(externalParameterState);
},
updateExternalParameterQsStash(state, qsStash) {
externalParameterState.qsStash = qsStash;
saveExternalParameterState(externalParameterState);
},
updateExternalParameterIsRepair(state, isRepair) {
externalParameterState.vehicleDamage.isRepair = isRepair;
saveExternalParameterState(externalParameterState);
@ -712,6 +724,8 @@ export const mutations = {
state.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.supportingItems = sessionInformation.order.lineItems.supportingItems;
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? [];
@ -1765,7 +1779,8 @@ export const actions = {
const glassArray = damage.glassToReplace;
const zipCode = order.serviceLocation.zipCode;
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;
// create a new array to avoid mutating state
@ -1781,6 +1796,7 @@ export const actions = {
vin: vin,
serviceType: serviceType,
referralSeqNumber: referralSeqNumber,
parentAccountNumber: parentAccountNumber,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
@ -1836,6 +1852,55 @@ export const actions = {
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 }) {
const carId = payload.carId;
const serviceZipCode = payload.serviceZipCode;
@ -1936,26 +2001,6 @@ export const actions = {
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 }) {
const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
@ -2421,6 +2466,7 @@ export const actions = {
partQuestionAnswers: order.damage.partQuestionAnswers,
moldingQuestionAnswers: order.damage.moldingQuestionAnswers,
capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers,
installOemGlass: order.damage.installOemGlass,
},
lineItems: {
glassParts: lineItems.glassParts,
@ -2927,6 +2973,17 @@ export const actions = {
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn);
},
saveIsRecalAcknowledgedForScheduling(context, isRecalAcknowledgedForScheduling) {
context.commit(
storeMutations.UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
isRecalAcknowledgedForScheduling
);
},
saveIsOemGlassSelected(context, isOemGlassSelected) {
context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected);
},
saveIsMSRFeeApplicable(context, isMSRFeeApplicable) {
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
},
@ -3093,40 +3150,24 @@ export const actions = {
promoCode: lineItem.promoCode ?? null,
}));
const pricedLineItemsFormattedForRequest =
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 order = context.getters.order;
const response = await globalMethods.callHttpClient({
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,
pageNameToLog: pageNameToLog,
});
@ -4105,6 +4146,7 @@ function createExternalParameterDefaultState() {
// create default externalParameter state
const externalParameterDefaultState = {
isExternalParameter: externalParameterStatus.NOT_SET,
qsStash: null,
vehicle: {
year: null,
make: null,

View file

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

View file

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