+
{{ afterpayPrice }}
{{ getInlineAltText(token) }}
-
- {{ token }}
-
+
({
}
return "test";
},
+ hasSettingEqualTo(settingName) {
+ if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
+ return false;
+ }
+ return "test";
+ },
},
}));
@@ -264,6 +270,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -313,6 +325,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -360,6 +378,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -404,6 +428,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -449,6 +479,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -494,6 +530,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -540,6 +582,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -588,6 +636,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -634,6 +688,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -681,6 +741,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
@@ -780,6 +846,12 @@ describe("quote.vue", () => {
lineItems: {
glassParts: ["item", "item2"],
},
+ pageData: jest.fn((page) => {
+ if (page === "quote") {
+ return { saveProgressPopupSkipped: true };
+ }
+ return {};
+ }),
applicationUser: {
experiments: [],
},
diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue
index fd21971b0..42ccfb09a 100644
--- a/src/layouts/quote/quote.vue
+++ b/src/layouts/quote/quote.vue
@@ -46,6 +46,7 @@
@@ -166,6 +167,7 @@ import {
getDeviceIdValue,
} from "@/helpers/heritage-integration/cookie-helper";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
+import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@@ -220,6 +222,36 @@ export default {
);
}
+ // AfterpayBreakoutDisplay Custom Exposure
+ const IGQExperiment = store.getters.applicationUser.experiments.find(
+ (e) => e.universeName === experimentUniverses.RECAL_PRICE_REMOVAL
+ );
+ const hasExposedIGQExperiment = IGQExperiment?.isExposed;
+
+ const afterpayBreakoutDisplayExperiment = store.getters.applicationUser.experiments.find(
+ (e) => e.universeName === experimentUniverses.AFTERPAY_BREAKOUT_DISPLAY
+ );
+ const hasExposedAfterpayBreakoutDisplay = afterpayBreakoutDisplayExperiment?.isExposed;
+ const shouldSendAfterpayBreakoutDisplayExposeRequest =
+ afterpayBreakoutDisplayExperiment &&
+ !hasExposedAfterpayBreakoutDisplay &&
+ !hasExposedIGQExperiment;
+
+ if (shouldSendAfterpayBreakoutDisplayExposeRequest) {
+ baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
+ {
+ userId: getUserIdValue(),
+ deviceId: getDeviceIdValue(),
+ sessionKey: getSessionKeyValue(),
+ pageName: to.query.fmgPage,
+ experiment: afterpayBreakoutDisplayExperiment,
+ },
+ "quote",
+ false
+ );
+ }
+
const promiseResultMap = [
{
resultKey: "cmsContent",
@@ -242,7 +274,9 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
const emailFromStore = store.getters.order.customer.emailAddress;
const isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
- const showSaveProgressPopup = isEmailInStoreOnPageLoad ? false : true;
+ const isPopupSkipped =
+ store.getters.pageData(fmgPageValues.QUOTE).saveProgressPopupSkipped === true;
+ const showSaveProgressPopup = isEmailInStoreOnPageLoad || isPopupSkipped ? false : true;
const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true;
const shouldSkipToInsurance =
getBoolFromString(
@@ -568,6 +602,15 @@ export default {
showRecalDisclaimer() {
return this.isRecalibrationOnOrder && this.isRecalPriceRemove;
},
+ isAfterpayBreakoutDisplay() {
+ return (
+ (!this.isRecalPriceRemove || !this.isRecalibrationOnOrder) &&
+ experimentMixin.methods.hasSettingEqualTo(
+ experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
+ "true"
+ )
+ );
+ },
},
methods: {
getColCount() {
@@ -626,6 +669,15 @@ export default {
async closeSaveProgressPopup() {
this.showSaveProgressPopup = false;
+ await this.dispatchStoreAction(
+ storeActions.SAVE_PAGE_DATA,
+ {
+ page: fmgPageValues.QUOTE,
+ data: { saveProgressPopupSkipped: true },
+ },
+ false
+ );
+
if (this.skipToInsurance) {
// skip ahead now if in IGQ skip experiment
await this.skip(true, externalParamPackageLabels.GLASS_ONLY);
diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue
index 68f031768..64c075408 100644
--- a/src/layouts/quote/service-package-question/service-package-question.vue
+++ b/src/layouts/quote/service-package-question/service-package-question.vue
@@ -4,7 +4,7 @@
:answers="servicePackageAnswers"
:groupName="groupName"
buttonTypeString="servicePackageRadio"
- :buttonTypeObject="hasPricingByDay() ? servicePackageRadioExperiment : servicePackageRadio"
+ :buttonTypeObject="getButtonTypeObject()"
v-model="selectedPackageName"
:validationRules="validationRules"
:isRequired="isRequired"
@@ -18,6 +18,7 @@ import baseMixin from "@/mixins/base-mixin.js";
import { processIfStatements } from "@/helpers/cms-content-helper";
import servicePackageRadio from "./service-package-radio/service-package-radio";
import servicePackageRadioExperiment from "@/experiment-components/service-package-radio";
+import servicePackageRadioAfterpayExperiment from "@/experiment-components/service-package-radio-for-afterpay";
import { partTypeStrings } from "@/constants/part-type-strings";
import { packageNames } from "@/constants/package-names";
import {
@@ -61,6 +62,7 @@ export default {
return {
servicePackageRadio: servicePackageRadio,
servicePackageRadioExperiment: servicePackageRadioExperiment,
+ servicePackageRadioAfterpayExperiment: servicePackageRadioAfterpayExperiment,
selectedPackageName: null,
};
},
@@ -156,6 +158,7 @@ export default {
Text: this.isInsuranceSelected
? false
: "Special: Save $" + this.getServicePackageDiscountPrice(),
+ isInsuranceSelected: this.isInsuranceSelected,
},
}));
this.$emit("currentNumberOfPackages", modifiedAnswers.length);
@@ -237,6 +240,24 @@ export default {
"true"
);
},
+ isAfterpayBreakoutDisplay() {
+ return (
+ (!this.shouldHideRecalibration || !this.isRecalibrationOnOrder) &&
+ experimentMixin.methods.hasSettingEqualTo(
+ experimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY,
+ "true"
+ )
+ );
+ },
+ getButtonTypeObject() {
+ if (this.isAfterpayBreakoutDisplay()) {
+ return this.servicePackageRadioAfterpayExperiment;
+ }
+ if (this.hasPricingByDay()) {
+ return this.servicePackageRadioExperiment;
+ }
+ return this.servicePackageRadio;
+ },
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "HeaderText");
},
diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js b/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js
index 63242df22..8ff7c9b1c 100644
--- a/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js
+++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.spec.js
@@ -59,7 +59,7 @@ afterEach(() => {
});
describe("appointment-type-question.vue", () => {
- it("Should display all options if both in-shop and mobile are available", async () => {
+ it("Should display all options if in-shop, mobile, and dropoff are available", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
mixins: [mockMixin],
@@ -67,6 +67,7 @@ describe("appointment-type-question.vue", () => {
cmsWidgetName: cmsWidgetName,
isServiceableInshop: true,
isServiceableMobile: true,
+ isServiceableDropoff: true,
},
mountOptions: {
attachTo: document.body,
@@ -99,7 +100,7 @@ describe("appointment-type-question.vue", () => {
]);
});
- it("Should display only the In-Shop and Drop-Off answers when only in-shop service is available", async () => {
+ it("Should display only the In-Shop answer when only in-shop service is available", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
mixins: [mockMixin],
@@ -107,6 +108,34 @@ describe("appointment-type-question.vue", () => {
cmsWidgetName: cmsWidgetName,
isServiceableInshop: true,
isServiceableMobile: false,
+ isServiceableDropoff: false,
+ },
+ mountOptions: {
+ attachTo: document.body,
+ },
+ });
+
+ // Assert
+ expect(wrapper.vm.answersToDisplay).toEqual([
+ {
+ AnswerImageUrl: "",
+ Name: "Inshop",
+ SubText: "",
+ SubWidgetName: "",
+ Text: "In-shop",
+ },
+ ]);
+ });
+
+ it("Should display only the In-Shop and Drop-Off answers when mobile service is not available", async () => {
+ // Arrange/Act
+ const { wrapper } = setupMocks({
+ mixins: [mockMixin],
+ props: {
+ cmsWidgetName: cmsWidgetName,
+ isServiceableInshop: true,
+ isServiceableMobile: false,
+ isServiceableDropoff: true,
},
mountOptions: {
attachTo: document.body,
@@ -140,6 +169,7 @@ describe("appointment-type-question.vue", () => {
cmsWidgetName: cmsWidgetName,
isServiceableInshop: false,
isServiceableMobile: true,
+ isServiceableDropoff: false,
},
mountOptions: {
attachTo: document.body,
@@ -172,6 +202,7 @@ describe("appointment-type-question.vue", () => {
cmsWidgetName: cmsWidgetName,
isServiceableInshop: true,
isServiceableMobile: true,
+ isServiceableDropoff: true,
},
mountOptions: {
attachTo: document.body,
@@ -205,6 +236,7 @@ describe("appointment-type-question.vue", () => {
cmsWidgetName: cmsWidgetName,
isServiceableInshop: false,
isServiceableMobile: false,
+ isServiceableDropoff: false,
},
mountOptions: {
attachTo: document.body,
diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue
index 64bf159ff..4c1c0d031 100644
--- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue
+++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue
@@ -32,6 +32,7 @@ export default {
cmsWidgetName: String,
isServiceableMobile: Boolean,
isServiceableInshop: Boolean,
+ isServiceableDropoff: Boolean,
mobileFeeApplies: Boolean,
},
computed: {
@@ -45,7 +46,7 @@ export default {
const shouldShowMobile = this.isServiceableMobile;
const shouldShowInshop = this.isServiceableInshop;
const shouldShowDropoff =
- this.isServiceableInshop && !this.$store.getters.damage.isRepair;
+ this.isServiceableDropoff && !this.$store.getters.damage.isRepair;
var answers = this.answersFromCms
? this.answersFromCms.filter((answer) => {
@@ -77,6 +78,11 @@ export default {
isMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop;
},
+ isInshopOnly() {
+ return (
+ this.isServiceableInshop && !this.isServiceableMobile && !this.isServiceableDropoff
+ );
+ },
},
watch: {
answersToDisplay: {
@@ -86,7 +92,7 @@ export default {
newValue.length == 1 &&
newValue.findIndex((answer) => answer.Name == "Mobile") != -1
) {
- this.selectedValues = "Mobile";
+ this.selectedValue = "Mobile";
}
},
immediate: true,
@@ -94,7 +100,14 @@ export default {
isMobileOnly: {
handler(newValue) {
if (newValue) {
- this.selectedValues = "Mobile";
+ this.selectedValue = "Mobile";
+ }
+ },
+ },
+ isInshopOnly: {
+ handler(newValue) {
+ if (newValue) {
+ this.selectedValue = "Inshop";
}
},
},
diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js
index 7a9ac00d0..106ed892c 100644
--- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js
+++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js
@@ -149,6 +149,23 @@ export async function getAvailabilityRating(
return shopStatus;
}
+export async function getClosestApplicableShops(serviceZipCode, carId, pageNameToLog) {
+ if (!serviceZipCode) {
+ return null;
+ }
+
+ const closestShops = await baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
+ {
+ zip: serviceZipCode,
+ carId: carId,
+ },
+ pageNameToLog
+ );
+
+ return closestShops.data;
+}
+
export async function getZipCodeData(serviceZipCode) {
return await baseMixin.methods.getZipCodeData(serviceZipCode, "service-location");
}
diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue
index 56c76bb2d..7b3163c87 100644
--- a/src/layouts/service-location/service-location.vue
+++ b/src/layouts/service-location/service-location.vue
@@ -15,6 +15,8 @@
editScreenReaderTextCmsWidgetName="ScreenReaderZipEditWidget"
v-model="serviceZipCodeQuestion"
ref="serviceZipCodeQuestion"
+ :carId="carId"
+ :isVehicleHeavyTruck="isVehicleHeavyTruck"
:mobileFeePart="mobileFeePart"
@updated-mobile-fee-part="setMobileFeePart"
@updated-recycle-fee-part="setRecycleFeePart"
@@ -74,6 +76,7 @@
v-show="isAppointmentTypeDisplayed"
:isServiceableMobile="isServiceableMobile"
:isServiceableInshop="isServiceableInshop"
+ :isServiceableDropoff="isServiceableDropoff"
:isDisplayed="isAppointmentTypeDisplayed"
:mobileFeeApplies="mobileFeeApplies"
ref="appointmentTypeQuestion"
@@ -186,12 +189,16 @@ export default {
return {
streetAddress: this.getServiceAddressFromStore(),
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
+ carId: this.getCarIdfromStore(),
city: this.getServiceCityFromStore(),
state: this.getServiceStateFromStore(),
zipCode: this.getServiceZipCodeFromStore(),
+ isVehicleHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
isGlassServiceableInshop: null,
isRecalibrationServiceableInshop: null,
+ isGlassServiceableDropoff: null,
+ isRecalibrationServiceableDropoff: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(),
@@ -350,6 +357,13 @@ export default {
return this.isGlassServiceableInshop;
}
},
+ isServiceableDropoff() {
+ if (this.isRecalibrationServiceableDropoff !== null) {
+ return this.isGlassServiceableDropoff && this.isRecalibrationServiceableDropoff;
+ } else {
+ return this.isGlassServiceableDropoff;
+ }
+ },
isShopQuestionDisplayed() {
return (
this.selectedAppointmentType === "Inshop" ||
@@ -514,6 +528,9 @@ export default {
});
}
},
+ getCarIdfromStore() {
+ return store.getters.vehicle.carId;
+ },
getServiceAddressFromStore() {
return store.getters.order.serviceLocation.address;
},
@@ -529,6 +546,9 @@ export default {
getServiceZipCodeFromStore() {
return store.getters.order.serviceLocation.zipCode;
},
+ getIsVehicleHeavyTruckFromStore() {
+ return store.getters.order.vehicle?.isBigTruck ?? false;
+ },
getIsVehicleProtectedFromStore() {
return store.getters.order.serviceLocation.isVehicleProtected;
},
@@ -549,6 +569,9 @@ export default {
this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop;
this.isRecalibrationServiceableInshop =
serviceabilityDetails.isRecalibrationServiceableInshop;
+ this.isGlassServiceableDropoff = serviceabilityDetails.isGlassServiceableDropoff;
+ this.isRecalibrationServiceableDropoff =
+ serviceabilityDetails.isRecalibrationServiceableDropoff;
this.isGlassServiceableMobile = serviceabilityDetails.isGlassServiceableMobile;
this.isRecalibrationServiceableMobile =
serviceabilityDetails.isRecalibrationServiceableMobile;
diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue
index a2ab4134e..5e7aa73e4 100644
--- a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue
+++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue
@@ -33,6 +33,13 @@
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
+
@@ -48,6 +55,7 @@ import {
getPricedRecycleFeePart,
getServiceabilityDetails,
getBillToAccountNumber,
+ getClosestApplicableShops,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
export default {
@@ -63,6 +71,7 @@ export default {
internalModel: this.copyModel(this.modelValue),
serviceZipCodeTextInputId: "",
displayInvalidZipAlert: false,
+ displayNoServiceAlert: false,
};
},
props: {
@@ -73,6 +82,11 @@ export default {
zipCode: "",
}),
},
+ carId: String,
+ isVehicleHeavyTruck: {
+ type: Boolean,
+ default: false,
+ },
mobileFeePart: {
type: Object,
default: () => ({}),
@@ -107,6 +121,7 @@ export default {
methods: {
resetAlerts() {
this.displayInvalidZipAlert = false;
+ this.displayNoServiceAlert = false;
},
resetsOnZipInput() {
this.resetAlerts();
@@ -155,6 +170,22 @@ export default {
this.focusOnZipInput();
this.resetModalButtonStyle();
} else {
+ if (this.isVehicleHeavyTruck) {
+ // check the location endpoint to verify this zip can service a heavy truck
+ const closestShops = await getClosestApplicableShops(
+ this.internalModel.zipCode,
+ this.carId,
+ "service-location"
+ );
+
+ if (!closestShops || closestShops.providers?.length === 0) {
+ this.displayNoServiceAlert = true;
+ this.focusOnZipInput();
+ this.resetModalButtonStyle();
+ return null;
+ }
+ }
+
this.internalModel.state = zipCodeData.state;
this.internalModel.zipCodeCtu = zipCodeData.zipCodeCtu;
diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue
index 4a7818834..417a3b4c4 100644
--- a/src/layouts/service-location/shop-question/shop-question.vue
+++ b/src/layouts/service-location/shop-question/shop-question.vue
@@ -179,12 +179,12 @@ export default {
}
});
- this.pushEventToGA(
- this.GaCategories.SERVICE_LOCATION,
- gaAction,
- shops.join(","),
- true
- );
+ var joinedShops = shops.join(",");
+ if (!joinedShops) {
+ joinedShops = "no-shops";
+ }
+
+ this.pushEventToGA(this.GaCategories.SERVICE_LOCATION, gaAction, joinedShops, true);
}
if (this.answers.length === 0) {
diff --git a/src/layouts/service-zip/service-zip.spec.js b/src/layouts/service-zip/service-zip.spec.js
index 3346970cf..e7cd68e3d 100644
--- a/src/layouts/service-zip/service-zip.spec.js
+++ b/src/layouts/service-zip/service-zip.spec.js
@@ -47,6 +47,7 @@ function resetMockStoreData() {
registration: {
licensePlate: null,
},
+ isServiceable: true,
},
serviceLocation: {
address: null,
@@ -187,6 +188,7 @@ function applyMockStoreDataToGetters() {
policy: mockStoreData.policy,
externalParameterServiceZip: mockStoreData.externalParameterServiceZip,
emailOrSms: mockStoreData.customer.emailAddress,
+ vehicle: mockStoreData.vehicle,
};
store.state.order = mockStoreData;
store.state.applicationUser.experiments = mockExperimentSettings;
@@ -413,6 +415,33 @@ describe("service-zip.vue", () => {
expect(wrapper.vm.displayInvalidZipAlert).toBe(false);
expect(wrapper.vm.displayNonServiceableZipAlert).toBe(false);
});
+ test("should show the AlertNoService when displayNoServiceAlert is true", async () => {
+ // Arrange
+ // no changes to store
+ applyMockStoreDataToGetters();
+
+ const wrapper = setupMocks({});
+ wrapper.vm.serviceZipCode = "12345";
+
+ // Act
+ wrapper.vm.displayNoServiceAlert = true;
+
+ // Check if the AlertNoService component is rendered
+ expect(wrapper.vm.displayNoServiceAlert).toBe(true);
+ });
+ test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
+ // Arrange
+ // no changes to store
+ applyMockStoreDataToGetters();
+
+ const wrapper = setupMocks({});
+ wrapper.vm.serviceZipCode = "12345";
+ //ACT
+ wrapper.vm.displayNoServiceAlert = false;
+
+ // Check if the AlertNoService component is rendered
+ expect(wrapper.vm.displayNoServiceAlert).toBe(false);
+ });
});
});
@@ -509,6 +538,16 @@ function setupMocks({ customMountOptions, customZipQuery, customZipDataResponse
];
const wrapper = shallowMount(serviceZip, mountOptions);
+ wrapper.vm.findClosestApplicableShops = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ providers: [
+ { id: 1, name: "Shop 1" },
+ { id: 2, name: "Shop 2" },
+ ],
+ },
+ });
+ });
return wrapper;
}
diff --git a/src/layouts/service-zip/service-zip.vue b/src/layouts/service-zip/service-zip.vue
index 9528d50bc..7d53d105d 100644
--- a/src/layouts/service-zip/service-zip.vue
+++ b/src/layouts/service-zip/service-zip.vue
@@ -47,10 +47,18 @@
v-if="displayNonServiceableZipAlert"
alertClass="alert-danger" />
+
+
@@ -110,8 +118,11 @@ export default {
return {
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
emailOrSms: this.getEmailOrSmsFromStore(),
+ carId: this.getCarIdfromStore(),
+ isHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
displayInvalidZipAlert: false,
displayNonServiceableZipAlert: false,
+ displayNoServiceAlert: false,
};
},
@@ -184,6 +195,19 @@ export default {
arePagePrerequisitesValid() {
return store.getters.damage.isRepair || store.getters.damage.glassToReplace?.length > 0;
},
+ getCarIdfromStore() {
+ return store.getters.vehicle.carId;
+ },
+ getIsVehicleHeavyTruckFromStore() {
+ return store.getters.isHeavyTruckVehicle;
+ },
+ async findClosestApplicableShops() {
+ return await this.dispatchStoreActionWithLogging(
+ storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
+ { zip: this.serviceZipCode, carId: this.carId },
+ "service-zip"
+ );
+ },
async backButtonAction() {
const skipVin = await skipVinLookup();
// route to move backwards
@@ -252,6 +276,16 @@ export default {
return this.$refs.navbar.removeLoader();
}
this.displayNonServiceableZipAlert = false;
+ if (this.isHeavyTruck) {
+ const closestShops = await this.findClosestApplicableShops(
+ this.serviceZipCode,
+ this.carId
+ );
+ this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
+ if (this.displayNoServiceAlert) {
+ return this.$refs.navbar.removeLoader();
+ }
+ }
const payment = this.$store.getters.payment;
const policy = this.$store.getters.policy;
@@ -309,6 +343,7 @@ export default {
watch: {
serviceZipCode() {
this.displayNonServiceableZipAlert = false;
+ this.displayNoServiceAlert = false;
},
},
components: {
diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue
index 5f769b9b7..bad52fb02 100644
--- a/src/layouts/vehicle/vehicle.vue
+++ b/src/layouts/vehicle/vehicle.vue
@@ -48,6 +48,19 @@
validationRules="style-required"
placeHolderText="Select style"
customDropdownId="styleQuestionField" />
+
+
+
@@ -557,4 +636,9 @@ export default {
margin-top: 1.5rem;
margin-bottom: 0;
}
+.separator-line {
+ grid-area: 2/1/2/8;
+ border-top: 1px solid $gray-500;
+ margin: 0.5rem 0;
+}
diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js
index 39aa0f008..458f9fc48 100644
--- a/src/layouts/vin-lookup/vin-lookup.spec.js
+++ b/src/layouts/vin-lookup/vin-lookup.spec.js
@@ -32,6 +32,7 @@ jest.mock("@/store", () => ({
damage: {
glassToReplace: "windshield",
},
+ isHeavyTruckVehicle: true,
},
}));
@@ -257,6 +258,39 @@ describe("vin-lookup.vue", () => {
// Assert
expect(wrapper.findAllComponents({ name: "alert" }).length).toBe(1);
});
+
+ test("should show the AlertNoService when displayNoServiceAlert is true", async () => {
+ const { wrapper } = setupMocks({
+ isZipServiceable: true,
+ displayNoServiceAlert: true,
+ lookupVinbyAddressResponse: {
+ isStatePermissible: true,
+ vinVehicles: [], // Return no vehicles
+ },
+ });
+ // Set displayNoServiceAlert to true
+ await wrapper.setData({ displayNoServiceAlert: true });
+
+ // Check if the AlertNoService component is rendered
+ const alertNoService = wrapper.findComponent({ ref: "AlertNoService" });
+ expect(alertNoService.exists()).toBe(true);
+ });
+ test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
+ const { wrapper } = setupMocks({
+ isZipServiceable: true,
+ displayNoServiceAlert: false,
+ lookupVinbyAddressResponse: {
+ isStatePermissible: true,
+ vinVehicles: [], // Return no vehicles
+ },
+ });
+ // Ensure displayNoServiceAlert is false
+ await wrapper.setData({ displayNoServiceAlert: false });
+
+ // Check if the AlertNoService component is not rendered
+ const alertNoService = wrapper.findComponent({ ref: "AlertNoService" });
+ expect(alertNoService.exists()).toBe(false);
+ });
});
describe("getVinFromImage", () => {
diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue
index f913993b2..788425861 100644
--- a/src/layouts/vin-lookup/vin-lookup.vue
+++ b/src/layouts/vin-lookup/vin-lookup.vue
@@ -71,7 +71,8 @@
vinPopulatedOnPageLoad &&
isInsuranceVerified &&
!displayInvalidZipAlert &&
- !displayNonServiceableZipAlert
+ !displayNonServiceableZipAlert &&
+ !displayNoServiceAlert
"
alertClass="alert-success" />
@@ -107,14 +108,23 @@
vinPopulatedOnPageLoad &&
!isInsuranceVerified &&
!displayInvalidZipAlert &&
- !displayNonServiceableZipAlert
+ !displayNonServiceableZipAlert &&
+ !displayNoServiceAlert
"
alertClass="alert-success" />
+
+
@@ -197,6 +207,8 @@ export default {
vin: this.getVinFromStore(),
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
emailOrSms: this.getEmailOrSmsFromStore(),
+ carId: this.getCarIdfromStore(),
+ isHeavyTruck: this.getIsVehicleHeavyTruckFromStore(),
isCarIdDifferent: false,
customAlertData: {},
previouslyEnteredCarId: "",
@@ -207,6 +219,7 @@ export default {
displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false,
displayVinScanFailedAlert: false,
+ displayNoServiceAlert: false,
};
},
methods: {
@@ -222,6 +235,12 @@ export default {
getZipFromStore() {
return this.$store.getters.order.serviceLocation.zipCode;
},
+ getCarIdfromStore() {
+ return store.getters.vehicle.carId;
+ },
+ getIsVehicleHeavyTruckFromStore() {
+ return store.getters.isHeavyTruckVehicle;
+ },
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA(
@@ -245,6 +264,13 @@ export default {
);
}
},
+ async findClosestApplicableShops() {
+ return await this.dispatchStoreActionWithLogging(
+ storeActions.GET_CLOSEST_APPLICABLE_SHOPS,
+ { zip: this.serviceZipCode, carId: this.carId },
+ "vin-lookup"
+ );
+ },
async forwardButtonAction() {
this.resetAlerts();
@@ -277,6 +303,17 @@ export default {
return this.$refs.navbar.removeLoader();
}
this.displayInvalidZipAlert = false;
+ if (this.isHeavyTruck) {
+ // If a VIN has already been found. Validate the Service Zip (in case of changes)
+ var closestShops = await this.findClosestApplicableShops(
+ this.serviceZipCode,
+ this.carId
+ );
+ this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
+ if (this.displayNoServiceAlert) {
+ return this.$refs.navbar.removeLoader();
+ }
+ }
// If either lookup fails, remove the loader and stop processing the page.
if (!resultMap.vehicleLookupResponse || !resultMap.zipCodeData.isServiceable) {
@@ -394,6 +431,15 @@ export default {
);
}
+ closestShops = await this.findClosestApplicableShops(
+ this.serviceZipCode,
+ this.carId
+ );
+ this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
+ if (this.displayNoServiceAlert) {
+ return this.$refs.navbar.removeLoader();
+ }
+
// 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);
@@ -476,6 +522,7 @@ export default {
this.displayInvalidZipAlert = false;
this.displayVinNotFoundAlert = false;
this.displayVinScanFailedAlert = false;
+ this.displayNoServiceAlert = false;
},
},
mounted() {
@@ -552,6 +599,7 @@ export default {
},
serviceZipCode() {
this.displayNonServiceableZipAlert = false;
+ this.displayNoServiceAlert = false;
},
},
components: {
diff --git a/src/router/index.js b/src/router/index.js
index d421ea4ad..eddc14e16 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -59,6 +59,7 @@ const routes = [
log(` --from.redirectedFrom:>${JSON.stringify(from.redirectedFrom)}<`, "");
await analyticsMixin.methods.validateSession();
+ var fromHeritage = getQuerystringParameter(queryStrings.FROM_HERITAGE);
var offerQuote = getQuerystringParameter(queryStrings.OFFER_QUOTE);
log(" --offerQuote: ", offerQuote);
// after session is validated, remove the fromHeritage querystring if it exists so session expiration works
@@ -185,7 +186,7 @@ const routes = [
}
//Update CASH tab when return from heritage and have offerQuote param
- if (to.query.fmgPage !== fmgPageValues.QUOTE && offerQuote) {
+ if (offerQuote === "true" && fromHeritage === "true") {
store.commit(storeMutations.UPDATE_IS_INSURANCE, false);
}
diff --git a/src/store/index.js b/src/store/index.js
index 97a31068e..c36af8bb0 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -66,9 +66,13 @@ const getDefaultState = () => {
make: null,
model: null,
style: null,
+ vehicleSubType: null,
+ vehicleSpecialClass: null,
+ isBigTruck: false,
carId: null,
category: null,
vin: null,
+ canSafeliteService: null,
imageUrl: null,
imageVifNumber: null,
imageColor: null,
@@ -216,12 +220,24 @@ export const mutations = {
updateStyle(state, style) {
state.order.vehicle.style = style;
},
+ updateVehicleSubType(state, vehicleSubType) {
+ state.order.vehicle.vehicleSubType = vehicleSubType;
+ },
+ updateVehicleSpecialClass(state, vehicleSpecialClass) {
+ state.order.vehicle.vehicleSpecialClass = vehicleSpecialClass;
+ },
+ updateIsBigTruck(state, isBigTruck) {
+ state.order.vehicle.isBigTruck = isBigTruck;
+ },
updateCarId(state, carId) {
state.order.vehicle.carId = carId;
},
updateVehicleCategory(state, category) {
state.order.vehicle.category = category;
},
+ updateVehicleCanSafeliteService(state, canSafeliteService) {
+ state.order.vehicle.canSafeliteService = canSafeliteService;
+ },
updateVehicleImageUrl(state, imageUrl) {
state.order.vehicle.imageUrl = imageUrl;
},
@@ -662,6 +678,9 @@ export const mutations = {
imageUrl: sessionInformation.order.vehicle?.imageUrl,
imageVifNumber: sessionInformation.order.vehicle?.imageVifNumber,
imageColor: sessionInformation.order.vehicle?.imageVifColor,
+ vehicleSubType: sessionInformation.order.vehicle?.vehicleSubType,
+ vehicleSpecialClass: sessionInformation.order.vehicle?.vehicleSpecialClass,
+ isBigTruck: sessionInformation.order.vehicle?.isBigTruck,
});
state.order.damage.glassToReplace = sessionInformation.order.damage.glassToReplace;
@@ -1248,6 +1267,15 @@ export const actions = {
});
},
+ getClosestApplicableShops(context, { payload: { zip, carId }, pageNameToLog }) {
+ return globalMethods.callHttpClient({
+ methods: endpoints.ClosestApplicableShops.method,
+ endpoint: `${endpoints.ClosestApplicableShops.url}/${zip}/${carId}`,
+ logApiCall: true,
+ pageNameToLog: pageNameToLog,
+ });
+ },
+
// Dependency Actions
resetVehicleStateAndDependencies(context) {
context.commit(storeMutations.RESET_VEHICLE_STATE);
@@ -1835,6 +1863,8 @@ export const actions = {
endPoint += `&${glassPieces}`;
}
+ endPoint += `&isHeavyTruckVehicle=${context.getters.order.vehicle.isBigTruck}`;
+
return globalMethods.callHttpClient({
method: endpoints.GetServiceabilityDetails.method,
endpoint: endPoint,
@@ -1861,6 +1891,8 @@ export const actions = {
if (windshieldPartWithRecal) {
url += `/${windshieldPartWithRecal.partNumber}`;
}
+ //heavy truck vehicle
+ url += `/${context.getters.order.vehicle.isBigTruck}`;
return globalMethods.callHttpClient({
method: endpoints.GetProviders.method,
@@ -2135,6 +2167,9 @@ export const actions = {
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
+ subType: vehicle.vehicleSubType,
+ specialClass: vehicle.vehicleSpecialClass,
+ isBigTruck: vehicle.isBigTruck,
isInsurance: order.payment.isInsurance ?? false,
isVerified: order.payment.insuranceCoverage.isVerified ?? false,
lineItems: {
@@ -2188,6 +2223,9 @@ export const actions = {
model: vehicle.model,
style: vehicle.style,
vin: vehicle.vin,
+ vehicleSubType: vehicle.vehicleSubType,
+ vehicleSpecialClass: vehicle.vehicleSpecialClass,
+ isBigTruck: vehicle.isBigTruck,
registration: {
firstName: vehicle.registration.firstName,
lastName: vehicle.registration.lastName,
@@ -2398,7 +2436,21 @@ export const actions = {
// Vehicle
saveVehicle(
context,
- { year, make, model, style, carId, category, imageUrl, imageVifNumber, imageVifColor }
+ {
+ year,
+ make,
+ model,
+ style,
+ vehicleSubType,
+ vehicleSpecialClass,
+ isBigTruck,
+ carId,
+ category,
+ canSafeliteService,
+ imageUrl,
+ imageVifNumber,
+ imageVifColor,
+ }
) {
if (
context.state.order.vehicle.year != year ||
@@ -2414,8 +2466,12 @@ export const actions = {
context.commit(storeMutations.UPDATE_MAKE, make);
context.commit(storeMutations.UPDATE_MODEL, model);
context.commit(storeMutations.UPDATE_STYLE, style);
+ context.commit(storeMutations.UPDATE_VEHICLE_SUBTYPE, vehicleSubType);
+ context.commit(storeMutations.UPDATE_VEHICLE_SPECIAL_CLASS, vehicleSpecialClass);
+ context.commit(storeMutations.UPDATE_IS_BIGTRUCK, isBigTruck);
context.commit(storeMutations.UPDATE_CAR_ID, carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, category);
+ context.commit(storeMutations.UPDATE_VEHICLE_CAN_SAFELITE_SERVICE, canSafeliteService);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor);
@@ -3161,8 +3217,8 @@ export const actions = {
context.commit(storeMutations.UPDATE_CUSTOMER_WAITLIST_REQUESTED, waitListRequested);
},
- savePageData(context, emailOrSms) {
- context.commit(storeMutations.UPDATE_PAGE_DATA, emailOrSms);
+ savePageData(context, pageData) {
+ context.commit(storeMutations.UPDATE_PAGE_DATA, pageData);
},
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue
index 4b66023e0..72f7c53e0 100644
--- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue
+++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue
@@ -136,7 +136,7 @@ export default {
color: $white;
background: linear-gradient(84.45deg, #125b7e 0%, #3b8fb8 100%);
border-radius: 0.5rem;
- z-index: 5;
+ z-index: 2;
}
&:checked:focus + .list-button-horizontal-content {