diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index ef5459395..71c782856 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -33,6 +33,7 @@ const errorMessages = { MAKE_REQUIRED: "Please select your vehicle make", MODEL_REQUIRED: "Please select your vehicle model", STYLE_REQUIRED: "Please select your vehicle style", + PROMO_REQUIRED: "Please enter a promo code", }; export { errorMessages }; diff --git a/src/fmg-components/funnel-header/funnel-header.vue b/src/fmg-components/funnel-header/funnel-header.vue index 26a9652d4..4a0936fd5 100644 --- a/src/fmg-components/funnel-header/funnel-header.vue +++ b/src/fmg-components/funnel-header/funnel-header.vue @@ -32,7 +32,6 @@ export default { name: "funnel-header", data() { return { - alertIdCounter: 0, globalAlertMessages: [], }; }, @@ -47,8 +46,7 @@ export default { methods: { pushGlobalAlert(alertToPush, isAutoDismissing) { alertToPush.displayAlert = true; - alertToPush.id = this.alertIdCounter; - this.alertIdCounter++; + alertToPush.id = crypto.randomUUID(); if (isAutoDismissing) { setTimeout(() => { alertToPush.displayAlert = false; diff --git a/src/fmg-components/loading-modal/loading-modal.vue b/src/fmg-components/loading-modal/loading-modal.vue index c3722000b..820ff6f7b 100644 --- a/src/fmg-components/loading-modal/loading-modal.vue +++ b/src/fmg-components/loading-modal/loading-modal.vue @@ -54,6 +54,14 @@ diff --git a/src/layouts/payment-method/promo-modal-question/promo-modal-question.spec.js b/src/layouts/payment-method/promo-modal-question/promo-modal-question.spec.js new file mode 100644 index 000000000..3846a6bfd --- /dev/null +++ b/src/layouts/payment-method/promo-modal-question/promo-modal-question.spec.js @@ -0,0 +1,90 @@ +import { mount, shallowMount } from "@vue/test-utils"; +import promoModalQuestion from "./promo-modal-question"; + +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(), + }, +})); + +const linkWidgetName = "linkWidgetName"; +const modalWidgetName = "modalWidgetName"; + +const mockLinkCmsContent = { + BodyText: "Sample link body text here.", +}; + +const mockModalCmsContent = { + FooterText: "Sample modal footer text here.", +}; + +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + if (widgetName === linkWidgetName) { + return mockLinkCmsContent[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: { + linkWidgetName: linkWidgetName, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + wrapper.vm.onModalClosed(); + + // Assert + expect(wrapper.vm.displayInvalidPromoAlert).toBe(false); + expect(wrapper.vm.displayInvalidOnOrderPromoAlert).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, + linkWidgetName: linkWidgetName, + modalWidgetName: modalWidgetName, + }, + attachTo: document.body, + }); + + // Act + + await wrapper.vm.onModalClosed(); + + // Assert + expect(wrapper.emitted("update:modelValue")).toEqual([[lineItems]]); + }); +}); diff --git a/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue b/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue new file mode 100644 index 000000000..ac42fce2e --- /dev/null +++ b/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue @@ -0,0 +1,266 @@ + + + diff --git a/src/layouts/payment-method/promo-modal-question/promo-question/promo-question.spec.js b/src/layouts/payment-method/promo-modal-question/promo-question/promo-question.spec.js new file mode 100644 index 000000000..97a4346ad --- /dev/null +++ b/src/layouts/payment-method/promo-modal-question/promo-question/promo-question.spec.js @@ -0,0 +1,40 @@ +import { shallowMount } from "@vue/test-utils"; +import promoQuestion from "./promo-question"; + +describe("promo-question.vue", () => { + it("Should get the modelValue", async () => { + // Arrange + const text = "test"; + const wrapper = shallowMount(promoQuestion, { + props: { + modelValue: text, + }, + attachTo: document.body, + }); + + // Act + const modelValueText = wrapper.vm.value; + wrapper.vm.value = "test also"; + + // Assert + expect(modelValueText).toEqual("test"); + }); + + it("Should emit to set value", async () => { + // Arrange + const text = "test"; + const wrapper = shallowMount(promoQuestion, { + props: { + modelValue: text, + }, + attachTo: document.body, + }); + + // Act + const modelValueText = wrapper.vm.value; + wrapper.vm.value = "test also"; + + // Assert + expect(wrapper.emitted("update:modelValue")).toEqual([["test also"]]); + }); +}); diff --git a/src/layouts/payment-method/promo-modal-question/promo-question/promo-question.vue b/src/layouts/payment-method/promo-modal-question/promo-question/promo-question.vue new file mode 100644 index 000000000..25744e652 --- /dev/null +++ b/src/layouts/payment-method/promo-modal-question/promo-question/promo-question.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index 54e49c620..8088cbb06 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -131,6 +131,7 @@ export default { } this.$refs.loadingModal.isModalVisible = false; + await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_ORDER); this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_SUCCESS, this.$route); }, forwardButtonAction() { diff --git a/src/router/index.js b/src/router/index.js index cec3ff193..f7ff73a94 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -169,7 +169,16 @@ router.beforeEach(async (to, from, next) => { showFmgLoadingModal(true); } - next(); + const toQueryPage = to.query?.fmgPage; + const notToPIAReturn = toQueryPage != "payment-pia-return"; + const isInIframe = window !== window.top; + + if (isInIframe && notToPIAReturn) { + const newUrl = `${window.top.location.origin}${to.href}`; + window.top.location.href = newUrl; + } else { + next(); + } }); router.afterEach(async (to, from) => { diff --git a/src/store/index.js b/src/store/index.js index e8edb1ed2..941b65aca 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -23,7 +23,10 @@ import { getDisplayTextForDurationLength, } from "@/layouts/schedule/helpers/schedule-helper"; import { paymentMethods } from "@/constants/payment-method-constants"; -import { getPromoCodeWithoutBundleIdentifier } from "@/helpers/promotions-helper"; +import { + getPromoCodeWithoutBundleIdentifier, + removeCurrentlyActivePromoCodesFromInactivePromos, +} from "@/helpers/promotions-helper"; import { getDateDifferenceInDays } from "@/helpers/date-helper"; // Export State const getDefaultState = () => { @@ -543,7 +546,6 @@ export const mutations = { state.order.customer.phoneNumber = sessionInformation.order.customer.phoneNumber; state.order.customer.isSmsOptIn = sessionInformation.order.customer.isSmsOptIn; - state.order.existingPromoCode = sessionInformation.order.existingPromoCode; state.applicationUser.experiments = sessionInformation.applicationUser.experiments; state.applicationUser.crmCustomerId = sessionInformation.applicationUser.crmCustomerId; state.applicationUser.pageData = sessionInformation.applicationUser.pageData; @@ -1715,7 +1717,6 @@ export const actions = { jobMaxMinutes: order.schedule?.jobMaxMinutes, jobMinMinutes: order.schedule?.jobMinMinutes, }, - existingPromoCode: null, referralCorrelationId: order.referralCorrelationId, referralDate: order.referralDate, referralNumber: order.referralNumber?.toString(), @@ -2120,13 +2121,6 @@ export const actions = { addGuidToLineItemsIfNotAlreadyThere(vaps); context.commit(storeMutations.UPDATE_VAPS, vaps); }, - // COMBINE THESE - savePromos(context, promos) { - context.commit(storeMutations.UPDATE_PROMOS, promos); - }, - saveInactivePromos(context, inactivePromos) { - context.commit(storeMutations.UPDATE_INACTIVE_PROMOS, inactivePromos); - }, // Manage promo saving to ensure a promoCode never ends up in both active and inactive saveActiveAndOrInactivePromos(context, { activePromos = null, inactivePromos = null }) { let activePromosToSave; @@ -2138,12 +2132,10 @@ export const actions = { } else if (activePromos && inactivePromos) { // Prioritize active promos when both inactive and active are supplied activePromosToSave = activePromos; - const activePromoCodes = activePromos.map((promoObject) => - getPromoCodeWithoutBundleIdentifier(promoObject.promoCode) + inactivePromosToSave = removeCurrentlyActivePromoCodesFromInactivePromos( + activePromosToSave, + inactivePromos ); - inactivePromosToSave = inactivePromos.filter((inactivePromo) => { - return !activePromoCodes.includes(inactivePromo); - }); } else if (!activePromos) { // Only inactivePromos supplied inactivePromosToSave = inactivePromos; @@ -2155,13 +2147,9 @@ export const actions = { } else if (!inactivePromos) { // Only activePromos supplied activePromosToSave = activePromos; - const activePromoCodes = activePromos.map((promoObject) => - getPromoCodeWithoutBundleIdentifier(promoObject.promoCode) - ); - inactivePromosToSave = (context.getters.payment.inactivePromos ?? []).filter( - (inactivePromo) => { - return !activePromoCodes.includes(inactivePromo); - } + inactivePromosToSave = removeCurrentlyActivePromoCodesFromInactivePromos( + activePromosToSave, + context.getters.payment.inactivePromos ?? [] ); } context.commit(storeMutations.UPDATE_PROMOS, activePromosToSave); @@ -2360,10 +2348,15 @@ export const actions = { const order = context.getters.order; activePromosToUse = activePromosToUse ?? order.lineItems.promos; - inactivePromosToUse = inactivePromosToUse ?? order.payment.inactivePromos; lineItemsToUse = lineItemsToUse ? deepClone(lineItemsToUse) : deepClone(order.lineItems); lineItemsToUse.promos = activePromosToUse; + inactivePromosToUse = inactivePromosToUse ?? order.payment.inactivePromos; + inactivePromosToUse = removeCurrentlyActivePromoCodesFromInactivePromos( + lineItemsToUse.promos, + inactivePromosToUse + ); + let requestObject = { inactivePromos: inactivePromosToUse, order: { @@ -2797,15 +2790,15 @@ function convertGlassPieceToBackEndCompatibleFormat(glassPieces) { function renameGlassToReplaceAttributes(glassToReplace) { let newGlassToReplace = []; if (glassToReplace) { - glassToReplace.forEach((item) => { - newGlassToReplace.push({ location: item.glassLocation, name: item.glassName }); + newGlassToReplace = glassToReplace.map((item) => { + return { location: item.glassLocation, name: item.glassName }; }); } return newGlassToReplace; } function addGuidToLineItemsIfNotAlreadyThere(lineItems) { - lineItems.forEach((lineItem) => { + lineItems?.forEach((lineItem) => { if (!lineItem.id) { lineItem.id = crypto.randomUUID(); }