Merge pull request #1512 from Safelite/feature/skiener/CSR-1452
CSR-1452 | Promo alerts
This commit is contained in:
commit
90b499424e
9 changed files with 323 additions and 62 deletions
|
|
@ -86,8 +86,7 @@ const storeActions = {
|
||||||
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
||||||
"saveSupportingItemsSuppressingStateResetting",
|
"saveSupportingItemsSuppressingStateResetting",
|
||||||
SAVE_VAPS: "saveVaps",
|
SAVE_VAPS: "saveVaps",
|
||||||
SAVE_PROMOS: "savePromos",
|
SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS: "saveActiveAndOrInactivePromos",
|
||||||
SAVE_INACTIVE_PROMOS: "saveInactivePromos",
|
|
||||||
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
|
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
|
||||||
SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes",
|
SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes",
|
||||||
SAVE_CCTOKEN: "saveCCToken",
|
SAVE_CCTOKEN: "saveCCToken",
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ export default {
|
||||||
let cartItem = null;
|
let cartItem = null;
|
||||||
|
|
||||||
cartItem = {
|
cartItem = {
|
||||||
name: "Some Kind of Promo", // get from CMS?
|
name: promoLineItem.promoCode,
|
||||||
category: cartItemCategories.PROMOS,
|
category: cartItemCategories.PROMOS,
|
||||||
cartItemType: promoLineItem.partType,
|
cartItemType: promoLineItem.partType,
|
||||||
isDisplayed: true,
|
isDisplayed: true,
|
||||||
|
|
@ -230,6 +230,7 @@ export default {
|
||||||
(promoLineItem.laborAmount ?? 0) +
|
(promoLineItem.laborAmount ?? 0) +
|
||||||
(promoLineItem.sellingPrice ?? 0),
|
(promoLineItem.sellingPrice ?? 0),
|
||||||
salesTax: promoLineItem.salesTax,
|
salesTax: promoLineItem.salesTax,
|
||||||
|
lineItems: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
promoLineItem.cartItemType = cartItem.cartItemType;
|
promoLineItem.cartItemType = cartItem.cartItemType;
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,18 @@
|
||||||
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
|
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
|
||||||
<menuModal />
|
<menuModal />
|
||||||
</div>
|
</div>
|
||||||
|
<template v-for="globalAlert in globalAlertMessages" :key="globalAlert.id">
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<alert
|
<alert
|
||||||
v-if="displayGlobalAlert"
|
v-if="globalAlert.displayAlert"
|
||||||
class="rounded-0 w-100 border-0 shadow-sm"
|
class="rounded-0 w-100 border-0 shadow-sm"
|
||||||
cmsWidgetName="GlobalAlert"
|
cmsWidgetName="GlobalAlert"
|
||||||
:manualHeadline="globalAlertMessage.messageHeadline"
|
:manualHeadline="globalAlert.messageHeadline"
|
||||||
:manualCopy="globalAlertMessage.messageCopy"
|
:manualCopy="globalAlert.messageCopy"
|
||||||
:alertClass="globalAlertMessage.type"
|
:alertClass="globalAlert.type"
|
||||||
:onAlertCloseCallback="onAlertClose"
|
v-bind:isDismissible="globalAlert.isDismissible" />
|
||||||
v-bind:isDismissible="globalAlertMessage.isDismissible" />
|
</transition>
|
||||||
</transition>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -32,14 +32,8 @@ export default {
|
||||||
name: "funnel-header",
|
name: "funnel-header",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
displayGlobalAlert: false,
|
alertIdCounter: 0,
|
||||||
globalAlertMessage: {
|
globalAlertMessages: [],
|
||||||
isDismissible: false,
|
|
||||||
messageCopy: "",
|
|
||||||
messageHeadline: "",
|
|
||||||
type: "",
|
|
||||||
},
|
|
||||||
countdownTimer: () => {},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -51,18 +45,18 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
triggerGlobalAlert(alertEvent, isAutoDismissing) {
|
pushGlobalAlert(alertToPush, isAutoDismissing) {
|
||||||
clearTimeout(this.countdownTimer);
|
alertToPush.displayAlert = true;
|
||||||
this.displayGlobalAlert = true;
|
alertToPush.id = this.alertIdCounter;
|
||||||
this.globalAlertMessage = alertEvent;
|
this.alertIdCounter++;
|
||||||
if (isAutoDismissing) {
|
if (isAutoDismissing) {
|
||||||
this.countdownTimer = setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.displayGlobalAlert = false;
|
alertToPush.displayAlert = false;
|
||||||
|
// force a re-evaluation of the globalAlertMessages to detect the v-if change
|
||||||
|
this.globalAlertMessages = this.globalAlertMessages.splice(0);
|
||||||
}, ALERT_DURATION);
|
}, ALERT_DURATION);
|
||||||
}
|
}
|
||||||
},
|
this.globalAlertMessages.push(alertToPush);
|
||||||
onAlertClose() {
|
|
||||||
this.displayGlobalAlert = false;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -77,8 +71,8 @@ export default {
|
||||||
);
|
);
|
||||||
// If alert event is on the bus, then display the alert
|
// If alert event is on the bus, then display the alert
|
||||||
if (alertEvent !== undefined) {
|
if (alertEvent !== undefined) {
|
||||||
this.displayGlobalAlert = true;
|
alertEvent.displayAlert = true;
|
||||||
this.globalAlertMessage = alertEvent;
|
this.globalAlertMessages.push(alertEvent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,32 @@ export const addableVapsPromoPartNumbers = [
|
||||||
promoPartNumberStrings.WIPER_DISCOUNT_PART_NUMBER,
|
promoPartNumberStrings.WIPER_DISCOUNT_PART_NUMBER,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const promoErrorCodes = {
|
||||||
|
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 const pagesToStripPromoQueryStringFrom = ["quote", "payment-method"];
|
||||||
|
|
||||||
export function getPromosWithAddableVaps(promoList) {
|
export function getPromosWithAddableVaps(promoList) {
|
||||||
|
|
@ -63,35 +89,36 @@ export function removeVapsPromosFromPromoArray(promoArray) {
|
||||||
return filteredPromoArray;
|
return filteredPromoArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revalidatePromosAndValidateNewPromo(
|
export async function revalidatePromosAndValidateQueryStringPromo(
|
||||||
newPromo,
|
newPromo,
|
||||||
pricedLineItems,
|
pricedLineItems,
|
||||||
pageNameToLog
|
pageNameToLog
|
||||||
) {
|
) {
|
||||||
const hasActivePromos = store.getters.order.lineItems.promos;
|
const hasActivePromos = store.getters.order.lineItems.promos;
|
||||||
const hasHasInactivePromos = store.getters.order.lineItems.inactivePromos;
|
const hasInactivePromos = store.getters.payment.inactivePromos;
|
||||||
const hasNewPromo = !!newPromo;
|
const hasNewPromo = !!newPromo;
|
||||||
const addableVaps = getAddableVapsFromAvailableLineItems(pricedLineItems);
|
const addableVaps = getAddableVapsFromAvailableLineItems(pricedLineItems);
|
||||||
|
|
||||||
let validatePromoResponse = null;
|
let validatePromoResponse = null;
|
||||||
let revalidatePromoResponse = null;
|
let revalidatePromoResponse = null;
|
||||||
|
|
||||||
if (hasActivePromos || hasHasInactivePromos) {
|
if (
|
||||||
|
(hasActivePromos && hasActivePromos.length) ||
|
||||||
|
(hasInactivePromos && hasInactivePromos.length)
|
||||||
|
) {
|
||||||
revalidatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
revalidatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||||
storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA,
|
storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA,
|
||||||
{},
|
{},
|
||||||
pageNameToLog,
|
pageNameToLog,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
baseMixin.methods.dispatchStoreAction(
|
|
||||||
storeActions.SAVE_PROMOS,
|
|
||||||
revalidatePromoResponse.promoLineItems,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const revalidationErrorPromoCodes = revalidatePromoResponse.errors.map((x) => x.promoCode);
|
const revalidationErrorPromoCodes = revalidatePromoResponse.errors.map((x) => x.promoCode);
|
||||||
baseMixin.methods.dispatchStoreAction(
|
baseMixin.methods.dispatchStoreAction(
|
||||||
storeActions.SAVE_INACTIVE_PROMOS,
|
storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS,
|
||||||
revalidationErrorPromoCodes,
|
{
|
||||||
|
activePromos: revalidatePromoResponse.promoLineItems,
|
||||||
|
inactivePromos: revalidationErrorPromoCodes,
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -105,11 +132,78 @@ export async function revalidatePromosAndValidateNewPromo(
|
||||||
pageNameToLog,
|
pageNameToLog,
|
||||||
false
|
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 };
|
return { validatePromoResponse, revalidatePromoResponse };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||||
|
promoResponse,
|
||||||
|
oldActivePromos = [],
|
||||||
|
oldInactivePromos = []
|
||||||
|
) {
|
||||||
|
const alerts = [];
|
||||||
|
// Revalidate always has an "errors" array
|
||||||
|
if (promoResponse.errors) {
|
||||||
|
const oldPromoCodes = oldActivePromos.map((promo) =>
|
||||||
|
getPromoCodeWithoutBundleIdentifier(promo.promoCode)
|
||||||
|
);
|
||||||
|
const activePromoCodesFromResponse = getPromoCodesFromPromoObjectsWithoutDuplicates(
|
||||||
|
promoResponse.promoLineItems
|
||||||
|
);
|
||||||
|
const newlyActivatedPromoCodes = activePromoCodesFromResponse.filter(
|
||||||
|
(newPromoCode) => !oldPromoCodes.includes(newPromoCode)
|
||||||
|
);
|
||||||
|
|
||||||
|
newlyActivatedPromoCodes.forEach((newlyActivatedPromoCode) => {
|
||||||
|
alerts.push(createPromoSuccessAlert(newlyActivatedPromoCode));
|
||||||
|
});
|
||||||
|
|
||||||
|
const inactivePromoCodesFromResponse = getPromoCodesFromPromoObjectsWithoutDuplicates(
|
||||||
|
promoResponse.errors
|
||||||
|
);
|
||||||
|
const newlyInactivatedPromos = inactivePromoCodesFromResponse.filter(
|
||||||
|
(errorPromoCode) => !oldInactivePromos.includes(errorPromoCode)
|
||||||
|
);
|
||||||
|
|
||||||
|
newlyInactivatedPromos.forEach((newlyInactivatedPromo) => {
|
||||||
|
alerts.push(createPromoErrorAlert(newlyInactivatedPromo));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// validate response
|
||||||
|
else {
|
||||||
|
if (promoResponse.orderPromos) {
|
||||||
|
const promoCodeToDisplay = getPromoCodeWithoutBundleIdentifier(
|
||||||
|
promoResponse.orderPromos[0].promoCode
|
||||||
|
);
|
||||||
|
alerts.push(createPromoSuccessAlert(promoCodeToDisplay));
|
||||||
|
} else {
|
||||||
|
alerts.push(
|
||||||
|
createPromoErrorAlert(
|
||||||
|
promoResponse.promoCode,
|
||||||
|
promoResponse.errorCode,
|
||||||
|
promoResponse.additionalInfo
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
|
||||||
export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) {
|
export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) {
|
||||||
const matchingPromos = [];
|
const matchingPromos = [];
|
||||||
promos.forEach((promo) => {
|
promos.forEach((promo) => {
|
||||||
|
|
@ -152,6 +246,11 @@ export function getVapsThatNeedToBeAddedToSatisfyPromos(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bundle promos look like: "bundlePromo/###"" for each promo, this returns "bundlePromo" only
|
||||||
|
export function getPromoCodeWithoutBundleIdentifier(promoCode) {
|
||||||
|
return promoCode.split("/")[0];
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldStripPromoQueryString(fmgPageQueryValue) {
|
export function shouldStripPromoQueryString(fmgPageQueryValue) {
|
||||||
return pagesToStripPromoQueryStringFrom.includes(fmgPageQueryValue);
|
return pagesToStripPromoQueryStringFrom.includes(fmgPageQueryValue);
|
||||||
}
|
}
|
||||||
|
|
@ -163,3 +262,42 @@ function findLineItemsWithPartType(typeToFind, itemsToSearch) {
|
||||||
);
|
);
|
||||||
return partTypeMatches;
|
return partTypeMatches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns a list of promo codes (string) only without duplicates or bundle identifiers
|
||||||
|
// <promos> parameter must be an array of objects with a 'promoCode' field
|
||||||
|
function getPromoCodesFromPromoObjectsWithoutDuplicates(promos) {
|
||||||
|
const promoCodes = [];
|
||||||
|
promos.forEach((promo) => {
|
||||||
|
const promoCodeToDisplay = getPromoCodeWithoutBundleIdentifier(promo.promoCode);
|
||||||
|
if (!promoCodes.includes(promoCodeToDisplay)) {
|
||||||
|
promoCodes.push(promoCodeToDisplay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return promoCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPromoSuccessAlert(promoCode) {
|
||||||
|
return {
|
||||||
|
messageHeadline: "Promo applied!",
|
||||||
|
messageCopy: `Promo "${promoCode.toUpperCase()}" was successfully applied to your cart.`,
|
||||||
|
type: "alert-success",
|
||||||
|
isDismissible: true,
|
||||||
|
shouldAutoFade: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPromoErrorAlert(promoCode, errorCode = null, additionalInfo) {
|
||||||
|
const alert = {
|
||||||
|
messageHeadline: "Promo error",
|
||||||
|
type: "alert-danger",
|
||||||
|
shouldAutoFade: false,
|
||||||
|
isDismissible: true,
|
||||||
|
};
|
||||||
|
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.toUpperCase()}" is not valid`;
|
||||||
|
}
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,8 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
import { experimentSettings } from "@/constants/experiments";
|
import { experimentSettings } from "@/constants/experiments";
|
||||||
import {
|
import {
|
||||||
revalidatePromosAndValidateNewPromo,
|
revalidatePromosAndValidateQueryStringPromo,
|
||||||
getPromosWithAddableVaps,
|
buildToastMessagesFromRevalidateOrValidatePromoResponse,
|
||||||
getVapsThatNeedToBeAddedToSatisfyPromos,
|
getVapsThatNeedToBeAddedToSatisfyPromos,
|
||||||
} from "@/helpers/promotions-helper";
|
} from "@/helpers/promotions-helper";
|
||||||
import { queryStrings } from "@/constants/query-strings";
|
import { queryStrings } from "@/constants/query-strings";
|
||||||
|
|
@ -159,10 +159,13 @@ export default {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Promo logic
|
// Promo logic
|
||||||
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
// Populate the previous state of promos for toast message usage in "next()"
|
||||||
|
const oldActivePromos = store.getters.lineItems.promos?.slice(0);
|
||||||
|
const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0);
|
||||||
|
|
||||||
|
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
||||||
const { validatePromoResponse, revalidatePromoResponse } =
|
const { validatePromoResponse, revalidatePromoResponse } =
|
||||||
await revalidatePromosAndValidateNewPromo(
|
await revalidatePromosAndValidateQueryStringPromo(
|
||||||
promoCodeFromQueryString,
|
promoCodeFromQueryString,
|
||||||
pricedLineItemsToTax,
|
pricedLineItemsToTax,
|
||||||
"payment-method"
|
"payment-method"
|
||||||
|
|
@ -210,6 +213,26 @@ export default {
|
||||||
vm.availableVaps = taxedVaps;
|
vm.availableVaps = taxedVaps;
|
||||||
vm.lineItems = lineItems;
|
vm.lineItems = lineItems;
|
||||||
vm.updateFooterButtonText(vm.customCtaCopy);
|
vm.updateFooterButtonText(vm.customCtaCopy);
|
||||||
|
|
||||||
|
if (revalidatePromoResponse) {
|
||||||
|
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||||
|
revalidatePromoResponse,
|
||||||
|
oldActivePromos,
|
||||||
|
oldInactivePromos
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidateAlerts.forEach((alert) => {
|
||||||
|
vm.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (validatePromoResponse) {
|
||||||
|
const validateAlerts =
|
||||||
|
buildToastMessagesFromRevalidateOrValidatePromoResponse(validatePromoResponse);
|
||||||
|
vm.$refs.funnelHeader.pushGlobalAlert(
|
||||||
|
validateAlerts[0],
|
||||||
|
validateAlerts[0].shouldAutoFade
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -303,6 +326,14 @@ export default {
|
||||||
"payment-method",
|
"payment-method",
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||||
|
revalidatePromoResponse,
|
||||||
|
this.lineItems.promos,
|
||||||
|
this.inactivePromos
|
||||||
|
);
|
||||||
|
revalidateAlerts.forEach((alert) => {
|
||||||
|
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||||
|
});
|
||||||
|
|
||||||
this.lineItems.promos = revalidatePromoResponse.promoLineItems;
|
this.lineItems.promos = revalidatePromoResponse.promoLineItems;
|
||||||
this.inactivePromos = revalidatePromoResponse.errors.map((x) => x.promoCode);
|
this.inactivePromos = revalidatePromoResponse.errors.map((x) => x.promoCode);
|
||||||
|
|
@ -330,10 +361,12 @@ export default {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
await this.dispatchStoreAction(storeActions.SAVE_VAPS, this.lineItems.vaps, false);
|
await this.dispatchStoreAction(storeActions.SAVE_VAPS, this.lineItems.vaps, false);
|
||||||
await this.dispatchStoreAction(storeActions.SAVE_PROMOS, this.lineItems.promos, false);
|
this.dispatchStoreAction(
|
||||||
await this.dispatchStoreAction(
|
storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS,
|
||||||
storeActions.SAVE_INACTIVE_PROMOS,
|
{
|
||||||
this.inactivePromos,
|
activePromos: this.lineItems.promos,
|
||||||
|
inactivePromos: this.inactivePromos,
|
||||||
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -375,7 +408,7 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showAlert(alert) {
|
showAlert(alert) {
|
||||||
this.$refs.funnelHeader.triggerGlobalAlert(alert, true);
|
this.$refs.funnelHeader.pushGlobalAlert(alert, true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
import { mapTaxedLineItemsToStoreFormat } from "../../store";
|
import { mapTaxedLineItemsToStoreFormat } from "../../store";
|
||||||
import {
|
import {
|
||||||
revalidatePromosAndValidateNewPromo,
|
revalidatePromosAndValidateQueryStringPromo,
|
||||||
getAddableVapsFromAvailableLineItems,
|
getAddableVapsFromAvailableLineItems,
|
||||||
getVapsThatNeedToBeAddedToSatisfyPromos,
|
getVapsThatNeedToBeAddedToSatisfyPromos,
|
||||||
} from "@/helpers/promotions-helper";
|
} from "@/helpers/promotions-helper";
|
||||||
|
|
@ -324,7 +324,7 @@ export default {
|
||||||
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
||||||
|
|
||||||
const { validatePromoResponse, revalidatePromoResponse } =
|
const { validatePromoResponse, revalidatePromoResponse } =
|
||||||
await revalidatePromosAndValidateNewPromo(
|
await revalidatePromosAndValidateQueryStringPromo(
|
||||||
promoCodeFromQueryString,
|
promoCodeFromQueryString,
|
||||||
pricedLineItemsToTax,
|
pricedLineItemsToTax,
|
||||||
"payment"
|
"payment"
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@/helpers/promotions-helper", () => ({
|
jest.mock("@/helpers/promotions-helper", () => ({
|
||||||
revalidatePromosAndValidateNewPromo: jest.fn(() => {
|
revalidatePromosAndValidateQueryStringPromo: jest.fn(() => {
|
||||||
return { validatePromoResponse: null, revalidatePromoResponse: null };
|
return { validatePromoResponse: null, revalidatePromoResponse: null };
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
@ -258,10 +258,14 @@ describe("quote.vue", () => {
|
||||||
test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => {
|
test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
},
|
},
|
||||||
|
payment: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const { wrapper } = setupMocks({});
|
const { wrapper } = setupMocks({});
|
||||||
|
|
@ -286,6 +290,9 @@ describe("quote.vue", () => {
|
||||||
test("should default to insurance if query param 'isInsurance' is true", async () => {
|
test("should default to insurance if query param 'isInsurance' is true", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
@ -315,6 +322,9 @@ describe("quote.vue", () => {
|
||||||
test("should default to cash if query param 'isInsurance' is false", async () => {
|
test("should default to cash if query param 'isInsurance' is false", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
@ -345,6 +355,9 @@ describe("quote.vue", () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
|
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
@ -375,6 +388,9 @@ describe("quote.vue", () => {
|
||||||
test("should default to cash if cash selection is saved to store", async () => {
|
test("should default to cash if cash selection is saved to store", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
@ -406,6 +422,9 @@ describe("quote.vue", () => {
|
||||||
// Also needs no query parameter or previous selection in store to be present
|
// Also needs no query parameter or previous selection in store to be present
|
||||||
//Arrange
|
//Arrange
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
@ -439,6 +458,9 @@ describe("quote.vue", () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
|
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
@ -467,10 +489,13 @@ describe("quote.vue", () => {
|
||||||
//Assert
|
//Assert
|
||||||
expect(wrapper.vm.isInsuranceSelected).toBe(true);
|
expect(wrapper.vm.isInsuranceSelected).toBe(true);
|
||||||
});
|
});
|
||||||
test("Should default to insurence if user Service zip is from certain States", async () => {
|
test("Should default to insurance if user Service zip is from certain States", async () => {
|
||||||
//Arrange
|
//Arrange
|
||||||
|
|
||||||
store.getters = {
|
store.getters = {
|
||||||
|
lineItems: {
|
||||||
|
glassParts: ["item", "item2"],
|
||||||
|
},
|
||||||
order: {
|
order: {
|
||||||
lineItems: {
|
lineItems: {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,10 @@ import { Form, defineRule } from "vee-validate";
|
||||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
import { applicationConfig } from "@/constants/application-config";
|
import { applicationConfig } from "@/constants/application-config";
|
||||||
import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states";
|
import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states";
|
||||||
import { revalidatePromosAndValidateNewPromo } from "@/helpers/promotions-helper";
|
import {
|
||||||
|
revalidatePromosAndValidateQueryStringPromo,
|
||||||
|
buildToastMessagesFromRevalidateOrValidatePromoResponse,
|
||||||
|
} from "@/helpers/promotions-helper";
|
||||||
import { queryStrings } from "@/constants/query-strings";
|
import { queryStrings } from "@/constants/query-strings";
|
||||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||||
|
|
||||||
|
|
@ -151,10 +154,14 @@ export default {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Promo logic
|
// Promo logic
|
||||||
|
// Populate the previous state of promos for toast message usage in "next()"
|
||||||
|
const oldActivePromos = store.getters.lineItems.promos?.slice(0);
|
||||||
|
const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0);
|
||||||
|
|
||||||
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
||||||
|
|
||||||
const { validatePromoResponse, revalidatePromoResponse } =
|
const { validatePromoResponse, revalidatePromoResponse } =
|
||||||
await revalidatePromosAndValidateNewPromo(
|
await revalidatePromosAndValidateQueryStringPromo(
|
||||||
promoCodeFromQueryString,
|
promoCodeFromQueryString,
|
||||||
pricingResults,
|
pricingResults,
|
||||||
"quote"
|
"quote"
|
||||||
|
|
@ -169,6 +176,25 @@ export default {
|
||||||
vm.availableLineItems = pricingResults;
|
vm.availableLineItems = pricingResults;
|
||||||
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems);
|
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems);
|
||||||
vm.newValidatedPromos = validatePromoResponse?.orderPromos;
|
vm.newValidatedPromos = validatePromoResponse?.orderPromos;
|
||||||
|
|
||||||
|
if (revalidatePromoResponse) {
|
||||||
|
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||||
|
revalidatePromoResponse,
|
||||||
|
oldActivePromos,
|
||||||
|
oldInactivePromos
|
||||||
|
);
|
||||||
|
revalidateAlerts.forEach((alert) => {
|
||||||
|
vm.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (validatePromoResponse) {
|
||||||
|
const validateAlerts =
|
||||||
|
buildToastMessagesFromRevalidateOrValidatePromoResponse(validatePromoResponse);
|
||||||
|
vm.$refs.funnelHeader.pushGlobalAlert(
|
||||||
|
validateAlerts[0],
|
||||||
|
validateAlerts[0].shouldAutoFade
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -265,7 +291,11 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
|
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;
|
const payment = this.$store.getters.payment;
|
||||||
if (payment.isInsurance) {
|
if (payment.isInsurance) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
getDisplayTextForDurationLength,
|
getDisplayTextForDurationLength,
|
||||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
|
import { getPromoCodeWithoutBundleIdentifier } from "@/helpers/promotions-helper";
|
||||||
import { getDateDifferenceInDays } from "@/helpers/date-helper";
|
import { getDateDifferenceInDays } from "@/helpers/date-helper";
|
||||||
// Export State
|
// Export State
|
||||||
const getDefaultState = () => {
|
const getDefaultState = () => {
|
||||||
|
|
@ -2120,12 +2120,53 @@ export const actions = {
|
||||||
addGuidToLineItemsIfNotAlreadyThere(vaps);
|
addGuidToLineItemsIfNotAlreadyThere(vaps);
|
||||||
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
||||||
},
|
},
|
||||||
|
// COMBINE THESE
|
||||||
savePromos(context, promos) {
|
savePromos(context, promos) {
|
||||||
context.commit(storeMutations.UPDATE_PROMOS, promos);
|
context.commit(storeMutations.UPDATE_PROMOS, promos);
|
||||||
},
|
},
|
||||||
saveInactivePromos(context, inactivePromos) {
|
saveInactivePromos(context, inactivePromos) {
|
||||||
context.commit(storeMutations.UPDATE_INACTIVE_PROMOS, 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
|
// Price order actions
|
||||||
async priceOrderItemsAndSaveServerData(
|
async priceOrderItemsAndSaveServerData(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue