diff --git a/src/constants/experiments.js b/src/constants/experiments.js
index b702b11bb..18ca6387d 100644
--- a/src/constants/experiments.js
+++ b/src/constants/experiments.js
@@ -8,6 +8,7 @@ const experimentSettings = {
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
+ IS_EMAIL_OPTIONAL: "isEmailOptional",
};
const experimentTriggers = {
diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue
index 11f232135..cc13231e1 100644
--- a/src/digital-components/textbox-question/textbox-question.vue
+++ b/src/digital-components/textbox-question/textbox-question.vue
@@ -121,6 +121,11 @@ export default {
hideInput: Boolean,
centerErrorMessage: Boolean,
keyDownHandler: Function,
+ addOptionalText: {
+ type: Boolean,
+ default: false,
+ isRequired: false,
+ },
},
setup(props) {
const uuid = uuidv4();
@@ -193,7 +198,9 @@ export default {
},
computed: {
questionText() {
- return this.getCmsContent(this.cmsWidgetName, "QuestionText");
+ return this.addOptionalText
+ ? this.getCmsContent(this.cmsWidgetName, "QuestionText") + " (optional)"
+ : this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
value: {
get: function () {
diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue
index 2d268e81a..76847f3e9 100644
--- a/src/fmg-components/cart/cart.vue
+++ b/src/fmg-components/cart/cart.vue
@@ -13,78 +13,107 @@
{{ getFormattedAmount("", amountDue) }}
-
-
-
-
- {{ getFormattedAmount("", packagePrice) }}
-
-
+
+
+
+
+
+ {{ getFormattedAmount("", packagePrice) }}
+
+
+ {{ getFormattedAmount("", packagePriceWithoutDiscount) }}
+
+
-
-
- {{ cartItem.name }}
-
-
- {{ cartItem.name }}
-
-
-
+
+
+ <{{ cartItem.name
+ }}>
+
+
+ {{ cartItem.name }}
+
+
+
-
-
- {{ cartItem.name }}
-
-
-
- {{ recycleFeeCartItem.name }}
-
- {{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}
-
+
+
+ {{ cartItem.name }}
+
+
+
+ {{ recycleFeeCartItem.name }}
+
+ {{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}
+
-
-
- {{ subtotalText }}{{ getFormattedAmount("", subTotal) }}
-
-
- {{ salesTaxText }}{{ getFormattedAmount("", salesTax) }}
-
-
- {{ amountPaidText }}{{ getFormattedAmount("", amountPaid) }}
-
-
-
{{ amountDueText }}{{ getFormattedAmount("", amountDue) }}
+
+
+ {{ subtotalText }}{{ getFormattedAmount("", subTotal) }}
+
+
+ {{ salesTaxText }}{{ getFormattedAmount("", salesTax) }}
+
+
+ {{ amountPaidText }}{{ getFormattedAmount("", amountPaid) }}
+
+
+ {{ amountDueText }}{{ getFormattedAmount("", amountDue) }}
+
+
+
+ Promo code <{{ promoCode }}> applied
+
@@ -96,6 +125,7 @@
import textLink from "@/ux-components/text-link/text-link";
import textBlock from "@/digital-components/text-block/text-block";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
+import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question";
// Mixins
import baseMixin from "@/mixins/base-mixin.js";
@@ -149,6 +179,17 @@ export default {
return subTotal;
},
+ getPromoDiscounts() {
+ const promoItems = this.promoCartItems;
+
+ let subTotal = 0;
+
+ promoItems?.forEach((item) => {
+ subTotal += item.subTotal;
+ });
+
+ return subTotal;
+ },
getVapsCartItemsForSelectedPackage(packageName) {
const packageContentTypes = getPackageContents(
this.glassToReplace,
@@ -167,6 +208,12 @@ export default {
});
});
+ if (this.promoCartItems) {
+ this.promoCartItems.forEach((promoCartItem) => {
+ vapsCartItemsForSelectedPackage.push(promoCartItem);
+ });
+ }
+
return vapsCartItemsForSelectedPackage;
},
getCmsContentForVapsType(vapsType) {
@@ -192,6 +239,16 @@ export default {
this.$emit("update:modelValue", this.lineItems);
},
+ getPromoCodeList() {
+ if (this.$refs["promoModalQuestion"]) {
+ return this.$refs["promoModalQuestion"].getPromoCodeList();
+ }
+ return [];
+ },
+ handleAddedPromo(lineItems) {
+ // NOTE: these lineItems are passed from promo-modal-question
+ this.$emit("update:modelValue", lineItems);
+ },
},
computed: {
screenReaderTotalAmountDueText() {
@@ -313,6 +370,12 @@ export default {
return packagePrice;
},
+ packagePriceWithoutDiscount() {
+ let fullPrice = this.packagePrice;
+ let discount = this.getPromoDiscounts();
+ fullPrice += Math.abs(discount);
+ return fullPrice;
+ },
isRepair() {
if (!this.damage) {
return;
@@ -540,7 +603,6 @@ export default {
return cartItem;
},
-
suppliesRepairCartItemName() {
return this.getCmsContent("SuppliesRepairTextWidget", "Text");
},
@@ -578,7 +640,6 @@ export default {
return cartItem;
},
-
mobileFeeCartItemName() {
return this.getCmsContent("MobileServiceTextWidget", "Text");
},
@@ -734,7 +795,6 @@ export default {
return promoCartItems;
},
-
removeLinkText() {
return this.getCmsContent("RemoveCartItemTextWidget", "Text");
},
@@ -795,6 +855,7 @@ export default {
textBlock,
textLink,
contentGroupModal,
+ promoModalQuestion,
},
};
@@ -824,12 +885,14 @@ export default {
.amount-due {
color: $green;
}
- .price-table {
+ .cart-panel {
max-height: 0;
transition: all 350ms ease-in;
overflow: hidden;
visibility: hidden;
color: $gray-600;
+ }
+ .price-table {
div {
display: flex;
justify-content: space-between;
@@ -846,6 +909,10 @@ export default {
background-color: $gray-100;
align-items: center;
}
+ .struck-out-price {
+ text-decoration: line-through;
+ padding-left: 0.5rem;
+ }
.packaged-cart-item {
background-color: $gray-100;
padding-left: 2.5rem;
@@ -934,17 +1001,26 @@ export default {
&.expanded:after {
transform: rotate(180deg);
}
- &.expanded + .price-table {
+ &.expanded + .cart-panel {
max-height: 650px;
transition: all 150ms ease-in;
overflow: hidden;
- padding-top: 1rem;
+ padding: 1rem 0 0.5rem;
visibility: visible;
}
a {
text-decoration: none;
}
}
+ .applied-promo {
+ span {
+ color: $green-700;
+ font-weight: 500;
+ padding: 2px 8px;
+ background-color: $green-100;
+ border-radius: 12px;
+ }
+ }
}
.payment-method-question {
.question-text {
diff --git a/src/fmg-components/promo-modal-question/promo-modal-question.spec.js b/src/fmg-components/promo-modal-question/promo-modal-question.spec.js
new file mode 100644
index 000000000..435a53277
--- /dev/null
+++ b/src/fmg-components/promo-modal-question/promo-modal-question.spec.js
@@ -0,0 +1,400 @@
+import { mount, shallowMount } from "@vue/test-utils";
+import promoModalQuestion from "./promo-modal-question";
+import { storeActions } from "@/constants/store-actions";
+import baseMixin from "@/mixins/base-mixin.js";
+import {
+ promoErrorCodes,
+ getPromoCodesFromPromoObjectsWithoutDuplicates,
+ getPromoCodeWithoutBundleIdentifier,
+} from "@/helpers/promotions-helper";
+import { cartItemCategories } from "@/constants/cart-item-categories";
+
+let mockReturnsForStoreActions = {};
+
+jest.mock("@/mixins/base-mixin", () => ({
+ ...jest.requireActual("@/mixins/base-mixin"),
+ methods: {
+ dispatchStoreActionWithLogging: jest.fn(
+ (action, { promoCode, addableVaps }, pageNameToLog, someBool) => {
+ return mockReturnsForStoreActions[action];
+ }
+ ),
+ },
+}));
+
+afterEach(() => {
+ // reset store action returns
+ mockReturnsForStoreActions = {};
+});
+
+jest.mock("@/digital-components/textbox-question/textbox-question", () => ({
+ getCmsContent: jest.fn((widgetName, cmsFieldName) => {
+ return widgetName[cmsFieldName];
+ }),
+}));
+
+jest.mock("@/digital-components/modal/modal", () => ({
+ methods: {
+ closeModal: jest.fn(),
+ resetButtonStyle: jest.fn(),
+ resetForm: jest.fn(),
+ },
+}));
+
+const modalWidgetName = "modalWidgetName";
+
+const mockModalCmsContent = {
+ FooterText: "Sample modal footer text here.",
+};
+
+const mockMixin = {
+ methods: {
+ getCmsContent: jest.fn((widgetName, cmsFieldName) => {
+ if (widgetName === modalWidgetName) {
+ return mockModalCmsContent[cmsFieldName];
+ }
+
+ return null;
+ }),
+ },
+};
+
+describe("promo-modal-question.vue", () => {
+ it("Should reset all alerts on onModalClosed", async () => {
+ // Arrange
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+
+ // Act
+ wrapper.vm.onModalClosed();
+
+ // Assert
+ expect(wrapper.vm.displayInvalidPromoAlert).toBe(false);
+ expect(wrapper.vm.displayStackingPromoAlert).toBe(false);
+ expect(wrapper.vm.displayInShopPromoAlert).toBe(false);
+ expect(wrapper.vm.displaySimilarPromoAlert).toBe(false);
+ });
+ it("Should emit update:modelValue on Modal closed", async () => {
+ // Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+
+ // Act
+
+ await wrapper.vm.onModalClosed();
+
+ // Assert
+ expect(wrapper.emitted("update:modelValue")).toEqual([[lineItems]]);
+ });
+ it("focuses on the input element when focusOnPromoInput is called", async () => {
+ // Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ const focusMock = jest.fn();
+ const inputMock = { focus: focusMock };
+
+ // Mock document.getElementById to return the inputMock
+ jest.spyOn(document, "getElementById").mockReturnValue(inputMock);
+
+ //Act
+
+ wrapper.vm.focusOnPromoInput();
+ await wrapper.vm.$nextTick();
+
+ //Assert
+ expect(focusMock).toHaveBeenCalled();
+ });
+ it("returns a validate response on applying promo", async () => {
+ // Arrange
+ const newPromo = "testPromo";
+ const pageNameToLog = "testPage";
+ const validateResponse = { orderPromos: [] };
+ const addableVaps = [];
+ mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] =
+ validateResponse;
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ // Act
+
+ wrapper.vm.getPromoCodeData();
+
+ const promoValidationResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA,
+ {
+ newPromo,
+ lineItems,
+ addableVaps,
+ },
+ pageNameToLog,
+ false
+ );
+
+ // Assert
+ expect(promoValidationResponse).toEqual(validateResponse);
+ });
+ test("If promocode is valid return taxed lineItems", async () => {
+ //Arrange
+ const taxedlineItems = {};
+
+ mockReturnsForStoreActions[storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA] =
+ taxedlineItems;
+
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ let promoCode = "1wiper0";
+ const pricedLineItemsToTax = [];
+
+ pricedLineItemsToTax.push(promoCode);
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+
+ attachTo: document.body,
+ });
+ wrapper.vm.addPromoCode();
+
+ const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
+ {
+ billToAccountNumber: "87291",
+ providerNumber: 2,
+ appointmentType: "IN_SHOP",
+ serviceLocationCity: "city",
+ serviceLocationState: "state",
+ serviceLocationZipCode: "12345",
+ pricedLineItems: pricedLineItemsToTax,
+ },
+
+ "payment-method",
+
+ false
+ );
+
+ // Assert
+
+ expect(taxedLineItems).toEqual(taxedlineItems);
+ });
+ test("Get promoCode list will return the applied promos", async () => {
+ // Arrange
+ const promos = [
+ { promoCode: "duplicatePromo" },
+ { promoCode: "duplicatePromo" },
+ { promoCode: "bundlePromo/400" },
+ { promoCode: "bundlePromo/401" },
+ ];
+ const appliedPromos = ["duplicatePromo", "bundlePromo"];
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+
+ // Act
+ wrapper.vm.getPromoCodeList();
+ const promoCodeToDisplay = getPromoCodesFromPromoObjectsWithoutDuplicates(promos);
+
+ // Assert
+ expect(promoCodeToDisplay).toEqual(appliedPromos);
+ });
+ test("On clicking remove the promo gets removed from the lineItems", async () => {
+ //Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [{ promoCode: "duplicatePromo" }, { promoCode: "duplicatePromo" }],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ const promo = ["bundlePromo"];
+ //Act
+ wrapper.vm.removeItem(promo);
+ const existingPromo = (lineItems[cartItemCategories.PROMOS] = lineItems[
+ cartItemCategories.PROMOS
+ ].filter(
+ (lineItemsToKeep) =>
+ getPromoCodeWithoutBundleIdentifier(lineItemsToKeep.promoCode) != promo
+ ));
+
+ //Assert
+ expect(existingPromo).toEqual(lineItems.promos);
+ });
+ test("Getting stacking alert on stack promo error code", async () => {
+ // Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ const additionalInfo = ["testAdditionalInfo"];
+
+ const stackingErrorCode = promoErrorCodes.PROMO_STACKING_NOT_ALLOWED;
+
+ // Act
+ wrapper.vm.getErrorMessage(stackingErrorCode, additionalInfo);
+
+ // Assert
+ expect(wrapper.vm.displayStackingPromoAlert).toBe(true);
+ });
+ test("Getting invalid promo alert on invalid promoCode", async () => {
+ // Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ const additionalInfo = ["testAdditionalInfo"];
+
+ const invalidErrorCode = promoErrorCodes.INVALID_PROMO_ON_ORDER;
+
+ // Act
+ wrapper.vm.getErrorMessage(invalidErrorCode, additionalInfo);
+
+ // Assert
+ expect(wrapper.vm.displayInvalidPromoAlert).toBe(true);
+ });
+ test("Getting invalid promo alert on invalid promoCode", async () => {
+ // Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ const additionalInfo = ["APPOINTMENT_TYPE"];
+
+ const invalidErrorCode = promoErrorCodes.INVALID_PROMO_ON_ORDER;
+
+ // Act
+ wrapper.vm.getErrorMessage(invalidErrorCode, additionalInfo);
+
+ // Assert
+ expect(wrapper.vm.displayInShopPromoAlert).toBe(true);
+ });
+ test("Get conflicting promoCodes on applying more than one promo", async () => {
+ // Arrange
+ const lineItems = {
+ glassParts: [],
+ supportingItems: [],
+ vaps: [],
+ promos: [],
+ };
+
+ const wrapper = mount(promoModalQuestion, {
+ mixins: [mockMixin],
+ props: {
+ modelValue: lineItems,
+ modalWidgetName: modalWidgetName,
+ },
+ attachTo: document.body,
+ });
+ const additionalInfo = ["1wiper0", "Glass30"];
+ const conflictingCodes = ["1wiper0", "Glass30"];
+
+ //Act
+ wrapper.vm.getConflictingPromoCode(additionalInfo);
+
+ //Assert
+ expect(conflictingCodes).toEqual(additionalInfo);
+ });
+});
diff --git a/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue b/src/fmg-components/promo-modal-question/promo-modal-question.vue
similarity index 81%
rename from src/layouts/payment-method/promo-modal-question/promo-modal-question.vue
rename to src/fmg-components/promo-modal-question/promo-modal-question.vue
index adb9bee3d..59c1523b2 100644
--- a/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue
+++ b/src/fmg-components/promo-modal-question/promo-modal-question.vue
@@ -64,7 +64,7 @@
diff --git a/src/layouts/quote/service-package-question/service-package-question.spec.js b/src/layouts/quote/service-package-question/service-package-question.spec.js
index c156ef34c..8163c6324 100644
--- a/src/layouts/quote/service-package-question/service-package-question.spec.js
+++ b/src/layouts/quote/service-package-question/service-package-question.spec.js
@@ -256,6 +256,57 @@ describe("service-package-question.vue", () => {
// Assert
expect(wrapper.vm.selectedPackageName).toBe("TierThree");
});
+ it("should select default package if promos are added", async () => {
+ //Arrange
+ mockProps.activePromos != null;
+ const wrapper = setupMocks({
+ mountOptionsMockData: {
+ store: {
+ getters: {
+ order: {
+ damage: {
+ isRepair: false,
+ glassToReplace: [{ glassLocation: "Windshield" }],
+ },
+ },
+ lineItems: {
+ vaps: [],
+ },
+ hasAnyNonWindshieldGlassParts: false,
+ payment: {
+ isInsurance: false,
+ },
+ },
+ },
+ },
+ });
+
+ //Act
+ wrapper.setProps({
+ activePromos: [
+ {
+ discountedLineItemIds: [
+ {
+ 0: "428ec73c-38e4-4e14-8703-a987b0391898",
+ 1: "79db94d7-163c-4408-a1ec-f83e58c11992",
+ },
+ ],
+ partType: "PROMO_DISCOUNT",
+ promoCode: "1WIPER0",
+ partNumber: "WIPER DISCOUNT",
+ laborAmount: 0,
+ sellingPrice: -10,
+ kitPrice: 0,
+ salesTax: null,
+ },
+ ],
+ });
+ const selectDefaultPackageMock = jest.spyOn(wrapper.vm, "selectDefaultPackage");
+ await nextTick();
+
+ //Assert
+ expect(selectDefaultPackageMock).toHaveBeenCalled();
+ });
});
describe("service-package-question.vue, matching business rules for package display", () => {
// mock scenarios in figma:
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 28727f155..6b84f3557 100644
--- a/src/layouts/quote/service-package-question/service-package-question.vue
+++ b/src/layouts/quote/service-package-question/service-package-question.vue
@@ -63,6 +63,9 @@ export default {
this.selectDefaultPackage();
}
},
+ activePromos() {
+ this.selectDefaultPackage();
+ },
selectedPackageName(newValue) {
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js
index f25bc68db..1d7d3f493 100644
--- a/src/layouts/vin-lookup/vin-lookup.spec.js
+++ b/src/layouts/vin-lookup/vin-lookup.spec.js
@@ -3,6 +3,7 @@ import vinLookup from "./vin-lookup.vue";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js";
import { settleAllPromises } from "@/helpers/layout-helper.js";
+import { experimentSettings } from "@/constants/experiments";
import store from "@/store";
@@ -378,5 +379,11 @@ function mockOutStubFunctions(wrapper) {
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => "placeholder CMS content"),
+ getSettingValue: jest.fn((settingName) => {
+ if (settingName === experimentSettings.IS_EMAIL_OPTIONAL) {
+ return "true";
+ }
+ return "false";
+ }),
},
};
diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue
index c809bae70..ce8365a53 100644
--- a/src/layouts/vin-lookup/vin-lookup.vue
+++ b/src/layouts/vin-lookup/vin-lookup.vue
@@ -53,8 +53,9 @@
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
customInputId="emailAddress"
- isRequired
- validationRules="email-address-required|email-address-format" />
+ :isRequired="!IsEmailOptional"
+ :validationRules="EmailValidationRules"
+ :addOptionalText="IsEmailOptional" />
diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js
index d394190ff..cfa68c82c 100644
--- a/src/mixins/analytics-mixin.js
+++ b/src/mixins/analytics-mixin.js
@@ -120,77 +120,182 @@ export default {
await this.logPageView(analyticsPageEvents.ENTRY);
},
- pushSubmittedOrderToDataLayer() {
- // check if submitted order exists; exit if not.
- const hasSubmittedOrder = store.getters.hasSubmittedOrder;
+ pushOrderToDataLayer() {
+ // helper check for if an object is defined (but maybe falsey)
+ const isDefined = (x) => x !== null && x !== undefined;
- if (!hasSubmittedOrder) {
- return;
+ // Get correct order object
+ const hasSubmittedOrder = store.getters.hasSubmittedOrder;
+ const order = hasSubmittedOrder ? store.getters.submittedOrder : store.getters.order;
+
+ // Begin assembling payload for data layer
+ const payload = {};
+
+ // Service Zip
+ if (
+ order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE &&
+ isDefined(order.serviceLocation.zipCode)
+ ) {
+ payload.serviceZipCode = order.serviceLocation.zipCode;
+ } else if (
+ isDefined(order.serviceLocation.appointmentType) &&
+ order.serviceLocation.appointmentType !== AppointmentTypeStrings.MOBILE &&
+ isDefined(order.serviceLocation.provider.address.zipCode)
+ ) {
+ payload.serviceZipCode = order.serviceLocation.provider.address.zipCode;
+ } else {
+ payload.serviceZipCode = "";
}
- const order = store.getters.submittedOrder;
+ // Damage Type
+ if (isDefined(order.damage.isRepair)) {
+ payload.damageType = order.damage.isRepair ? "repair" : "replace";
+ } else {
+ payload.damageType = "";
+ }
- // assemble data for payload
- // // reduce promocode array
+ // Account Type
+ if (isDefined(order.payment.isInsurance)) {
+ payload.accountType = order.payment.isInsurance ? "insurance" : "cash";
+ } else {
+ payload.accountType = "";
+ }
+
+ // Promo Codes
const promos = order.lineItems.promos ?? [];
- const promoCodes = promos.map((promo) => promo.promoCode);
- const promoString =
- promoCodes.length === 0 ? "" : promoCodes.reduce((prev, next) => `${prev},${next}`);
+ if (promos.length === 0) {
+ payload.promoCodes = "";
+ } else {
+ const promoCodes = promos.map((promo) => promo.promoCode);
+ const promoString = promoCodes.reduce((prev, next) => `${prev},${next}`);
+ payload.promoCodes = promoString;
+ }
- // // reduce glass array
- const glassToReplace = order.damage.glassToReplace ?? [];
- const glassToReplaceNames = glassToReplace.map(
- (glassPiece) => `${glassPiece.glassLocation}/${glassPiece.glassName}`
- );
- const glassString =
- glassToReplaceNames.length === 0
- ? ""
- : glassToReplaceNames.reduce((prev, next) => `${prev},${next}`);
+ // Vehicle info
+ if (isDefined(order.vehicle.year)) {
+ // Ensure cast to string.
+ payload.vehicleYear = `${order.vehicle.year}`;
+ } else {
+ payload.vehicleYear = "";
+ }
- // // calculate subtotal
- const lineItems = order.lineItems;
+ if (isDefined(order.vehicle.make)) {
+ payload.vehicleMake = order.vehicle.make;
+ } else {
+ payload.vehicleMake = "";
+ }
+
+ if (isDefined(order.vehicle.model)) {
+ payload.vehicleModel = order.vehicle.model;
+ } else {
+ payload.vehicleModel = "";
+ }
+
+ if (isDefined(order.vehicle.style)) {
+ payload.vehicleStyle = order.vehicle.style;
+ } else {
+ payload.vehicleStyle = "";
+ }
+
+ // Glass pieces
+ const glass = order.damage.glassToReplace ?? [];
+ if (glass.length === 0) {
+ payload.glassToReplace = "";
+ } else {
+ const glassNames = glass.map((g) => `${g.glassLocation}/${g.glassName}`);
+ const glassString = glassNames.reduce((prev, next) => `${prev},${next}`);
+
+ payload.glassToReplace = glassString;
+ }
+
+ // Work Order Id
+ if (order.workOrderId) {
+ const parsedId = parseInt(order.workOrderId);
+ if (!isNaN(parsedId)) {
+ payload.workOrderId = parsedId;
+ } else {
+ payload.workOrderId = "";
+ }
+ } else {
+ payload.workOrderId = "";
+ }
+
+ // Provider Ctu
+ if (isDefined(order.serviceLocation.zipCodeCtu)) {
+ const parsedCtu = parseInt(order.serviceLocation.zipCodeCtu);
+ if (!isNaN(parsedCtu)) {
+ payload.providerCtu = parseInt(order.serviceLocation.zipCodeCtu);
+ } else {
+ payload.providerCtu = "";
+ }
+ } else {
+ payload.providerCtu = "";
+ }
+
+ // Work Order Number
+ if (order.workOrderNumber) {
+ payload.orderNumber = order.workOrderNumber;
+ } else {
+ payload.orderNumber = "";
+ }
+
+ // Pricing
+ // Only fire for completed orders?
+
+ const lineItems = order.lineItems ?? {};
const combinedLineItems = [
...(lineItems.glassParts ?? []),
...(lineItems.supportingItems ?? []),
...(lineItems.vaps ?? []),
...(lineItems.promos ?? []),
];
- const subtotal = baseMixin.methods
- .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false)
- .toFixed(2);
- // // get correct zip code
- const providerZip = order.serviceLocation.provider.address.zipCode;
- const serviceZip =
- order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
- ? order.serviceLocation.zipCode
- : providerZip;
+ const isPricingAvailable =
+ combinedLineItems.length > 0 &&
+ combinedLineItems.every(
+ (lineItem) =>
+ isDefined(lineItem.kitPrice) &&
+ isDefined(lineItem.laborAmount) &&
+ isDefined(lineItem.sellingPrice)
+ );
+ const isTaxAvailable =
+ isPricingAvailable &&
+ combinedLineItems.every((lineItem) => isDefined(lineItem.salesTax));
- // // calculate total
- const total = baseMixin.methods
- .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, true)
- .toFixed(2);
+ if (isPricingAvailable) {
+ const subtotal = baseMixin.methods
+ .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false)
+ .toFixed(2);
- const payload = {
- serviceZipCode: serviceZip,
- damageType: order.damage.isRepair ? "repair" : "replace",
- accountType: order.payment.isInsurance ? "insurance" : "cash",
- promoCodes: promoString,
- vehicleYear: `${order.vehicle.year}`,
- vehicleMake: order.vehicle.make,
- vehicleModel: order.vehicle.model,
- vehicleStyle: order.vehicle.style,
- glassToReplace: glassString,
- workOrderId: parseInt(order.workOrderId),
- providerCtu: parseInt(order.serviceLocation.zipCodeCtu),
- orderNumber: order.workOrderNumber,
- priceTotal: parseFloat(total),
- priceSubTotal: parseFloat(subtotal),
- isRecalibrationOnOrder: store.getters.isRecalibrationOnSubmittedOrder,
- appointmentType: order.serviceLocation.appointmentType,
- };
+ payload.priceSubTotal = parseFloat(subtotal);
+ } else {
+ payload.priceSubTotal = "";
+ }
+
+ if (isTaxAvailable) {
+ const total = baseMixin.methods
+ .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, true)
+ .toFixed(2);
+
+ payload.priceTotal = parseFloat(total);
+ } else {
+ payload.priceTotal = "";
+ }
+
+ // Recalibration
+ if (hasSubmittedOrder) {
+ payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnSubmittedOrder;
+ } else {
+ payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder;
+ }
+
+ // Appointment Type
+ if (isDefined(order.serviceLocation.appointmentType)) {
+ payload.appointmentType = order.serviceLocation.appointmentType;
+ } else {
+ payload.appointmentType = "";
+ }
- // push to data layer.
pushToDataLayerIfDefined(payload);
},
diff --git a/src/mixins/analytics-mixin.spec.js b/src/mixins/analytics-mixin.spec.js
index ff6c30e7e..42aba5f31 100644
--- a/src/mixins/analytics-mixin.spec.js
+++ b/src/mixins/analytics-mixin.spec.js
@@ -38,27 +38,38 @@ const parts = {
requiresRecalibration: true,
salesTax: 63.86,
sellingPrice: 791.46,
+ kitPrice: 0,
+ laborAmount: 0,
},
frontWipers: {
name: "front wipers",
partNumber: "SBB16",
description: "SAFELITE BEAM BLADE 16",
partType: "FRONT WIPER",
- price: 32.64,
+ sellingPrice: 32.64,
+ kitPrice: 0,
+ laborAmount: 0,
+ salesTax: 1,
},
rearWipers: {
name: "rear wipers",
partNumber: "SBBR12A",
description: "SAFELITE REAR BLADE 12A",
partType: "REAR WIPER",
- price: 24.48,
+ sellingPrice: 24.48,
+ kitPrice: 0,
+ laborAmount: 0,
+ salesTax: 2,
},
rainDefense: {
name: "rain defense",
partNumber: "RAIN DEFENSE",
description: null,
partType: "RAIN DEFENSE",
- price: 35.5,
+ sellingPrice: 35.5,
+ kitPrice: 0,
+ laborAmount: 0,
+ salesTax: 0,
},
};
@@ -324,10 +335,10 @@ describe("analyticsMixin.js", () => {
]);
});
- describe("pushSubmittedOrderToDataLayer", () => {
+ describe("pushOrderToDataLayer", () => {
beforeEach(() => {
- store.getters.hasSubmittedOrder = true;
- store.getters.submittedOrder = {
+ store.getters.hasSubmittedOrder = false;
+ store.getters.order = {
vehicle: {
year: "2020",
make: "acura",
@@ -342,8 +353,8 @@ describe("analyticsMixin.js", () => {
address2: "add2",
city: "city",
state: "state",
- zipCode: "zip",
- zipCodeCtu: "zipCtu",
+ zipCode: "11111",
+ zipCodeCtu: "11110",
appointmentType: "IN_SHOP",
isVehicleProtected: true,
provider: {
@@ -352,8 +363,8 @@ describe("analyticsMixin.js", () => {
streetAddress: "add3",
city: "city2",
state: "state2",
- zipCode: "zip2",
- zipCodeCtu: "zipCtu2",
+ zipCode: "22222",
+ zipCodeCtu: "22220",
},
},
techNotes: "",
@@ -368,13 +379,28 @@ describe("analyticsMixin.js", () => {
damage: {
isRepair: false,
numberOfChips: null,
- glassToReplace: [{ glassName: "single", location: "windshield" }],
+ glassToReplace: [{ glassName: "single", glassLocation: "windshield" }],
},
lineItems: {
glassParts: [parts.windshield],
supportingItems: [],
vaps: [parts.frontWipers],
- promos: [{ promoCode: "promoTEST" }, { promoCode: "promoTEST2" }],
+ promos: [
+ {
+ promoCode: "promoTEST",
+ kitPrice: 0,
+ sellingPrice: 0,
+ laborAmount: 0,
+ salesTax: 0,
+ },
+ {
+ promoCode: "promoTEST2",
+ kitPrice: 0,
+ sellingPrice: 0,
+ laborAmount: 0,
+ salesTax: 0,
+ },
+ ],
},
payment: {
isInsurance: false,
@@ -397,6 +423,8 @@ describe("analyticsMixin.js", () => {
workOrderNumber: "01820-111111",
workOrderId: "222222222222",
};
+ store.getters.isRecalibrationOnOrder = true;
+ store.getters.isRecalibrationOnSubmittedOrder = false;
});
test("Pushes to data layer if nominal", () => {
@@ -408,28 +436,203 @@ describe("analyticsMixin.js", () => {
};
// Act
- analyticsMixin.methods.pushSubmittedOrderToDataLayer();
+ analyticsMixin.methods.pushOrderToDataLayer();
// Assert
expect(mockDataLayerFn).toHaveBeenCalled();
});
- test("Does not push to data layer if no submitted order available.", () => {
+ test("Pushes populated data to data layer", () => {
// Arrange
- store.getters.hasSubmittedOrder = false;
- store.getters.submittedOrder = undefined;
+ window.dataLayer = [];
- const mockDataLayerFn = jest.fn();
+ // Act
+ analyticsMixin.methods.pushOrderToDataLayer();
- window.dataLayer = {
- push: mockDataLayerFn,
+ const result = window.dataLayer[0];
+
+ // Assert
+ console.log(result);
+ const allFieldsPopulated = Object.keys(result).every(
+ (key) => result[key] === false || !!result[key]
+ );
+ expect(allFieldsPopulated).toBe(true);
+ });
+
+ test("Pushes correct data when submitted order is present", () => {
+ // Arrange
+ window.dataLayer = [];
+ store.getters.hasSubmittedOrder = true;
+ store.getters.submittedOrder = store.getters.order;
+ store.getters.order = {
+ vehicle: {
+ year: null,
+ make: null,
+ model: null,
+ style: null,
+ carId: null,
+ category: null,
+ vin: null,
+ },
+ serviceLocation: {
+ address: null,
+ address2: null,
+ city: null,
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ appointmentType: null,
+ isVehicleProtected: null,
+ provider: {
+ providerNumber: null,
+ address: {
+ streetAddress: null,
+ city: null,
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ },
+ },
+ techNotes: null,
+ },
+ customer: {
+ firstName: null,
+ lastName: null,
+ emailAddress: null,
+ phoneNumber: null,
+ isSmsOptIn: null,
+ },
+ damage: {
+ isRepair: null,
+ numberOfChips: null,
+ glassToReplace: null,
+ },
+ lineItems: {
+ glassParts: null,
+ supportingItems: null,
+ vaps: null,
+ promos: null,
+ },
+ payment: {
+ isInsurance: null,
+ insuranceCoverage: {
+ isVerified: null,
+ coverageStatus: null,
+ coverageVerificationType: null,
+ },
+ isPia: null,
+ piaType: null,
+ inactivePromos: null,
+ },
+ schedule: {
+ date: null,
+ startTime: null,
+ endTime: null,
+ jobMinMinutes: null,
+ jobMaxMinutes: null,
+ },
+ workOrderNumber: null,
+ workOrderId: null,
};
// Act
- analyticsMixin.methods.pushSubmittedOrderToDataLayer();
+ analyticsMixin.methods.pushOrderToDataLayer();
+
+ const result = window.dataLayer[0];
// Assert
- expect(mockDataLayerFn).not.toHaveBeenCalled();
+ console.log(result);
+ const allFieldsPopulated = Object.keys(result).every(
+ (key) => result[key] === false || !!result[key]
+ );
+ expect(allFieldsPopulated).toBe(true);
+ });
+
+ test("Pushes default values when data is missing", () => {
+ // Arrange
+ window.dataLayer = [];
+ store.getters.order = {
+ vehicle: {
+ year: null,
+ make: null,
+ model: null,
+ style: null,
+ carId: null,
+ category: null,
+ vin: null,
+ },
+ serviceLocation: {
+ address: null,
+ address2: null,
+ city: null,
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ appointmentType: null,
+ isVehicleProtected: null,
+ provider: {
+ providerNumber: null,
+ address: {
+ streetAddress: null,
+ city: null,
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ },
+ },
+ techNotes: null,
+ },
+ customer: {
+ firstName: null,
+ lastName: null,
+ emailAddress: null,
+ phoneNumber: null,
+ isSmsOptIn: null,
+ },
+ damage: {
+ isRepair: null,
+ numberOfChips: null,
+ glassToReplace: null,
+ },
+ lineItems: {
+ glassParts: null,
+ supportingItems: null,
+ vaps: null,
+ promos: null,
+ },
+ payment: {
+ isInsurance: null,
+ insuranceCoverage: {
+ isVerified: null,
+ coverageStatus: null,
+ coverageVerificationType: null,
+ },
+ isPia: null,
+ piaType: null,
+ inactivePromos: null,
+ },
+ schedule: {
+ date: null,
+ startTime: null,
+ endTime: null,
+ jobMinMinutes: null,
+ jobMaxMinutes: null,
+ },
+ workOrderNumber: null,
+ workOrderId: null,
+ };
+
+ // Act
+ analyticsMixin.methods.pushOrderToDataLayer();
+
+ const result = window.dataLayer[0];
+
+ // Assert
+ console.log(result);
+ const allFieldsPopulatedOrDefault = Object.keys(result).every(
+ (key) => result[key] === false || !!result[key] || result[key] === ""
+ );
+ expect(allFieldsPopulatedOrDefault).toBe(true);
});
test("Glass and Promo strings correctly formatted", () => {
@@ -437,7 +640,7 @@ describe("analyticsMixin.js", () => {
window.dataLayer = [];
// Act
- analyticsMixin.methods.pushSubmittedOrderToDataLayer();
+ analyticsMixin.methods.pushOrderToDataLayer();
// Assert
const glassString = window.dataLayer[0].glassToReplace;
diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js
index a0a435000..98eb7a75b 100644
--- a/src/mixins/vin-pages-mixin.js
+++ b/src/mixins/vin-pages-mixin.js
@@ -2,8 +2,20 @@ import { storeActions } from "@/constants/store-actions.js";
import store from "@/store";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
+import { experimentSettings } from "@/constants/experiments";
export default {
+ computed: {
+ IsEmailOptional() {
+ const emailOptional = this.getSettingValue(experimentSettings.IS_EMAIL_OPTIONAL);
+ return emailOptional === "true";
+ },
+ EmailValidationRules() {
+ return this.IsEmailOptional
+ ? "email-address-format"
+ : "email-address-required|email-address-format";
+ },
+ },
methods: {
async navigateForwardWithSingleCarMatch() {
const pageName = this.$options?.name;
diff --git a/src/router/index.js b/src/router/index.js
index 8fc4d7960..2373b6e48 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -225,6 +225,9 @@ router.afterEach(async (to, from) => {
// Push experiments to Data Layer
analyticsMixin.methods.pushExperimentsToDataLayer();
+
+ // Push current order status to Data Layer
+ analyticsMixin.methods.pushOrderToDataLayer();
});
router.navigateWithoutSaving = (
diff --git a/src/store/index.js b/src/store/index.js
index 3241695fb..e213e34aa 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2361,6 +2361,9 @@ export const actions = {
) {
const order = context.getters.order;
lineItemsToUse = lineItemsToUse ?? order.lineItems;
+ addGuidToLineItemsIfNotAlreadyThere(lineItemsToUse.vaps);
+ syncLineItemIds(addableVaps, lineItemsToUse.vaps);
+ // addableVaps still won't have IDs if they weren't already in lineItemsToUse
addGuidToLineItemsIfNotAlreadyThere(addableVaps);
const requestObject = {
promoCode: promoCode,
@@ -2879,12 +2882,18 @@ function syncLineItemIds(lineItemsWithoutIds, lineItemsWithIds) {
if (!lineItemsWithIds || !lineItemsWithoutIds) {
return;
}
+ var clonedLineItemsWithIds = deepClone(lineItemsWithIds);
lineItemsWithoutIds.forEach((noId) => {
- lineItemsWithIds.forEach((withId) => {
+ var matchedLineItemIndex = -1;
+ clonedLineItemsWithIds.forEach((withId, index) => {
if (noId.partNumber === withId.partNumber && noId.partType === withId.partType) {
+ matchedLineItemIndex = index;
noId.id = withId.id;
}
});
+ if (matchedLineItemIndex > -1) {
+ clonedLineItemsWithIds.splice(matchedLineItemIndex, 1);
+ }
});
}
diff --git a/src/store/store.spec.js b/src/store/store.spec.js
index 17dc27ec2..bfa09f9a1 100644
--- a/src/store/store.spec.js
+++ b/src/store/store.spec.js
@@ -2933,7 +2933,7 @@ describe("Actions", () => {
// Arrange
const context = state;
const promoCode = "testPromo";
- const lineItemsToUse = { vaps: [1], promos: [2] };
+ const lineItemsToUse = { vaps: [{ partNumber: 1 }], promos: [2] };
const addableVaps = [{ partNumber: "addableVap" }];
context["getters"] = {
@@ -3013,7 +3013,7 @@ describe("Actions", () => {
},
referralSequenceNumber: "test",
lineItems: {
- vaps: [1],
+ vaps: [{ partNumber: 1 }],
promos: [2],
serverData: "test",
},
@@ -3026,7 +3026,7 @@ describe("Actions", () => {
crypto.randomUUID = jest.fn(() => "GUID");
- const expectedLineItemsOnOrder = [1, 2];
+ const expectedLineItemsOnOrder = [{ partNumber: 1, id: "GUID" }, 2];
// Act
actions.validateOrderPromoAndSaveServerData(context, {
@@ -3073,7 +3073,7 @@ describe("Actions", () => {
},
referralSequenceNumber: "test",
lineItems: {
- vaps: [1],
+ vaps: [{ partNumber: 1 }],
promos: [2],
serverData: "test",
},
@@ -3131,7 +3131,7 @@ describe("Actions", () => {
},
referralSequenceNumber: "test",
lineItems: {
- vaps: [1],
+ vaps: [{ partNumber: 1 }],
promos: [2],
serverData: "test",
},