diff --git a/src/constants/application-config.js b/src/constants/application-config.js
index 244882509..b8a59a129 100644
--- a/src/constants/application-config.js
+++ b/src/constants/application-config.js
@@ -11,6 +11,7 @@ const applicationConfig = {
PAGE_QUERYSTRING: "fmgPage",
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass",
CASH_PARENT_ACCOUNT_NUMBER: 167132,
+ CASH_DEFAULT_BILL_TO_ACCOUNT_NUMBER: "87291",
MY_ACCOUNT: process.env.VUE_APP_MY_ACCOUNT,
PIA_RESPONSE_URL:
location.protocol +
diff --git a/src/constants/coverage-status.js b/src/constants/coverage-status.js
new file mode 100644
index 000000000..7cf757aed
--- /dev/null
+++ b/src/constants/coverage-status.js
@@ -0,0 +1,18 @@
+const coverageStatus = {
+ PENDING: "Pending",
+ NOCOMP: "NoComp",
+ VERIFIED: "Verified",
+};
+
+export function coverageStatusValue(intCoverageStatus) {
+ switch (intCoverageStatus) {
+ case 0:
+ return coverageStatus.PENDING;
+ case 1:
+ return coverageStatus.NOCOMP;
+ case 2:
+ return coverageStatus.VERIFIED;
+ default:
+ return null;
+ }
+}
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/constants/store-actions.js b/src/constants/store-actions.js
index 9b6fdb865..d77237170 100644
--- a/src/constants/store-actions.js
+++ b/src/constants/store-actions.js
@@ -80,6 +80,7 @@ const storeActions = {
SAVE_PAYMENT_TYPE: "savePaymentType",
SAVE_PAYMENT_METHOD_CHOICE: "savePaymentMethodChoice",
SAVE_PARENT_ACCOUNT_NUMBER: "saveParentAccountNumber",
+ SAVE_BILL_TO_ACCOUNT_NUMBER: "saveBillToAccountNumber",
SAVE_SUPPORTING_ITEMS: "saveSupportingItems",
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
"saveSupportingItemsSuppressingStateResetting",
diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js
index 5b0b66509..0b0338fef 100644
--- a/src/constants/store-mutations.js
+++ b/src/constants/store-mutations.js
@@ -46,6 +46,7 @@ const storeMutations = {
UPDATE_REFERRAL_DATE: "updateReferralDate",
UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId",
UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber",
+ UPDATE_BILL_TO_ACCT_NUMBER: "updateBillToAcctNumber",
UPDATE_EON: "updateEON",
UPDATE_IS_INSURANCE: "updateIsInsurance",
UPDATE_SAVED_SESSION_ID: "updateSavedSessionId",
diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue
index e30b7563e..dc99951da 100644
--- a/src/digital-components/textbox-question/textbox-question.vue
+++ b/src/digital-components/textbox-question/textbox-question.vue
@@ -125,6 +125,11 @@ export default {
hideInput: Boolean,
centerErrorMessage: Boolean,
keyDownHandler: Function,
+ addOptionalText: {
+ type: Boolean,
+ default: false,
+ isRequired: false,
+ },
},
setup(props) {
const uuid = uuidv4();
@@ -202,7 +207,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 b4b02b3ed..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/schedule/schedule.vue b/src/layouts/schedule/schedule.vue
index c2fee87ee..05f439472 100644
--- a/src/layouts/schedule/schedule.vue
+++ b/src/layouts/schedule/schedule.vue
@@ -314,14 +314,13 @@ export default {
serviceLocation.provider.providerNumber);
const paymentInfo = store.getters.payment.isInsurance !== null;
- const supportingItems = store.getters.lineItems.supportingItems !== null;
const damageInfo =
store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0);
- return serviceLocationPreReqs && paymentInfo && supportingItems && damageInfo;
+ return serviceLocationPreReqs && paymentInfo && damageInfo;
},
async getAvailableDatesMethod(startDate, endDate) {
const newShopTimeSlots = await getAvailableDates(
@@ -348,9 +347,14 @@ export default {
},
getSelectedTimeSlotInfo() {
const supportingItems = this.getSupportingItems();
- const isPremiumAppointment =
- !!supportingItems.filter((lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE)
- .length > 0;
+
+ var isPremiumAppointment = false;
+ if (supportingItems) {
+ isPremiumAppointment =
+ !!supportingItems.filter(
+ (lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
+ ).length > 0;
+ }
const selectedTimeSlotInfo = {
timeSlot: store.getters.order.schedule,
@@ -461,6 +465,10 @@ export default {
false
);
} else {
+ if (!supportingItems) {
+ return;
+ }
+
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
const removePremiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
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 26f2e6962..e92349ddf 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
@@ -16,6 +16,14 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
false
);
+ if (
+ mobileFeePart.data === null ||
+ mobileFeePart.data === undefined ||
+ mobileFeePart.data === ""
+ ) {
+ return null;
+ }
+
// Get the Mobile Fee Part Price
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue
index 6a43f456f..353612d5d 100644
--- a/src/layouts/service-location/service-location.vue
+++ b/src/layouts/service-location/service-location.vue
@@ -138,6 +138,7 @@ import {
getServiceabilityDetails,
getShopProviderData,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
+import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { Provider } from "@/layouts/service-location/classes/provider";
@@ -348,11 +349,19 @@ export default {
},
methods: {
arePagePrerequisitesValid() {
- return (
- store.getters.lineItems.supportingItems !== null &&
- store.getters.order.serviceLocation.zipCode !== null &&
- store.getters.payment.isInsurance !== null
- );
+ // insurance drops the recycle fee on replace orders so it won't be in supportingItems
+ if (store.getters.payment.isInsurance && !store.getters.order.damage.isRepair) {
+ return (
+ store.getters.order.serviceLocation.zipCode !== null &&
+ store.getters.payment.isInsurance !== null
+ );
+ } else {
+ return (
+ store.getters.lineItems.supportingItems !== null &&
+ store.getters.order.serviceLocation.zipCode !== null &&
+ store.getters.payment.isInsurance !== null
+ );
+ }
},
setData(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) {
if (zipCodeData) {
@@ -438,7 +447,14 @@ export default {
this.recalibrationInformationModal.openModal();
},
backButtonAction() {
- this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
+ if (store.getters.order.payment.isInsurance) {
+ navigateToHeritageFunnel({ shouldSaveSession: false });
+ } else {
+ this.$router.navigateWithoutSaving(
+ this.navigationScenarios.CLICKED_BACK,
+ this.$route
+ );
+ }
},
updateAndSaveSupportingItems() {
const supportingItems = store.getters.lineItems.supportingItems;
@@ -463,7 +479,7 @@ export default {
);
} else {
// if it's not a mobile, then make sure we remove any that may have been added
- const removeMobileFeeIndex = supportingItems.findIndex(
+ const removeMobileFeeIndex = supportingItems?.findIndex(
(item) => item.partType == MOBILE_FEE_PART_TYPE
);
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 4a13a8fc7..2373b6e48 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -103,6 +103,7 @@ const routes = [
}
if (!arePagePrerequisitesValid(component)) {
+ console.log("Page prereq error: " + component.default.name);
GoToFunnelStartOn404(next);
}
@@ -110,6 +111,7 @@ const routes = [
}
if (!isExistingFmgPageName(to.query.fmgPage)) {
+ console.log("Not an existing fmg page name:" + to.query.fmgPage);
GoToFunnelStartOn404(next);
}
@@ -131,6 +133,9 @@ const routes = [
.components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
+ console.log(
+ "Page Prereqs not valid for next component: " + nextComponent.default.name
+ );
GoToFunnelStartOn404(next);
}
@@ -220,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 ffc6e6e08..e213e34aa 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -29,6 +29,7 @@ import {
} from "@/helpers/promotions-helper";
import { getDateDifferenceInDays } from "@/helpers/date-helper";
import sharedMutations from "./vuex-shared-mutations/vuexSharedMutations";
+import { coverageStatusValue } from "@/constants/coverage-status";
// Export State
const getDefaultState = () => {
return {
@@ -123,6 +124,7 @@ const getDefaultState = () => {
currentDeductible: 0,
policyNumber: null,
dateOfLoss: null,
+ lossCause: null,
isItac: false,
additionalAuthFlag: null,
},
@@ -248,6 +250,9 @@ export const mutations = {
updateParentAcctNumber(state, parentAcctNumber) {
state.order.payment.parentAccountNumber = parentAcctNumber;
},
+ updateBillToAcctNumber(state, billToAcctNumber) {
+ state.order.payment.billToAccountNumber = billToAcctNumber;
+ },
updateEON(state, eon) {
state.order.eon = eon;
},
@@ -523,6 +528,8 @@ export const mutations = {
state.order.payment.parentAccountNumber =
sessionInformation.order.payment.parentAccountNumber;
+ state.order.payment.billToAccountNumber =
+ sessionInformation.order.payment.billToAccountNumber;
state.order.payment.inactivePromos = sessionInformation.order.payment.inactivePromos;
state.order.serviceLocation.address =
@@ -557,8 +564,9 @@ export const mutations = {
state.order.payment.isInsurance = sessionInformation.order.payment.isInsurance;
state.order.payment.insuranceCoverage.isVerified =
sessionInformation?.order.payment.insuranceCoverage.isVerified;
- state.order.payment.insuranceCoverage.coverageStatus =
- sessionInformation?.order.payment.insuranceCoverage.coverageStatus;
+ state.order.payment.insuranceCoverage.coverageStatus = coverageStatusValue(
+ sessionInformation?.order.payment.insuranceCoverage.coverageStatus
+ );
state.order.payment.insuranceCoverage.coverageVerificationType =
sessionInformation?.order.payment.insuranceCoverage.coverageVerificationType;
@@ -573,12 +581,24 @@ export const mutations = {
state.applicationUser.pageData = sessionInformation.applicationUser.pageData;
state.applicationUser.lastPage = sessionInformation.applicationUser.lastPage;
+ if (sessionInformation.applicationUser.savedSessionId) {
+ state.applicationUser.savedSessionId =
+ sessionInformation.applicationUser.savedSessionId;
+ }
+
state.order.schedule.date = sessionInformation.order.schedule?.date;
state.order.schedule.startTime = sessionInformation.order.schedule?.startTime;
state.order.schedule.endTime = sessionInformation.order.schedule?.endTime;
state.order.schedule.routeCode = sessionInformation.order.schedule?.routeCode;
state.order.schedule.jobMaxMinutes = sessionInformation.order.schedule?.jobMaxMinutes;
state.order.schedule.jobMinMinutes = sessionInformation.order.schedule?.jobMinMinutes;
+
+ state.order.policy.currentDeductible = sessionInformation.order.policy?.currentDeductible;
+ state.order.policy.additionalAuthFlag = sessionInformation.order.policy?.additionalAuthFlag;
+ state.order.policy.isItac = sessionInformation.order.policy?.isItac;
+ state.order.policy.policyNumber = sessionInformation.order.policy?.policyNumber;
+ state.order.policy.dateOfLoss = sessionInformation.order.policy?.lossDate;
+ state.order.policy.lossCause = sessionInformation.order.policy?.lossCause;
},
updateExperiments(state, experiments) {
state.applicationUser.experiments = experiments;
@@ -1343,7 +1363,7 @@ export const actions = {
getMobileFeePart(context, { pageNameToLog }) {
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
const parentAccountNumber = context.getters.payment.parentAccountNumber;
- const billToAccountNumber = 87291; // TODO: MAKE THIS REAL
+ const billToAccountNumber = context.getters.payment.billToAccountNumber;
return globalMethods.callHttpClient({
method: endpoints.GetMobileFeePart.method,
@@ -1354,15 +1374,19 @@ export const actions = {
},
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
- const lineItemsWithOnlyPartNumbers = context.getters.order.lineItems.supportingItems.map(
+ const lineItemsWithOnlyPartNumbers = context.getters.order.lineItems.supportingItems?.map(
(lineItem) => ({
partNumber: lineItem.partNumber,
})
);
- const lineItems = buildQueryStringParameterFromArrayOfComplexObjects(
- lineItemsWithOnlyPartNumbers,
- "lineItems"
- );
+
+ var lineItems = null;
+ if (lineItemsWithOnlyPartNumbers) {
+ lineItems = buildQueryStringParameterFromArrayOfComplexObjects(
+ lineItemsWithOnlyPartNumbers,
+ "lineItems"
+ );
+ }
const vehicle = context.getters.vehicle;
const carId = vehicle.carId;
@@ -1374,9 +1398,18 @@ export const actions = {
"glassPieces"
);
+ var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}`;
+ if (lineItems) {
+ endPoint += `&${lineItems}`;
+ }
+
+ if (glassPieces) {
+ endPoint += `&${glassPieces}`;
+ }
+
return globalMethods.callHttpClient({
method: endpoints.GetServiceabilityDetails.method,
- endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&${lineItems}&${glassPieces}`,
+ endpoint: endPoint,
logApiCall: true,
pageNameToLog: pageNameToLog,
});
@@ -1453,7 +1486,7 @@ export const actions = {
) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
-
+ const payment = context.state.order.payment;
let lineItems = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
@@ -1475,15 +1508,16 @@ export const actions = {
endDate: endDate,
shopAppointmentType: shopAppointmentType,
applicationName: applicationConfig.APPLICATION_NAME,
- parentAccountNumber: context.getters.payment.parentAccountNumber,
+ parentAccountNumber: payment.parentAccountNumber,
carId: vehicle.carId,
lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
+ billToAccountNumber: context.getters.payment.billToAccountNumber,
coverage: {
- status: "",
- deductible: 0,
- additionalAuthFlag: "",
+ status: payment.insuranceCoverage.coverageStatus,
+ deductible: order.policy.currentDeductible,
+ additionalAuthFlag: order.policy.additionalAuthFlag,
},
partSelection: {
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
@@ -1521,6 +1555,7 @@ export const actions = {
getMobileTimeSlots(context, { payload: { startDate, endDate }, pageNameToLog }) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
+ const payment = context.state.order.payment;
let lineItems = [
...(order.lineItems.supportingItems ?? []),
...(order.lineItems.vaps ?? []),
@@ -1539,15 +1574,16 @@ export const actions = {
startDate: startDate,
endDate: endDate,
applicationName: applicationConfig.APPLICATION_NAME,
- parentAccountNumber: context.getters.payment.parentAccountNumber,
+ parentAccountNumber: payment.parentAccountNumber,
carId: vehicle.carId,
lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
+ billToAccountNumber: context.getters.payment.billToAccountNumber,
coverage: {
- status: "",
- deductible: 0,
- additionalAuthFlag: "",
+ status: payment.insuranceCoverage.coverageStatus,
+ deductible: order.policy.currentDeductible,
+ additionalAuthFlag: order.policy.additionalAuthFlag,
},
partSelection: {
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
@@ -1566,6 +1602,7 @@ export const actions = {
},
zipCode: order.serviceLocation.zipCode,
};
+
return globalMethods.callHttpClient({
method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url,
@@ -2117,6 +2154,9 @@ export const actions = {
saveParentAccountNumber(context, parentAccountNumber) {
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber);
},
+ saveBillToAccountNumber(context, billToAccountNumber) {
+ context.commit(storeMutations.UPDATE_BILL_TO_ACCT_NUMBER, billToAccountNumber);
+ },
saveSupportingItems(context, supportingItems) {
if (!deepEqual(supportingItems, context.state.order.lineItems.supportingItems)) {
@@ -2210,6 +2250,7 @@ export const actions = {
const vehicle = context.getters.order.vehicle;
+ // TODO: Insurance pricing...`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}`
let queryString =
`ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` +
`&CTU=${ctuToUse}` +
@@ -2320,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,
@@ -2838,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",
},