diff --git a/src/constants/query-strings.js b/src/constants/query-strings.js index 40f3a2242..5c056c934 100644 --- a/src/constants/query-strings.js +++ b/src/constants/query-strings.js @@ -19,6 +19,7 @@ const queryStrings = { AUTH_CODE: "auth_code", TRANSACTION_ID: "transaction_id", TRANS_REFERENCE_NUMBER: "auth_trans_ref_no", + DISPLAY_PIA_ALERT: "displayPiaAlert", }; export { queryStrings }; diff --git a/src/digital-components/textarea-question/textarea-question.vue b/src/digital-components/textarea-question/textarea-question.vue index aa81c8286..eda3a6f9c 100644 --- a/src/digital-components/textarea-question/textarea-question.vue +++ b/src/digital-components/textarea-question/textarea-question.vue @@ -35,7 +35,7 @@ export default { isRequired: Boolean, maxLength: { type: Number, - default: 250, + default: 150, }, modelValue: String, }, @@ -103,7 +103,7 @@ export default { } } p { - color: $gray-500; + color: $gray-600; &.urgent-countdown { color: $red; } diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index c1a9608a8..1180b3152 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -5,7 +5,10 @@ class="row vin-toggle flex align-items-center pt-4" :class="[isExpanded ? 'expanded' : '']" @click="toggleIsExpanded()"> - + {{ amountDueText }} {{ getFormattedAmount("", amountDue) }} @@ -13,7 +16,8 @@
- + + {{ screenReaderTotalAmountDueText }} {{ getFormattedAmount("", packagePrice) }}
@@ -27,7 +31,11 @@ linkType="text" :text="removeLinkText" href="#!" - @click-event="removeItem(cartItem.cartItemType, cartItem.category)" /> + @click-event="removeItem(cartItem.cartItemType, cartItem.category)"> + +
@@ -56,25 +64,40 @@ {{ recycleFeeCartItem.name }} - {{ getFormattedAmount(cartItem.category, cartItem.subTotal) }} + {{ screenReaderRecycleFeeText }}{{ getFormattedAmount(cartItem.category, cartItem.subTotal) }}
{{ subtotalText }}{{ getFormattedAmount("", subTotal) }} + >{{ screenReaderSubTotalText }}{{ getFormattedAmount("", subTotal) }}
{{ salesTaxText }}{{ getFormattedAmount("", salesTax) }} + >{{ screenReaderSalesTaxText }}{{ getFormattedAmount("", salesTax) }}
{{ amountPaidText }}{{ getFormattedAmount("", amountPaid) }} + >{{ screenReaderAmountPaidText }}{{ getFormattedAmount("", amountPaid) }}
{{ amountDueText }}{{ getFormattedAmount("", amountDue) }} + >{{ screenReaderTotalAmountDueText }}{{ getFormattedAmount("", amountDue) }}
@@ -187,6 +210,24 @@ export default { }, }, computed: { + screenReaderTotalAmountDueText() { + return this.getCmsContent("ScreenReaderTotalAmountDueWidget", "Text"); + }, + screenReaderRemoveVapsText() { + return this.getCmsContent("ScreenReaderRemoveVapsWidget", "Text"); + }, + screenReaderRecycleFeeText() { + return this.getCmsContent("ScreenReaderRecycleFeeWidget", "Text"); + }, + screenReaderSubTotalText() { + return this.getCmsContent("ScreenReaderSubTotalWidget", "Text"); + }, + screenReaderAmountPaidText() { + return this.getCmsContent("ScreenReaderAmountPaidWidget", "Text"); + }, + screenReaderSalesTaxText() { + return this.getCmsContent("ScreenReaderSalesTaxWidget", "Text"); + }, availableLineItems() { if (!this.lineItems || this.lineItems.length < 1) { return []; diff --git a/src/helpers/promotions-helper.js b/src/helpers/promotions-helper.js index 567304de6..3968248d6 100644 --- a/src/helpers/promotions-helper.js +++ b/src/helpers/promotions-helper.js @@ -132,8 +132,19 @@ export async function revalidatePromosAndValidateQueryStringPromo( pageNameToLog, false ); - if (!excludeFromInactivePromoErrorCodes.includes(validatePromoResponse.errorCode)) { - // Save any valid (applied or not) query string promoCode to inactivePromos + if (validatePromoResponse.errorCode == null) { + // Save promo to store + const activePromos = store.getters.order.lineItems.promos ?? []; + activePromos.push(...validatePromoResponse.orderPromos); + baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { + activePromos: activePromos, + }, + false + ); + } else if (!excludeFromInactivePromoErrorCodes.includes(validatePromoResponse.errorCode)) { + // Save any valid query string promoCode (not applied) 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)) { @@ -202,16 +213,33 @@ export function buildToastMessagesFromRevalidateOrValidatePromoResponse( } export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) { + const matchingPromoCodes = []; const matchingPromos = []; + const consolidatedPromosWithIds = {}; + // Combine bundle promos to make sure each part of the bundle is satisfied promos.forEach((promo) => { + const cleanPromoCode = getPromoCodeWithoutBundleIdentifier(promo.promoCode); + if (consolidatedPromosWithIds[cleanPromoCode]) { + consolidatedPromosWithIds[cleanPromoCode].push(...promo.discountedLineItemIds); + } else { + consolidatedPromosWithIds[cleanPromoCode] = promo.discountedLineItemIds.slice(0); + } + }); + Object.keys(consolidatedPromosWithIds).forEach((promoCode) => { let allIdsMatch = true; - promo.discountedLineItemIds.forEach((id) => { + consolidatedPromosWithIds[promoCode].forEach((id) => { if (lineItemsOnOrder.filter((x) => x.id === id).length === 0) { allIdsMatch = false; return; } }); if (allIdsMatch) { + matchingPromoCodes.push(promoCode); + } + }); + promos.forEach((promo) => { + const cleanPromoCode = getPromoCodeWithoutBundleIdentifier(promo.promoCode); + if (matchingPromoCodes.includes(cleanPromoCode)) { matchingPromos.push(promo); } }); diff --git a/src/layouts/customer-details/customer-details.vue b/src/layouts/customer-details/customer-details.vue index 668bd0495..08d547f96 100644 --- a/src/layouts/customer-details/customer-details.vue +++ b/src/layouts/customer-details/customer-details.vue @@ -48,7 +48,7 @@ class="mb-4" v-model="techNotes" cmsWidgetName="TextAreaContentWidget" - maxLength="250" /> + maxLength="150" /> diff --git a/src/layouts/payment-method/add-vaps-modal-buttons/add-vaps-modal-buttons.vue b/src/layouts/payment-method/add-vaps-modal-buttons/add-vaps-modal-buttons.vue index cbbb51ceb..b8aa2dd1a 100644 --- a/src/layouts/payment-method/add-vaps-modal-buttons/add-vaps-modal-buttons.vue +++ b/src/layouts/payment-method/add-vaps-modal-buttons/add-vaps-modal-buttons.vue @@ -62,6 +62,7 @@ import { defineRule } from "vee-validate"; import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import { processIfStatements } from "@/helpers/cms-content-helper"; +import { deepClone } from "@/helpers/object-helper"; // DEFINE VALIDATION RULES defineRule("checkbox-required", required(errorMessages.OPTION_REQUIRED)); @@ -198,6 +199,12 @@ export default { return null; } }, + getAnswerFromCms(name, answers) { + const answerIndex = answers.findIndex((answer) => { + return answer.Name === name; + }); + return answers[answerIndex]; + }, }, computed: { currentCartItems() { @@ -280,21 +287,17 @@ export default { } return result; }, + wipersModalDataFromCms() { + return this.getAnswerFromCms("WipersModal", this.answersCmsData); + }, + rainDefenseModalDataFromCms() { + return this.getAnswerFromCms("RainDefenseModal", this.answersCmsData); + }, buttonsToDisplay() { const buttons = []; if (!this.answersCmsData) return buttons; - const getAnswer = (name, answers) => { - const answerIndex = answers.findIndex((answer) => { - return answer.Name === name; - }); - return answers[answerIndex]; - }; - - const modalData = getAnswer("WipersModal", this.answersCmsData); - - // remove custom var for now - modalData.Text = modalData.Text?.replaceAll("{custom:WIPERTYPE}", ""); + const wipersModalData = deepClone(this.wipersModalDataFromCms); if (this.wipersOffered === wipersOfferedStrings.BOTH) { // NO REAR WIPERS or FRONT WIPERS IN CART; SHOW COMBO BUTTON @@ -303,27 +306,50 @@ export default { this.wipersModal?.totalRearPrice, ]; prices.sort((a, b) => a - b); - modalData.SubText = "+$" + prices[0] + " - $" + prices[prices.length - 1]; - buttons.push(modalData); + wipersModalData.SubText = "+$" + prices[0] + " - $" + prices[prices.length - 1]; + wipersModalData.Text = wipersModalData.Text?.replaceAll("{custom:WIPERTYPE}", ""); + buttons.push(wipersModalData); } if (this.wipersOffered === wipersOfferedStrings.FRONT) { // NO FRONT WIPERS IN CART; SHOW ONLY FRONT WIPERS BUTTON - modalData.SubText = "+$" + this.wipersModal?.totalFrontPrice; - buttons.push(modalData); + wipersModalData.SubText = "+$" + this.wipersModal?.totalFrontPrice; + if (this.rearWipersInCart) { + wipersModalData.Text = wipersModalData.Text?.replaceAll( + "{custom:WIPERTYPE}", + wipersOfferedStrings.FRONT.toLowerCase() + " " + ); + } else { + wipersModalData.Text = wipersModalData.Text?.replaceAll( + "{custom:WIPERTYPE}", + "" + ); + } + buttons.push(wipersModalData); } if (this.wipersOffered === wipersOfferedStrings.REAR) { // NO REAR WIPERS IN CART; SHOW ONLY REAR WIPERS BUTTON - modalData.SubText = "+$" + this.wipersModal?.totalRearPrice; - buttons.push(modalData); + wipersModalData.SubText = "+$" + this.wipersModal?.totalRearPrice; + if (this.frontWipersInCart) { + wipersModalData.Text = wipersModalData.Text?.replaceAll( + "{custom:WIPERTYPE}", + wipersOfferedStrings.REAR.toLowerCase() + " " + ); + } else { + wipersModalData.Text = wipersModalData.Text?.replaceAll( + "{custom:WIPERTYPE}", + "" + ); + } + buttons.push(wipersModalData); } 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 rainDefenseModalData = deepClone(this.rainDefenseModalDataFromCms); + rainDefenseModalData.SubText = "+$" + this.rainDefenseModal?.listPrice; + buttons.push(rainDefenseModalData); } return buttons; @@ -417,6 +443,7 @@ export default { p.price { text-align: center; color: $green; + font-weight: 500; } p:last-child { margin-bottom: 0; @@ -438,6 +465,7 @@ export default { .additional-button-data { font-size: 0.875rem; + font-weight: 500; line-height: 1.75; padding-left: 0.5rem; color: $green; diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index d7e8c465b..b81ed9d5a 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -103,6 +103,7 @@ import { getNewlyInactivatedPromos, } from "@/helpers/promotions-helper"; import { queryStrings } from "@/constants/query-strings"; +import { routerParams } from "@/router/router-constants/router-params"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { deepClone } from "@/helpers/object-helper"; @@ -440,7 +441,7 @@ export default { } catch (error) { console.log("error: response from pia submit work order:" + error.message); this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - this.$route.params[this.routerParams.DISPLAY_PIA_ALERT] = true; + this.$route.params[routerParams.DISPLAY_PIA_ALERT] = true; return; } } @@ -488,7 +489,10 @@ export default { return this.paymentMethodInternalModel; }, shouldDisplayPiaAlert() { - return this.$route.params[this.routerParams.DISPLAY_PIA_ALERT]; + return ( + this.$route.query[queryStrings.DISPLAY_PIA_ALERT] || + this.$route.params[routerParams.DISPLAY_PIA_ALERT] + ); }, // Necessary to make the watcher of lineItems work // JavaScript does not keep a record of the old value, only a reference to it's location in the memory. 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 index 94de72765..16d170e82 100644 --- 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 @@ -11,6 +11,7 @@ jest.mock("@/digital-components/modal/modal", () => ({ methods: { closeModal: jest.fn(), resetButtonStyle: jest.fn(), + resetForm: jest.fn(), }, })); @@ -49,7 +50,9 @@ describe("promo-modal-question.vue", () => { // Assert expect(wrapper.vm.displayInvalidPromoAlert).toBe(false); - expect(wrapper.vm.displayInvalidOnOrderPromoAlert).toBe(false); + expect(wrapper.vm.displayStackingPromoAlert).toBe(false); + expect(wrapper.vm.displayInShopPromoAlert).toBe(false); + expect(wrapper.vm.displaySimilarPromoAlert).toBe(false); }); it("Should emit update:modelValue on Modal closed", async () => { // Arrange 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 index cde033066..7c3a95151 100644 --- a/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue +++ b/src/layouts/payment-method/promo-modal-question/promo-modal-question.vue @@ -22,19 +22,34 @@ + v-bind:isDismissible="false" /> + v-bind:isDismissible="false" /> + +
Promo code "{{ promoCode }}" applied 1) { + if (additionalInfo?.length > 2) conflictingCodes += ","; + for (let i = 1; i < additionalInfo?.length - 1; i++) { + conflictingCodes += ` "${additionalInfo[i]},`; + } + conflictingCodes += `" and "${additionalInfo?.slice(-1)[0]}`; + } + return conflictingCodes; }, onInputIdAssigned(inputId) { this.promoTextInputId = inputId; @@ -146,6 +169,7 @@ export default { this.resetAlerts(); this.$emit("update:modelValue", this.lineItems); this.promoCode = ""; + this.modal.resetForm(); }, focusOnPromoInput() { const input = document.getElementById(this.promoTextInputId); @@ -171,6 +195,7 @@ export default { isValid: !("errorCode" in promoValidationResponse), promoCode: promoValidationResponse?.orderPromos, errorCode: promoValidationResponse?.errorCode, + additionalInfo: promoValidationResponse?.additionalInfo, }; }, removeItem(promo) { @@ -181,6 +206,43 @@ export default { getPromoCodeWithoutBundleIdentifier(lineItemsToKeep.promoCode) != promo ); }, + getErrorMessage(error, additionalInfo) { + switch (error) { + case promoErrorCodes.PROMO_STACKING_NOT_ALLOWED: + case promoErrorCodes.SIMILAR_PROMO_ALREADY_ON_ORDER: + return additionalInfo.length === 1 && + additionalInfo[0] === this.promoCode.toUpperCase() + ? (this.displaySimilarPromoAlert = true) + : ((this.errorMessage = this.StackPromoText?.replace( + /\{custom:(NEW|OLD)PROMOCODE\}/g, + (match, group) => + group === "NEW" + ? this.promoCode.toUpperCase() + : this.getConflictingPromoCode(additionalInfo) + )), + (this.displayStackingPromoAlert = true)); + case promoErrorCodes.INVALID_PROMO_ON_ORDER: + if (additionalInfo.some((x) => x.toUpperCase() === "APPOINTMENT_TYPE")) { + this.errorMessage = this.InShopPromoText?.replaceAll( + "{custom:INSHOPPROMOCODE}", + this.promoCode.toUpperCase() + ); + return (this.displayInShopPromoAlert = true); + } else { + this.errorMessage = this.PromoText?.replaceAll( + "{custom:PROMOCODE}", + this.promoCode.toUpperCase() + ); + return (this.displayInvalidPromoAlert = true); + } + default: + this.errorMessage = this.PromoText?.replaceAll( + "{custom:PROMOCODE}", + this.promoCode.toUpperCase() + ); + return (this.displayInvalidPromoAlert = true); + } + }, async addPromoCode() { if (this.promoCode) { this.resetAlerts(); @@ -227,11 +289,7 @@ export default { this.lineItems.vaps?.push(...getVaps); this.closeModal(); } else { - if (promoCodeData.errorCode == promoErrorCodes.PROMO_STACKING_NOT_ALLOWED) { - this.displayInvalidOnOrderPromoAlert = true; - } else { - this.displayInvalidPromoAlert = true; - } + this.getErrorMessage(promoCodeData.errorCode, promoCodeData.additionalInfo); this.focusOnPromoInput(); this.resetModalButtonStyle(); } @@ -241,7 +299,9 @@ export default { watch: { promoCode() { this.displayInvalidPromoAlert = false; - this.displayInvalidOnOrderPromoAlert = false; + this.displayStackingPromoAlert = false; + this.displaySimilarPromoAlert = false; + this.displayInShopPromoAlert = false; }, modelValue: { handler(newValue) { diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index 8088cbb06..d12e45705 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -24,12 +24,9 @@ export default { if (piaError) { console.log("Error during payment: " + piaError); - this.$router.navigateWithoutSaving( - this.navigationScenarios.PIA_ERROR, - this.$route, - {}, - { [routerParams.DISPLAY_PIA_ALERT]: true } - ); + this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_ERROR, this.$route, { + [queryStrings.DISPLAY_PIA_ALERT]: true, + }); } else { switch (store.getters.order.payment.piaType) { case paymentMethods.CREDIT_CARD: diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 95f427b8f..d86859668 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -350,8 +350,8 @@ export default { const taxedVaps = mapTaxedLineItemsToStoreFormat(taxedLineItems, availableVaps); // Add items that were not in the store yet but added via query string promo validation - lineItems.promos = lineItems.promos ?? []; - lineItems.promos.push(...newValidatedPromos); + //newValidatedPromos contains both validated and revalidated promos. + lineItems.promos = Array.from(newValidatedPromos); const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos( newValidatedPromos, taxedVaps, diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 1357e5acc..e48e8fff8 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -178,7 +178,6 @@ export default { vm.supportingItems = resultMap.supportingItems; vm.availableLineItems = pricingResults; vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems); - vm.newValidatedPromos = validatePromoResponse?.orderPromos; if (revalidatePromoResponse) { const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse( @@ -207,16 +206,11 @@ export default { availableLineItems: null, supportingItems: null, pricedGlassParts: null, - newValidatedPromos: null, }; }, computed: { allActivePromos() { - const allActivePromos = []; - if (this.newValidatedPromos) allActivePromos.push(...this.newValidatedPromos); - if (this.$store.getters.order.lineItems.promos) - allActivePromos.push(...this.$store.getters.order.lineItems.promos); - return allActivePromos; + return this.$store.getters.order.lineItems.promos ?? []; }, }, methods: { @@ -294,11 +288,6 @@ export default { } this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, 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/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index 3455de18e..ea33f7642 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.vue +++ b/src/layouts/vehicle-parts/vehicle-parts.vue @@ -249,5 +249,8 @@ export default { height: auto; } } + .two-list-card-width { + width: 100%; + } } diff --git a/src/store/index.js b/src/store/index.js index 332b60cf6..efaf5b428 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2130,6 +2130,8 @@ export const actions = { }, // Manage promo saving to ensure a promoCode never ends up in both active and inactive saveActiveAndOrInactivePromos(context, { activePromos = null, inactivePromos = null }) { + activePromos = activePromos?.slice(0); + inactivePromos = inactivePromos?.slice(0); let activePromosToSave; let inactivePromosToSave; if (!activePromos && !inactivePromos) {