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 }} - - - - {{ 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", },