diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d2a61a5d3..a7b9b705a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -97,9 +97,7 @@ stages: __VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) - __VUE_APP_SAFELITE_HOP___: $(__VUE_APP_SAFELITE_HOP__) - __VUE_APP_PAYPAL_SUCCESS_URL__: $(__VUE_APP_PAYPAL_SUCCESS_URL__) - __VUE_APP_PAYPAL_CANCEL_URL__: $(__VUE_APP_PAYPAL_CANCEL_URL__) + __VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__) __VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__) __VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__) indexDeployVariables: @@ -158,9 +156,7 @@ stages: __VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) - __VUE_APP_SAFELITE_HOP___: $(__VUE_APP_SAFELITE_HOP__) - __VUE_APP_PAYPAL_SUCCESS_URL__: $(__VUE_APP_PAYPAL_SUCCESS_URL__) - __VUE_APP_PAYPAL_CANCEL_URL__: $(__VUE_APP_PAYPAL_CANCEL_URL__) + __VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__) __VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__) __VUE_APP_MY_ACCOUNT__: $(__VUE_APP_MY_ACCOUNT__) indexDeployVariables: @@ -219,9 +215,7 @@ stages: __VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) - __VUE_APP_SAFELITE_HOP___: $(__VUE_APP_SAFELITE_HOP__) - __VUE_APP_PAYPAL_SUCCESS_URL__: $(__VUE_APP_PAYPAL_SUCCESS_URL__) - __VUE_APP_PAYPAL_CANCEL_URL__: $(__VUE_APP_PAYPAL_CANCEL_URL__) + __VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__) __VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__) indexDeployVariables: __VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) @@ -279,9 +273,7 @@ stages: __VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__) __VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__) __VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__) - __VUE_APP_SAFELITE_HOP___: $(__VUE_APP_SAFELITE_HOP__) - __VUE_APP_PAYPAL_SUCCESS_URL__: $(__VUE_APP_PAYPAL_SUCCESS_URL__) - __VUE_APP_PAYPAL_CANCEL_URL__: $(__VUE_APP_PAYPAL_CANCEL_URL__) + __VUE_APP_SAFELITE_HOP__: $(__VUE_APP_SAFELITE_HOP__) __VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__) indexDeployVariables: __VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) diff --git a/jest.config.js b/jest.config.js index 93249f226..f114ef2c3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,9 +27,9 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 78, + statements: 77, }, }, // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit - // silent: true, + silent: true, }; diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 808acfe0a..b4c0d9a1e 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -11,6 +11,16 @@ const applicationConfig = { SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass", CASH_PARENT_ACCOUNT_NUMBER: 167132, MY_ACCOUNT: process.env.VUE_APP_MY_ACCOUNT, + PAYPAL_SUCCESS_URL: + location.protocol + + "//" + + location.host + + "/fmg/?fmgPage=payment-pia-return&src=concept-funnel", + PAYPAL_CANCEL_URL: + location.protocol + + "//" + + location.host + + "/fmg/?fmgPage=payment-method&src=concept-funnel", }; export { applicationConfig }; diff --git a/src/constants/experiments.js b/src/constants/experiments.js index f89aad963..b702b11bb 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -6,6 +6,8 @@ const experimentSettings = { GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index", SUPPRESS_VIN_CAPTURE: "SuppressVinCapture", DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators", + PIA_EXPERIENCE: "PIA Experience", + SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA", }; const experimentTriggers = { diff --git a/src/constants/payment-method-constants.js b/src/constants/payment-method-constants.js new file mode 100644 index 000000000..807e44c0e --- /dev/null +++ b/src/constants/payment-method-constants.js @@ -0,0 +1,13 @@ +export const paymentMethods = { + NONE: null, + LATER: "Later", + CREDIT_CARD: "CreditCard", + PAYPAL: "Paypal", + AFTERPAY: "Afterpay", +}; + +export const paymentTimes = { + NONE: null, + ADVANCE: "Advance", + LATER: "Later", +}; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 420e8a2de..288ceb67b 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -87,6 +87,7 @@ const storeActions = { SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes", SAVE_WORK_ORDER_FLAG: "saveWorkOrderFlag", SAVE_PIA_ERROR_CODE: "savePiaErrorCode", + SAVE_PIA_WORK_ORDER: "savePiaWorkOrder", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index f3f4a3c53..632379de3 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -1,6 +1,7 @@ const storeMutations = { UPDATE_WORK_ORDER_FLAG: "updateWorkOrderFlag", UPDATE_PIA_ERROR_CODE: "updatePiaErrorCode", + UPDATE_PIA_WORK_ORDER: "updatePiaWorkOrder", // VEHICLE MUTATIONS UPDATE_YEAR: "updateYear", diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index ffbb8fd79..cec014712 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -192,7 +192,7 @@ export default { classes = "d-flex flex-row p-0"; break; case "listCard": - classes = "row g-2 g-md-5 justify-content-center mb-1"; + classes = "row g-2 g-md-5 justify-content-center mb-1 flex-nowrap"; if (this.isWide) { classes += "flex-column"; } @@ -223,6 +223,10 @@ export default { classes = "package-wrapper"; } + if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) { + classes += " two-list-card-width"; + } + return classes; }, buttonsInfo() { @@ -304,6 +308,10 @@ export default { diff --git a/src/layouts/payment-method/payment-method-question/payment-method-question.spec.js b/src/layouts/payment-method/payment-method-question/payment-method-question.spec.js new file mode 100644 index 000000000..d3a486d57 --- /dev/null +++ b/src/layouts/payment-method/payment-method-question/payment-method-question.spec.js @@ -0,0 +1,130 @@ +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; + +import paymentMethodQuestion from "@/layouts/payment-method/payment-method-question/payment-method-question"; + +const testConstants = { + cms: { + question: { + text: "Choose a payment option", + }, + answers: { + optionA: { + name: "NameA", + text: "TextA", + subText: "SubTextA", + imageId: "idA", + image: "imageA", + subWidget: "subWidgetA", + }, + optionB: { + name: "NameB", + text: "TextB", + subText: "SubTextB", + imageId: "idB", + image: "imageB", + subWidget: "subWidgetB", + }, + }, + }, +}; + +let cmsContent; + +describe("Payment Method Question", () => { + beforeEach(() => { + cmsContent = { + PaymentMethodWidget: { + QuestionText: testConstants.cms.question, + Answers: [ + { + Name: testConstants.cms.answers.optionA.name, + Text: testConstants.cms.answers.optionA.text, + SubText: testConstants.cms.answers.optionA.subText, + ImageId: testConstants.cms.answers.optionA.imageId, + AnswerImageUrl: testConstants.cms.answers.optionA.image, + SubWidgetName: testConstants.cms.answers.optionA.subWidget, + }, + { + Name: testConstants.cms.answers.optionB.name, + Text: testConstants.cms.answers.optionB.text, + SubText: testConstants.cms.answers.optionB.subText, + ImageId: testConstants.cms.answers.optionB.imageId, + AnswerImageUrl: testConstants.cms.answers.optionB.image, + SubWidgetName: testConstants.cms.answers.optionB.subWidget, + }, + ], + }, + }; + }); + + it("Should map cms content correctly", () => { + // Arrange + const props = generateDefaultProps(); + + const { wrapper } = setupMocks({ + propsData: props, + }); + + // Act + const mappedContent = wrapper.vm.paymentMethodAnswerData; + + // Assert + expect(mappedContent).toEqual([ + { + buttonLabel: testConstants.cms.answers.optionA.text, + altText: testConstants.cms.answers.optionA.text, + groupName: "payment-method", + value: testConstants.cms.answers.optionA.name, + buttonImage: testConstants.cms.answers.optionA.image, + }, + { + buttonLabel: testConstants.cms.answers.optionB.text, + altText: testConstants.cms.answers.optionB.text, + groupName: "payment-method", + value: testConstants.cms.answers.optionB.name, + buttonImage: testConstants.cms.answers.optionB.image, + }, + ]); + }); + + it("Handles null cms content gracefully", () => { + // Arrange + cmsContent = {}; + const props = generateDefaultProps(); + + const { wrapper } = setupMocks({ + propsData: props, + }); + + // Act + const mappedContent = wrapper.vm.paymentMethodAnswerData; + + // Assert + expect(mappedContent).toEqual([]); + }); +}); + +function generateDefaultProps() { + return { + modelValue: "modelValue", + }; +} + +function setupMocks(customMountOptions) { + const mountOptions = getMountOptions(customMountOptions); + + const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + return cmsContent?.[widgetName]?.[cmsFieldName] ?? ""; + }), + }, + }; + + mountOptions.global.mixins = [mockMixin]; + + const wrapper = shallowMount(paymentMethodQuestion, mountOptions); + wrapper.vm.setCmsContent = jest.fn(); + return { wrapper }; +} diff --git a/src/layouts/payment-method/payment-method-question/payment-method-question.vue b/src/layouts/payment-method/payment-method-question/payment-method-question.vue new file mode 100644 index 000000000..473e36114 --- /dev/null +++ b/src/layouts/payment-method/payment-method-question/payment-method-question.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index 130a2def4..803f10829 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -5,6 +5,7 @@ import customerDetails from "@/layouts/payment-method/payment-method.vue"; import { shallowMount } from "@vue/test-utils"; import { getMountOptions } from "@/helpers/unit-test-helper.js"; import store from "@/store"; +import { experimentSettings } from "@/constants/experiments"; // Mock our module for promises. jest.mock("@/helpers/layout-helper.js", () => ({ @@ -15,6 +16,8 @@ jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({ submitWorkOrder: jest.fn(), })); +let piaDisabledFlag = false; + describe("payment-method.vue", () => { describe("navigation", () => { test("if the back button is clicked, navigate back", async () => { @@ -52,19 +55,37 @@ function setupMocks() { }, }; - const wrapper = shallowMount( - customerDetails, - getMountOptions({ - router: { - navigate: jest.fn(), - navigateWithSaving: jest.fn(), - navigateWithoutSaving: jest.fn(), - }, - store: { - getters: store.getters, - }, - }) - ); + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn(), + navigateWithSaving: jest.fn(), + navigateWithoutSaving: jest.fn(), + }, + store: { + getters: store.getters, + }, + }); + + const mockMixin = { + methods: { + getSettingValue: jest.fn((settingName) => { + if (!piaDisabledFlag) { + if (settingName === experimentSettings.PIA_EXPERIENCE) { + return "PIA Optional"; + } + if (settingName === experimentSettings.SUBMIT_ORDER_ENABLE_PIA) { + return "true"; + } + } + + return "false"; + }), + }, + }; + + mountOptions.global.mixins = [mockMixin]; + + const wrapper = shallowMount(customerDetails, mountOptions); return { wrapper }; } diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 2b34185f4..eac00fe15 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -17,9 +17,10 @@ + servicePackageOptionsCmsName="ServicePackageTitle" + @cart-remove="cartRemove" />
-
- - - - -
-
- -
+ + { vm.setCmsContent(resultMap.cmsContent); - vm.pricedGlassParts = clonedGlassParts; - vm.supportingItems = resultMap.supportingItems; vm.availableLineItems = pricingResults; vm.availableWipers = resultMap.wipers; vm.availableRainDefense = resultMap.rainDefense; + vm.cartItems = cartItems; }); }, data() { @@ -174,11 +161,61 @@ export default { supportingItems: null, pricedGlassParts: null, displayPaypalAlert: this.getPaypalAlert(), + paymentMethodInternalModel: null, }; }, methods: { arePagePrerequisitesValid() { - return true; + // Line Items + const packageReqs = !!store.getters.order.lineItems.supportingItems; + + // Service Location + const serviceLocation = store.getters.order.serviceLocation; + const mobileReqs = !!( + serviceLocation.address && + serviceLocation.city && + serviceLocation.state && + serviceLocation.zipCode + ); + + const providerLocation = serviceLocation.provider.address; + const dropOffInshopReqs = !!( + providerLocation.streetAddress && + providerLocation.city && + providerLocation.state && + providerLocation.zipCode + ); + + const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; + + const serviceLocationReqs = + (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs); + + // Insurance + const isInsuranceSet = store.getters.order.payment.isInsurance !== null; + + // Schedule + const schedule = store.getters.order.schedule; + const scheduleReqs = !!( + schedule.date && + schedule.startTime && + (!isMobile || schedule.endTime) && + schedule.jobMaxMinutes && + schedule.jobMinMinutes + ); + + // Customer + const customer = store.getters.order.customer; + const customerReqs = !!( + customer.firstName && + customer.lastName && + customer.phoneNumber && + customer.emailAddress + ); + + return ( + packageReqs && serviceLocationReqs && isInsuranceSet && scheduleReqs && customerReqs + ); }, getPaypalAlert() { return store.getters.order.payment.piaErrorCode ? true : false; @@ -186,6 +223,22 @@ export default { vapsItemsSelectedAction(vapsItemsSelected) { this.cartItems = vapsItemsSelected; }, + // TODO (as part of CSR-1384) enable adding to the cart + // vapsItemsSelectedAction(vapsItemsSelected) { + // this.cartItems = vapsItemsSelected; + // }, + cartRemove(item) { + const matchedIndices = []; + this.cartItems.forEach((cartItem, i) => { + if (cartItem.partType === item.partType) { + console.log("adding this to matchedIndices: ", i); + matchedIndices.push(i); + } + }); + this.cartItems = this.cartItems.filter((cartItem, index) => { + return cartItem.partType !== item.partType; + }); + }, backButtonAction() { this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false); this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); @@ -200,22 +253,29 @@ export default { ); }, async ccdPayment() { - //leaving site, clear any save session promises that we will not be able to resolve when we return - this.$refs.loadingModal.showModal(); - this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false); - await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - await this.dispatchStoreAction(storeActions.SAVE_PAY_NOW_TYPE, "cc", false); - this.$router.navigateWithoutSaving( - this.navigationScenarios.CLICKED_PAY_NOW, - this.$route - ); + this.piaSetup("cc"); }, async paypal() { - //leaving site, clear any save session promises that we will not be able to resolve when we return + this.piaSetup("pp"); + }, + async afterpay() { + this.piaSetup("ap"); + }, + async piaSetup(payNowType) { this.$refs.loadingModal.showModal(); - this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false); + + //leaving site, clear any save session promises that we will not be able to resolve when we return await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - await this.dispatchStoreAction(storeActions.SAVE_PAY_NOW_TYPE, "pp", false); + await this.dispatchStoreAction(storeActions.SAVE_PAY_NOW_TYPE, payNowType, false); + + // make sure pia error codes are reset, and get a pia work order in delete status + this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false); + this.dispatchStoreAction(storeActions.SAVE_PIA_WORK_ORDER, true); + + if (!store.getters.order.workOrderNumber) { + await submitWorkOrder({ pageNameToLog: "payment-method" }); + } + this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_PAY_NOW, this.$route @@ -226,9 +286,42 @@ export default { damageInfo() { return this.$store.getters.damage; }, - orderLineItems() { + storeLineItems() { + // console.log("storeLineItems, aka lineItems from store: ", this.$store.getters.lineItems) return this.$store.getters.lineItems; }, + customCtaCopy() { + switch (this.paymentMethod) { + case paymentMethods.CREDIT_CARD: + return "Continue to checkout"; + case paymentMethods.PAYPAL: + return "Continue to Paypal"; + case paymentMethods.AFTERPAY: + return "Continue to Afterpay"; + default: + return null; + } + }, + isPiaDisabled() { + const piaExperience = + this.getSettingValue(experimentSettings.PIA_EXPERIENCE) === "PIA Optional"; + const enablePIA = + this.getSettingValue(experimentSettings.SUBMIT_ORDER_ENABLE_PIA) === "true"; + + return !(piaExperience && enablePIA); + }, + paymentMethod() { + if (this.isPiaDisabled) { + return paymentMethods.LATER; + } + + return this.paymentMethodInternalModel; + }, + }, + watch: { + customCtaCopy(newValue) { + this.$refs.navbar.updateButtonText(newValue); + }, }, components: { funnelHeader, @@ -239,6 +332,7 @@ export default { cart, alert, addVapsModalButtons, + paymentMethodQuestion, }, }; diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index 86051265c..8d547cd34 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -1,5 +1,7 @@ diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 0258f7f82..914a51df2 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -11,6 +11,7 @@