diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 1861ac2c1..daa65d2cf 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -86,8 +86,7 @@ const storeActions = { SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING: "saveSupportingItemsSuppressingStateResetting", SAVE_VAPS: "saveVaps", - SAVE_PROMOS: "savePromos", - SAVE_INACTIVE_PROMOS: "saveInactivePromos", + SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS: "saveActiveAndOrInactivePromos", SAVE_CUSTOMER_DETAILS: "saveCustomerDetails", SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes", SAVE_CCTOKEN: "saveCCToken", diff --git a/src/helpers/promotions-helper.js b/src/helpers/promotions-helper.js index 90e3b74f5..d18a4aa17 100644 --- a/src/helpers/promotions-helper.js +++ b/src/helpers/promotions-helper.js @@ -17,9 +17,31 @@ export const addableVapsPromoPartNumbers = [ ]; const promoErrorCodes = { - CANNOT_COMBINE: "SimilarPromoAlreadyOnOrder", + UNKNOWN: "Unknown", + PROMO_NOT_YET_IN_USE: "PromoNotYetInUse", + PROMO_USAGE_COUNT_EXCEEDED: "PromoUsageCountExceeded", + PROMO_EXPIRED: "PromoExpired", + PROMO_DOES_NOT_EXIST: "PromoDoesNotExist", + PROMO_STACKING_NOT_ALLOWED: "PromoStackingNotAllowed", + SIMILAR_PROMO_ALREADY_ON_ORDER: "SimilarPromoAlreadyOnOrder", + INVALID_PROMO_ON_ORDER: "InvalidPromoOnOrder", + PROMO_VALIDATION_ERROR: "PromoValidationError", + NO_SUITABLE_ITEM_FOR_PROMO: "NoSuitableItemForPromo", }; +const stackingPromoErrorCodes = [ + promoErrorCodes.PROMO_STACKING_NOT_ALLOWED, + promoErrorCodes.SIMILAR_PROMO_ALREADY_ON_ORDER, +]; + +const excludeFromInactivePromoErrorCodes = [ + promoErrorCodes.UNKNOWN, + promoErrorCodes.PROMO_NOT_YET_IN_USE, + promoErrorCodes.PROMO_USAGE_COUNT_EXCEEDED, + promoErrorCodes.PROMO_EXPIRED, + promoErrorCodes.PROMO_DOES_NOT_EXIST, +]; + export const pagesToStripPromoQueryStringFrom = ["quote", "payment-method"]; export function getPromosWithAddableVaps(promoList) { @@ -67,35 +89,36 @@ export function removeVapsPromosFromPromoArray(promoArray) { return filteredPromoArray; } -export async function revalidatePromosAndValidateNewPromo( +export async function revalidatePromosAndValidateQueryStringPromo( newPromo, pricedLineItems, pageNameToLog ) { const hasActivePromos = store.getters.order.lineItems.promos; - const hasHasInactivePromos = store.getters.order.lineItems.inactivePromos; + const hasInactivePromos = store.getters.payment.inactivePromos; const hasNewPromo = !!newPromo; const addableVaps = getAddableVapsFromAvailableLineItems(pricedLineItems); let validatePromoResponse = null; let revalidatePromoResponse = null; - if (hasActivePromos || hasHasInactivePromos) { + if ( + (hasActivePromos && hasActivePromos.length) || + (hasInactivePromos && hasInactivePromos.length) + ) { revalidatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA, {}, pageNameToLog, false ); - baseMixin.methods.dispatchStoreAction( - storeActions.SAVE_PROMOS, - revalidatePromoResponse.promoLineItems, - false - ); const revalidationErrorPromoCodes = revalidatePromoResponse.errors.map((x) => x.promoCode); baseMixin.methods.dispatchStoreAction( - storeActions.SAVE_INACTIVE_PROMOS, - revalidationErrorPromoCodes, + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { + activePromos: revalidatePromoResponse.promoLineItems, + inactivePromos: revalidationErrorPromoCodes, + }, false ); } @@ -109,8 +132,22 @@ export async function revalidatePromosAndValidateNewPromo( pageNameToLog, false ); + if (!excludeFromInactivePromoErrorCodes.includes(validatePromoResponse.errorCode)) { + // Save any valid (applied or not) query string promoCode to inactivePromos + // in order to not lose the query string promoCode if back button is pressed + const inactivePromoCodes = store.getters.payment.inactivePromos ?? []; + if (!inactivePromoCodes.includes(newPromo)) { + inactivePromoCodes.push(newPromo); + baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { + inactivePromos: inactivePromoCodes, + }, + false + ); + } + } } - return { validatePromoResponse, revalidatePromoResponse }; } @@ -243,7 +280,7 @@ function getPromoCodesFromPromoObjectsWithoutDuplicates(promos) { function createPromoSuccessAlert(promoCode) { return { messageHeadline: "Promo applied!", - messageCopy: `Promo "${promoCode}" was successfully applied to your cart.`, + messageCopy: `Promo "${promoCode.toUpperCase()}" was successfully applied to your cart.`, type: "alert-success", isDismissible: true, shouldAutoFade: true, @@ -257,10 +294,10 @@ function createPromoErrorAlert(promoCode, errorCode = null, additionalInfo) { shouldAutoFade: false, isDismissible: true, }; - if (errorCode == promoErrorCodes.CANNOT_COMBINE) { - alert.messageCopy = `Sorry, promo code "${promoCode}" cannot be combined with ${additionalInfo[0]}`; + if (stackingPromoErrorCodes.includes(errorCode)) { + alert.messageCopy = `Sorry, promo code "${promoCode.toUpperCase()}" cannot be combined with "${additionalInfo[0].toUpperCase()}"`; } else { - alert.messageCopy = `Sorry, promo code "${promoCode}" is not valid`; + alert.messageCopy = `Sorry, promo code "${promoCode.toUpperCase()}" is not valid`; } return alert; } diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index a7bda52cb..7a43b7440 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -86,7 +86,7 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { paymentMethods } from "@/constants/payment-method-constants"; import { experimentSettings } from "@/constants/experiments"; import { - revalidatePromosAndValidateNewPromo, + revalidatePromosAndValidateQueryStringPromo, buildToastMessagesFromRevalidateOrValidatePromoResponse, getVapsThatNeedToBeAddedToSatisfyPromos, } from "@/helpers/promotions-helper"; @@ -165,7 +165,7 @@ export default { const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO); const { validatePromoResponse, revalidatePromoResponse } = - await revalidatePromosAndValidateNewPromo( + await revalidatePromosAndValidateQueryStringPromo( promoCodeFromQueryString, pricedLineItemsToTax, "payment-method" @@ -360,10 +360,12 @@ export default { false ); await this.dispatchStoreAction(storeActions.SAVE_VAPS, this.lineItems.vaps, false); - await this.dispatchStoreAction(storeActions.SAVE_PROMOS, this.lineItems.promos, false); - await this.dispatchStoreAction( - storeActions.SAVE_INACTIVE_PROMOS, - this.inactivePromos, + this.dispatchStoreAction( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { + activePromos: this.lineItems.promos, + inactivePromos: this.inactivePromos, + }, false ); diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index a0c75fe82..132acccab 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -190,7 +190,7 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import baseMixin from "@/mixins/base-mixin.js"; import { mapTaxedLineItemsToStoreFormat } from "../../store"; import { - revalidatePromosAndValidateNewPromo, + revalidatePromosAndValidateQueryStringPromo, getAddableVapsFromAvailableLineItems, getVapsThatNeedToBeAddedToSatisfyPromos, } from "@/helpers/promotions-helper"; @@ -324,7 +324,7 @@ export default { const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO); const { validatePromoResponse, revalidatePromoResponse } = - await revalidatePromosAndValidateNewPromo( + await revalidatePromosAndValidateQueryStringPromo( promoCodeFromQueryString, pricedLineItemsToTax, "payment" diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index 61242cb33..a41f73595 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -22,7 +22,7 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ })); jest.mock("@/helpers/promotions-helper", () => ({ - revalidatePromosAndValidateNewPromo: jest.fn(() => { + revalidatePromosAndValidateQueryStringPromo: jest.fn(() => { return { validatePromoResponse: null, revalidatePromoResponse: null }; }), })); diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index fb635c226..6f293c43b 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -81,7 +81,7 @@ import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigat import { applicationConfig } from "@/constants/application-config"; import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states"; import { - revalidatePromosAndValidateNewPromo, + revalidatePromosAndValidateQueryStringPromo, buildToastMessagesFromRevalidateOrValidatePromoResponse, } from "@/helpers/promotions-helper"; import { queryStrings } from "@/constants/query-strings"; @@ -161,7 +161,7 @@ export default { const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO); const { validatePromoResponse, revalidatePromoResponse } = - await revalidatePromosAndValidateNewPromo( + await revalidatePromosAndValidateQueryStringPromo( promoCodeFromQueryString, pricingResults, "quote" @@ -291,7 +291,11 @@ export default { } this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false); - this.dispatchStoreAction(this.storeActions.SAVE_PROMOS, this.allActivePromos, false); + this.dispatchStoreAction( + this.storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { activePromos: this.allActivePromos }, + false + ); const payment = this.$store.getters.payment; if (payment.isInsurance) { diff --git a/src/store/index.js b/src/store/index.js index cffd8fee2..c55377ee3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -23,7 +23,7 @@ import { getDisplayTextForDurationLength, } from "@/layouts/schedule/helpers/schedule-helper"; import { paymentMethods } from "@/constants/payment-method-constants"; - +import { getPromoCodeWithoutBundleIdentifier } from "@/helpers/promotions-helper"; import { getDateDifferenceInDays } from "@/helpers/date-helper"; // Export State const getDefaultState = () => { @@ -2125,6 +2125,46 @@ export const actions = { 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; + let inactivePromosToSave; + if (!activePromos && !inactivePromos) { + // Allow double null to save empty arrays to store + activePromosToSave = []; + inactivePromosToSave = []; + } 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 = inactivePromos.filter((inactivePromo) => { + return !activePromoCodes.includes(inactivePromo); + }); + } else if (!activePromos) { + // Only inactivePromos supplied + inactivePromosToSave = inactivePromos; + activePromosToSave = (context.getters.lineItems.promos ?? []).filter((activePromo) => { + return !inactivePromosToSave.includes( + getPromoCodeWithoutBundleIdentifier(activePromo.promoCode) + ); + }); + } 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); + } + ); + } + context.commit(storeMutations.UPDATE_PROMOS, activePromosToSave); + context.commit(storeMutations.UPDATE_INACTIVE_PROMOS, inactivePromosToSave); + }, // Price order actions async priceOrderItemsAndSaveServerData(