Merge branch 'develop' into feature/CSR-1388-tech-review-changes

This commit is contained in:
Leah Schumann 2023-11-16 09:12:10 -05:00
commit a15cdcea59
13 changed files with 141 additions and 86 deletions

View file

@ -53,6 +53,9 @@
:text="recycleFeeCartItem.name" :text="recycleFeeCartItem.name"
href="#!" href="#!"
@click-event="openModal(recyclingModalCmsWidgetName)" /> @click-event="openModal(recyclingModalCmsWidgetName)" />
<span v-else-if="cartItem == recycleFeeCartItem">
{{ recycleFeeCartItem.name }}
</span>
<span>{{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}</span> <span>{{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}</span>
</div> </div>

View file

@ -5,7 +5,7 @@ export function getDateDifferenceInDays(startDate, endDate) {
// Use the built-in method to get the difference in milliseconds // Use the built-in method to get the difference in milliseconds
var difference = date1 - date2; var difference = date1 - date2;
// Convert milliseconds to days and return the result // Convert milliseconds to days and return the result
return Math.floor(difference / (1000 * 3600 * 24)); return Math.round(difference / (1000 * 3600 * 24));
} }
export function get12HourTimeFormat(time) { export function get12HourTimeFormat(time) {
// Check correct time format and split into components // Check correct time format and split into components

View file

@ -173,13 +173,10 @@ export function buildToastMessagesFromRevalidateOrValidatePromoResponse(
alerts.push(createPromoSuccessAlert(newlyActivatedPromoCode)); alerts.push(createPromoSuccessAlert(newlyActivatedPromoCode));
}); });
const inactivePromoCodesFromResponse = getPromoCodesFromPromoObjectsWithoutDuplicates( const newlyInactivatedPromos = getNewlyInactivatedPromos(
oldInactivePromos,
promoResponse.errors promoResponse.errors
); );
const newlyInactivatedPromos = inactivePromoCodesFromResponse.filter(
(errorPromoCode) => !oldInactivePromos.includes(errorPromoCode)
);
newlyInactivatedPromos.forEach((newlyInactivatedPromo) => { newlyInactivatedPromos.forEach((newlyInactivatedPromo) => {
alerts.push(createPromoErrorAlert(newlyInactivatedPromo)); alerts.push(createPromoErrorAlert(newlyInactivatedPromo));
}); });
@ -269,6 +266,45 @@ export function shouldStripPromoQueryString(fmgPageQueryValue) {
return pagesToStripPromoQueryStringFrom.includes(fmgPageQueryValue); return pagesToStripPromoQueryStringFrom.includes(fmgPageQueryValue);
} }
export function getNewlyInactivatedPromos(oldInactivePromos, newInactivePromos) {
const inactivePromoCodesFromResponse =
getPromoCodesFromPromoObjectsWithoutDuplicates(newInactivePromos);
return inactivePromoCodesFromResponse.filter(
(errorPromoCode) => !oldInactivePromos.includes(errorPromoCode)
);
}
export function createPromoSuccessAlert(promoCode) {
return {
messageHeadline: "Promo applied!",
messageCopy: `Promo "${getPromoCodeWithoutBundleIdentifier(
promoCode.toUpperCase()
)}" was successfully applied to your cart.`,
type: "alert-success",
isDismissible: true,
shouldAutoFade: true,
};
}
export 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 "${getPromoCodeWithoutBundleIdentifier(
promoCode.toUpperCase()
)}" cannot be combined with "${additionalInfo[0].toUpperCase()}"`;
} else {
alert.messageCopy = `Sorry, promo code "${getPromoCodeWithoutBundleIdentifier(
promoCode.toUpperCase()
)}" is not valid`;
}
return alert;
}
// private methods // private methods
function findLineItemsWithPartType(typeToFind, itemsToSearch) { function findLineItemsWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = itemsToSearch?.filter( const partTypeMatches = itemsToSearch?.filter(
@ -290,28 +326,3 @@ function getPromoCodesFromPromoObjectsWithoutDuplicates(promos) {
return promoCodes; 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;
}

View file

@ -98,13 +98,13 @@ export default {
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
const lineItemsFromStore = deepClone(store.getters.order.lineItems); const lineItemsFromSubmittedOrder = deepClone(store.getters.submittedOrder.lineItems);
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.lineItems = lineItemsFromStore; vm.lineItems = lineItemsFromSubmittedOrder;
vm.vaps = lineItemsFromStore.vaps; vm.vaps = lineItemsFromSubmittedOrder.vaps;
}); });
}, },
data() { data() {

View file

@ -287,13 +287,6 @@ export default {
return answers[answerIndex]; return answers[answerIndex];
}; };
if (!this.rainDefenseInCart) {
// NO RAIN DEFENSE IN CART; SHOW RAIN DEFENSE BUTTON
const modalData = getAnswer("RainDefenseModal", this.answersCmsData);
modalData.SubText = "+$" + this.rainDefenseModal?.listPrice;
buttons.push(modalData);
}
const modalData = getAnswer("WipersModal", this.answersCmsData); const modalData = getAnswer("WipersModal", this.answersCmsData);
if (this.wipersOffered === wipersOfferedStrings.BOTH) { if (this.wipersOffered === wipersOfferedStrings.BOTH) {
// NO REAR WIPERS or FRONT WIPERS IN CART; SHOW COMBO BUTTON // NO REAR WIPERS or FRONT WIPERS IN CART; SHOW COMBO BUTTON
@ -318,6 +311,13 @@ export default {
buttons.push(modalData); buttons.push(modalData);
} }
if (!this.rainDefenseInCart) {
// NO RAIN DEFENSE IN CART; SHOW RAIN DEFENSE BUTTON
const modalData = getAnswer("RainDefenseModal", this.answersCmsData);
modalData.SubText = "+$" + this.rainDefenseModal?.listPrice;
buttons.push(modalData);
}
return buttons; return buttons;
}, },
wipersOfferedStrings() { wipersOfferedStrings() {

View file

@ -33,7 +33,6 @@
class="pb-2" class="pb-2"
v-model="lineItems" v-model="lineItems"
:availableVaps="availableVaps" :availableVaps="availableVaps"
linkWidgetName="PromoLinkWidget"
modalWidgetName="PromoModalWidget" /> modalWidgetName="PromoModalWidget" />
<hr class="mb-5" /> <hr class="mb-5" />
@ -97,6 +96,11 @@ import {
revalidatePromosAndValidateQueryStringPromo, revalidatePromosAndValidateQueryStringPromo,
buildToastMessagesFromRevalidateOrValidatePromoResponse, buildToastMessagesFromRevalidateOrValidatePromoResponse,
getVapsThatNeedToBeAddedToSatisfyPromos, getVapsThatNeedToBeAddedToSatisfyPromos,
getPromoCodeWithoutBundleIdentifier,
removeCurrentlyActivePromoCodesFromInactivePromos,
createPromoSuccessAlert,
createPromoErrorAlert,
getNewlyInactivatedPromos,
} from "@/helpers/promotions-helper"; } 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";
@ -220,6 +224,10 @@ export default {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.availableVaps = taxedVaps; vm.availableVaps = taxedVaps;
vm.lineItems = lineItems; vm.lineItems = lineItems;
vm.inactivePromos = removeCurrentlyActivePromoCodesFromInactivePromos(
vm.lineItems.promos,
vm.inactivePromos
);
vm.updateFooterButtonText(vm.customCtaCopy); vm.updateFooterButtonText(vm.customCtaCopy);
if (revalidatePromoResponse) { if (revalidatePromoResponse) {
@ -335,17 +343,20 @@ export default {
"payment-method", "payment-method",
false false
); );
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse( // Handle error alerts here, success alerts are handled in the watcher
revalidatePromoResponse, if (revalidatePromoResponse.errors.length) {
this.lineItems.promos, getNewlyInactivatedPromos(
this.inactivePromos this.inactivePromos,
); revalidatePromoResponse.errors
revalidateAlerts.forEach((alert) => { ).forEach((promoCode) => {
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade); const errorAlert = createPromoErrorAlert(promoCode);
}); this.$refs.funnelHeader.pushGlobalAlert(errorAlert, errorAlert.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) =>
getPromoCodeWithoutBundleIdentifier(x.promoCode)
);
this.$refs.loadingModal.hideModal(); this.$refs.loadingModal.hideModal();
}, },
backButtonAction() { backButtonAction() {
@ -479,7 +490,19 @@ export default {
) { ) {
return; return;
} }
if (oldValue.vaps.length != newValue.vaps.length) { if (oldValue.promos.length < newValue.promos.length) {
const oldPromoCodes = oldValue.promos.map(
(promoObject) => promoObject.promoCode
);
const newlyActivatedPromoCodes = newValue.promos.filter(
(newPromo) => !oldPromoCodes.includes(newPromo.promoCode)
);
const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
} else if (
oldValue.promos.length == newValue.promos.length &&
oldValue.vaps.length != newValue.vaps.length
) {
this.revalidatePromos(); this.revalidatePromos();
} }
}, },

View file

@ -14,13 +14,8 @@ jest.mock("@/digital-components/modal/modal", () => ({
}, },
})); }));
const linkWidgetName = "linkWidgetName";
const modalWidgetName = "modalWidgetName"; const modalWidgetName = "modalWidgetName";
const mockLinkCmsContent = {
BodyText: "Sample link body text here.",
};
const mockModalCmsContent = { const mockModalCmsContent = {
FooterText: "Sample modal footer text here.", FooterText: "Sample modal footer text here.",
}; };
@ -28,10 +23,6 @@ const mockModalCmsContent = {
const mockMixin = { const mockMixin = {
methods: { methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => { getCmsContent: jest.fn((widgetName, cmsFieldName) => {
if (widgetName === linkWidgetName) {
return mockLinkCmsContent[cmsFieldName];
}
if (widgetName === modalWidgetName) { if (widgetName === modalWidgetName) {
return mockModalCmsContent[cmsFieldName]; return mockModalCmsContent[cmsFieldName];
} }
@ -48,7 +39,6 @@ describe("promo-modal-question.vue", () => {
const wrapper = mount(promoModalQuestion, { const wrapper = mount(promoModalQuestion, {
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
linkWidgetName: linkWidgetName,
modalWidgetName: modalWidgetName, modalWidgetName: modalWidgetName,
}, },
attachTo: document.body, attachTo: document.body,
@ -74,7 +64,6 @@ describe("promo-modal-question.vue", () => {
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
modelValue: lineItems, modelValue: lineItems,
linkWidgetName: linkWidgetName,
modalWidgetName: modalWidgetName, modalWidgetName: modalWidgetName,
}, },
attachTo: document.body, attachTo: document.body,

View file

@ -10,12 +10,14 @@
:ref="modalName" :ref="modalName"
:headerText="modalHeaderText" :headerText="modalHeaderText"
:onModalClosedCallback="onModalClosed" :onModalClosedCallback="onModalClosed"
:onModalOpenedCallback="focusOnPromoInput"
:footerButtonText="modalFooterText" :footerButtonText="modalFooterText"
@footer-button-event="addPromoCode"> @footer-button-event="addPromoCode">
<promoQuestion <promoQuestion
ref="promoQuestion" ref="promoQuestion"
customInputId="promoCode" customInputId="promoCode"
v-model="promoCode" v-model="promoCode"
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
cmsWidgetName="PromoQuestionWidget" /> cmsWidgetName="PromoQuestionWidget" />
<alert <alert
ref="alertInvalidPromo" ref="alertInvalidPromo"
@ -74,13 +76,12 @@ export default {
}, },
props: { props: {
modelValue: Object, modelValue: Object,
linkWidgetName: String,
modalWidgetName: String, modalWidgetName: String,
availableVaps: Object, availableVaps: Object,
}, },
computed: { computed: {
promoLinkText() { promoLinkText() {
return this.getCmsContent(this.linkWidgetName, "BodyText"); return this.getCmsContent(this.modalWidgetName, "HeaderText");
}, },
modalHeaderText() { modalHeaderText() {
return this.getCmsContent("PromoQuestionWidget", "QuestionText"); return this.getCmsContent("PromoQuestionWidget", "QuestionText");
@ -127,6 +128,9 @@ export default {
getPromoCode() { getPromoCode() {
return this.lineItems.promos?.[0].promoCode; return this.lineItems.promos?.[0].promoCode;
}, },
onInputIdAssigned(inputId) {
this.promoTextInputId = inputId;
},
openModal() { openModal() {
this.modal.openModal(); this.modal.openModal();
}, },
@ -217,13 +221,11 @@ export default {
} else { } else {
if (promoCodeData.errorCode == promoErrorCodes.PROMO_STACKING_NOT_ALLOWED) { if (promoCodeData.errorCode == promoErrorCodes.PROMO_STACKING_NOT_ALLOWED) {
this.displayInvalidOnOrderPromoAlert = true; this.displayInvalidOnOrderPromoAlert = true;
this.focusOnPromoInput();
this.resetModalButtonStyle();
} else { } else {
this.displayInvalidPromoAlert = true; this.displayInvalidPromoAlert = true;
this.focusOnPromoInput();
this.resetModalButtonStyle();
} }
this.focusOnPromoInput();
this.resetModalButtonStyle();
} }
} }
}, },

View file

@ -8,6 +8,7 @@
cornerStyle="rounded" cornerStyle="rounded"
:displayQuestionText="false" :displayQuestionText="false"
isRequired isRequired
@input="validateAlphanumeric"
validationRules="promo-required" /> validationRules="promo-required" />
</template> </template>
@ -22,6 +23,8 @@ import { errorMessages } from "@/constants/error-messages";
// Define Validation Rules // Define Validation Rules
defineRule("promo-required", required(errorMessages.PROMO_REQUIRED)); defineRule("promo-required", required(errorMessages.PROMO_REQUIRED));
const ALPHANUMERIC_REGEX = /[^a-zA-Z0-9]/g;
export default { export default {
name: "promo-question", name: "promo-question",
props: { props: {
@ -38,6 +41,11 @@ export default {
}, },
}, },
}, },
methods: {
validateAlphanumeric() {
this.value = this.value.replace(ALPHANUMERIC_REGEX, "");
},
},
components: { components: {
textboxQuestion, textboxQuestion,
}, },

View file

@ -267,8 +267,6 @@ export default {
"payment" "payment"
); );
// TODO: Data fetching Promise declarations
const promiseResultMap = [ const promiseResultMap = [
{ {
resultKey: "cmsContent", resultKey: "cmsContent",
@ -360,6 +358,7 @@ export default {
next(async (vm) => { next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.fetchSignatureInfo(resultMap.signature); vm.fetchSignatureInfo(resultMap.signature);
vm.setIFrameListener();
vm.availableVaps = taxedVaps; vm.availableVaps = taxedVaps;
vm.lineItems = lineItems; vm.lineItems = lineItems;
@ -508,17 +507,26 @@ export default {
} }
}, },
getPiaLineItems() { getPiaLineItems() {
let itemsForPia = ""; const itemMap = new Map();
const glassParts = store.getters.order.lineItems.glassParts; const glassParts = store.getters.order.lineItems.glassParts;
const supportingItems = store.getters.order.lineItems.supportingItems; const supportingItems = store.getters.order.lineItems.supportingItems;
const vaps = store.getters.order.lineItems.vaps; const vaps = store.getters.order.lineItems.vaps;
const items = [...(glassParts ?? []), ...(supportingItems ?? []), ...(vaps ?? [])]; const items = [...(glassParts ?? []), ...(supportingItems ?? []), ...(vaps ?? [])];
for (let i = 0; i < items.length; i++) { for (const item of items) {
const amount = items[i].laborAmount + items[i].sellingPrice; const amount = item.laborAmount + item.sellingPrice;
itemsForPia += items[i].partType + "|" + amount + "|1|"; const key = `${item.partType}|${amount}`;
if (itemMap.has(key)) {
itemMap.get(key).quantity++;
} else {
itemMap.set(key, { partType: item.partType, amount, quantity: 1 });
}
} }
return itemsForPia;
return Array.from(itemMap.values())
.map((item) => `${item.partType}|${item.amount}|${item.quantity}`)
.join("||");
}, },
getAmountDue() { getAmountDue() {
return baseMixin.methods.getAmountDue(store.getters.order.lineItems); return baseMixin.methods.getAmountDue(store.getters.order.lineItems);
@ -577,6 +585,16 @@ export default {
this.submitHopForm(); this.submitHopForm();
}, },
handleIFrameContentWindwMessage(event) {
if (event.data.indexOf("afterpayClosed") > -1) {
this.backButtonAction();
}
},
setIFrameListener() {
window.addEventListener("message", (event) =>
this.handleIFrameContentWindwMessage(event)
);
},
}, },
components: { components: {
funnelHeader, funnelHeader,

View file

@ -9,10 +9,6 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="siteSubHeader" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="siteSubHeader" />
<div
cmsWidgetName="FindYourVehicleCopyWidget"
v-html="this.findYourVehicleCopy"
class="mt-2 mb-4 small"></div>
<vehicleQuestion <vehicleQuestion
ref="vehicleYearQuestion" ref="vehicleYearQuestion"
v-model="selectedYear" v-model="selectedYear"
@ -298,9 +294,6 @@ export default {
else if (this.imageUrl == null && this.carId !== null) return false; else if (this.imageUrl == null && this.carId !== null) return false;
else return true; else return true;
}, },
findYourVehicleCopy() {
return this.getCmsContent("FindYourVehicleCopyWidget", "QuestionText");
},
}, },
methods: { methods: {
getVehicle(year, make, model, style) { getVehicle(year, make, model, style) {

View file

@ -1317,7 +1317,6 @@ export const actions = {
}); });
syncLineItemIds(response.data, context.getters.order.lineItems.vaps); syncLineItemIds(response.data, context.getters.order.lineItems.vaps);
return response; return response;
}, },
@ -2349,6 +2348,7 @@ export const actions = {
activePromosToUse = activePromosToUse ?? order.lineItems.promos; activePromosToUse = activePromosToUse ?? order.lineItems.promos;
lineItemsToUse = lineItemsToUse ? deepClone(lineItemsToUse) : deepClone(order.lineItems); lineItemsToUse = lineItemsToUse ? deepClone(lineItemsToUse) : deepClone(order.lineItems);
addGuidToLineItemsIfNotAlreadyThere(lineItemsToUse.vaps);
lineItemsToUse.promos = activePromosToUse; lineItemsToUse.promos = activePromosToUse;
inactivePromosToUse = inactivePromosToUse ?? order.payment.inactivePromos; inactivePromosToUse = inactivePromosToUse ?? order.payment.inactivePromos;

View file

@ -3,6 +3,7 @@
class="loader" class="loader"
role="alert" role="alert"
aria-label="Loading new page" aria-label="Loading new page"
@click="captureClick"
v-bind:class="[ v-bind:class="[
this.loaderColor, this.loaderColor,
this.loaderPosition, this.loaderPosition,
@ -30,6 +31,13 @@ export default {
default: false, default: false,
}, },
}, },
methods: {
captureClick(event) {
if (!this.allowPageInteraction) {
event.stopPropagation();
}
},
},
}; };
</script> </script>
@ -95,7 +103,7 @@ export default {
} }
&.allow-ui-interaction::before { &.allow-ui-interaction::before {
//no-block to enable clicking on certain buttons with loaders while the loader is actives //no-block to enable clicking on certain buttons with loaders while the loader is actives
z-index: -1; pointer-events: none;
} }
} }
</style> </style>