From 36b8ed0ac1c8f2ad8fc708786400ffef56d24e1e Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Fri, 24 Oct 2025 16:11:05 -0400 Subject: [PATCH 01/21] CASH-1712: rename for clarity --- src/layouts/schedule/schedule.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 716c819e2..81d4315b6 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -624,7 +624,7 @@ export default { vm.showPricingByDay = showPricingByDay; vm.preSelectedDate = preSelectedDate; vm.appointmentType = appointmentType; - vm.setData( + vm.setDataOnLoad( resultMap.zipCodeData, resultMap.serviceabilityDetails, resultMap.mobileFeePart, @@ -966,7 +966,7 @@ export default { } }, - setData(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) { + setDataOnLoad(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) { if (zipCodeData) { this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase; this.zipCodeCtu = zipCodeData.zipCodeCtu; From 4405c1c3f0323ca01ef0c76c13ee221c0ebc6287 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Fri, 24 Oct 2025 16:18:11 -0400 Subject: [PATCH 02/21] CASH-1712 - restrict showAnotherMonth logic to be more specific to avoid loading next month if only one appointment type is available --- src/layouts/schedule/schedule.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 81d4315b6..c5c697d35 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1217,6 +1217,8 @@ export default { async initializeDatePicker() { this.selectedDate = null; + const includeMobileTimeSlots = this.isServiceableMobile; + const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff; const datePickerInitialData = await this.$refs.datePicker.loadInitialData({ // setup config options for date-picker selectableDatesSetting: "custom", @@ -1225,8 +1227,8 @@ export default { preSelectedDate: this.preSelectedDate, providerNumber: this.selectedProvider?.providerNumber, zipCode: this.zipCode, - includeMobileTimeSlots: this.isServiceableMobile, - includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff, + includeMobileTimeSlots: includeMobileTimeSlots, + includeInshopTimeSlots: includeInshopTimeSlots, }); datePickerInitialData.pricingByDayBasePrice = this.pricingByDayBasePrice; datePickerInitialData.pricingByDayUpcharge = this.pricingByDayUpcharge; @@ -1242,8 +1244,8 @@ export default { } else { // if no date is preselected on load, make sure there are some dates available if ( - this.selectableDatesInshop.days.length < 1 || - this.selectableDatesMobile.days.length < 1 + (includeInshopTimeSlots && this.selectableDatesInshop.days.length < 1) || + (includeMobileTimeSlots && this.selectableDatesMobile.days.length < 1) ) { await this.$nextTick(); await this.$refs.datePicker.showAnotherMonth(); From 55d0ae9a7e2ec3ce78d0d096c01af7679b3bd1ae Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Fri, 24 Oct 2025 16:22:17 -0400 Subject: [PATCH 03/21] CASH-1712 - attempt to prevent repetitive api calls and initDatePicker when toggling appt types --- src/layouts/schedule/schedule.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c5c697d35..e882a2652 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1743,7 +1743,6 @@ export default { this.lastSelectedInshopOrDropoffProvider = this.selectedProvider; } this.appointmentType = AppointmentTypeStrings.MOBILE; - this.updateSelectedProvider(); } else if (newAppointmentType) { if (this.appointmentType != AppointmentTypeStrings.MOBILE) { // Clear last shop selected if appointment type was changed in any manner other than from Mobile @@ -1757,9 +1756,6 @@ export default { } else { this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; } - - // make sure a selectedProvider exists - this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider); } else { this.appointmentType = null; } From 3969eb4f352123b55c0030f36d13fdd434c68bf7 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Mon, 27 Oct 2025 10:56:26 -0400 Subject: [PATCH 04/21] Revert "Merge pull request #2914 from Safelite/feature/CASH-1529-revert" This reverts commit ea7262594b424d3f0fcbe0740f2a9b05fabe7054, reversing changes made to 71c7625631b187b85cf8b7dc938f4242c91edec1. --- src/fmg-components/cart/cart.vue | 1 + .../promo-modal-question.vue | 35 ++- src/helpers/pricing-helper.js | 20 ++ src/layouts/payment-method/payment-method.vue | 190 ++++++++--------- src/layouts/payment/payment.spec.js | 1 - src/layouts/payment/payment.vue | 114 +--------- src/layouts/quote/quote.vue | 32 +-- src/layouts/schedule/schedule.vue | 199 +++++++----------- src/store/index.js | 37 ++-- 9 files changed, 226 insertions(+), 403 deletions(-) diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 4da3982cd..e9152dd9d 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -322,6 +322,7 @@ export default { false ); } + this.$emit("itemRemoved"); }, async saveVaps(lineItems) { diff --git a/src/fmg-components/promo-modal-question/promo-modal-question.vue b/src/fmg-components/promo-modal-question/promo-modal-question.vue index 8efad6f3e..1e87d5bb5 100644 --- a/src/fmg-components/promo-modal-question/promo-modal-question.vue +++ b/src/fmg-components/promo-modal-question/promo-modal-question.vue @@ -290,8 +290,18 @@ export default { ); if (promoCodeData.isValid) { if (this.taxPromos) { - const pricedLineItemsToTax = []; - pricedLineItemsToTax.push(...promoCodeData.promoCode); // promoCodeData.promoCode should be an array + const vapsToAdd = getVapsThatNeedToBeAddedToSatisfyPromos( + promoCodeData.promoCode, + this.addableVaps, + this.lineItems + ); + const pricedLineItemsToTax = { + glassParts: this.lineItems.glassParts, + promos: promoCodeData.promoCode, + supportingItems: this.lineItems.supportingItems, + vaps: [...this.lineItems.vaps, ...vapsToAdd], + }; + const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA, @@ -314,25 +324,10 @@ export default { // Match all line items to the line items as they are in the store // and rebuild the original structure. - this.lineItems = mapTaxedLineItemsToStoreFormat( - taxedLineItems, - this.lineItems - ); - const taxedVaps = mapTaxedLineItemsToStoreFormat( - taxedLineItems, - this.addableVaps - ); - - const getVaps = getVapsThatNeedToBeAddedToSatisfyPromos( - promoCodeData.promoCode, - taxedVaps, - this.lineItems - ); - - this.lineItems.vaps?.push(...getVaps); + this.lineItems = taxedLineItems; + } else { + this.lineItems?.promos.push(...promoCodeData.promoCode); } - this.lineItems?.promos.push(...promoCodeData.promoCode); - this.$emit("promoAdded", this.lineItems); this.closeModal(); diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js index 9aa317353..0a8b85d7e 100644 --- a/src/helpers/pricing-helper.js +++ b/src/helpers/pricing-helper.js @@ -99,3 +99,23 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) { return pricingResults[0]; } + +export function addPricesToLineItems(lineItems, pricingLineItems) { + lineItems.forEach((lineItem) => { + const lineItemIndex = pricingLineItems.findIndex( + (pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber + ); + + if (lineItem.childParts) { + addPricesToLineItems(lineItem.childParts, pricingLineItems); + } + + const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0]; + lineItem.laborAmount = pricedLineItem.laborAmount; + lineItem.sellingPrice = pricedLineItem.sellingPrice; + lineItem.kitPrice = pricedLineItem.kitPrice; + lineItem.salesTax = pricedLineItem.salesTax; + }); + + return lineItems; +} diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 2c51f42a1..680d060dc 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -34,7 +34,8 @@ :isItac="isItac" :isNoComp="isNoComp" :isExpandedOnLoad="false" - :isMSRFeeApplicable="isMSRFeeApplicable" /> + :isMSRFeeApplicable="isMSRFeeApplicable" + @itemRemoved="evaluatePromosAndTaxItemsOnOrder" /> { + // this assumes childparts will never be a glass part + lineItem.isChildPart = childPartRecursiveCall; + flattenedArray.push(lineItem); + if (lineItem.childParts) { + flattenedArray = [ + ...flattenedArray, + ...getFlattenedArrayOfLineItemsWithChildParts(lineItem.childParts, true), + ]; + } + }); + + return flattenedArray; +} + export default { name: "paymentMethod", props: { @@ -191,32 +200,14 @@ export default { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.name); - const lineItemsFromStore = deepClone(store.getters.order.lineItems); - - const frontWipersOnOrder = - lineItemsFromStore.vaps.filter( - (wiper) => wiper.partType == partTypeStrings.FRONT_WIPER - ) ?? []; - - const rearWipersOnOrder = - lineItemsFromStore.vaps.filter( - (wiper) => wiper.partType == partTypeStrings.REAR_WIPER - ) ?? []; - - const orderHasFrontWipers = frontWipersOnOrder.length > 0; - const orderHasRearWipers = rearWipersOnOrder.length > 0; - - const wipersPromise = - !orderHasFrontWipers || !orderHasRearWipers - ? baseMixin.methods.dispatchStoreActionWithLogging( - storeActions.GET_WIPERS, - { - serviceZipCode: store.getters.order.serviceLocation.zipCode, - carId: store.getters.vehicle.carId, - }, - "payment-method" - ) - : Promise.resolve([]); + const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.GET_WIPERS, + { + serviceZipCode: store.getters.order.serviceLocation.zipCode, + carId: store.getters.vehicle.carId, + }, + "payment-method" + ); const rainRepelPromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_RAIN_REPEL, @@ -224,9 +215,6 @@ export default { "payment-method" ); - // const reviewDropdownPromise = reviewDropdown.methods.loadInitialData(); - // (removed temporarily for Heritage parity effort) - const promiseResultMap = [ { resultKey: "cmsContent", @@ -240,57 +228,40 @@ export default { resultKey: "rainRepel", promise: rainRepelPromise, }, - // { - // resultKey: "reviewDropdownData", - // promise: reviewDropdownPromise, - // }, - // (removed temporarily for Heritage parity effort) ]; const resultMap = await settleAllPromises(promiseResultMap); + let lineItemsFromStore = deepClone(store.getters.order.lineItems); const glassParts = lineItemsFromStore.glassParts ?? []; const supportingItems = lineItemsFromStore.supportingItems ?? []; const vaps = lineItemsFromStore.vaps ?? []; - // if the order already has wipers on it from the quote page, use those as the available wipers - // instead of what comes from the backend. This is to prevent issues with part interchange. - const availableFrontWipers = orderHasFrontWipers - ? frontWipersOnOrder - : (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.FRONT_WIPER) ?? - []); - const availableRearWipers = orderHasRearWipers - ? rearWipersOnOrder - : (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.REAR_WIPER) ?? - []); + const availableVaps = [resultMap.rainRepel, ...resultMap.wipers]; - const allLineItems = [ - resultMap.rainRepel, - ...supportingItems, - ...availableFrontWipers, - ...availableRearWipers, + const lineItemsOnOrderAndAvailableVaps = [ + ...availableVaps, ...glassParts, + ...supportingItems, ...vaps, ]; - const lineItemsToTax = Array.from( - new Map(allLineItems.map((item) => [item.partNumber, item])).values() - ); - - const availableVaps = [ - resultMap.rainRepel, - ...availableFrontWipers, - ...availableRearWipers, - ]; - - const pricedLineItemsToTax = await baseMixin.methods.dispatchStoreActionWithLogging( + // All line items are already priced except availableVaps + // Price everything again to ensure that serverData has all values + // Specifically this addresses an error where insurance client glass parts are not in serverData + // See CASH-1713 for details + let pricedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, { - availableLineItems: lineItemsToTax, + availableLineItems: lineItemsOnOrderAndAvailableVaps, }, "payment-method", false ); + pricedLineItems = getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems); + + // Add prices to the availableVaps + const pricedAvailableVaps = addPricesToLineItems(availableVaps, pricedLineItems); // Promo logic // Populate the previous state of promos for toast message usage in "next()" @@ -298,22 +269,38 @@ export default { const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0); const promoCodeFromQueryString = consumeQueryFromStash(queryStrings.PROMO); + // New promos are saved to store with this const { validatePromoResponse, revalidatePromoResponse } = await revalidatePromosAndValidateQueryStringPromo( promoCodeFromQueryString, - pricedLineItemsToTax, + lineItemsOnOrderAndAvailableVaps, "payment-method" ); + // update lineItemsFromStore with newly added promos + let lineItemsForCart = deepClone(store.getters.order.lineItems); + delete lineItemsForCart.serverData; - // Add newly validated promos to the array to get taxed - const newValidatedPromos = validatePromoResponse?.orderPromos ?? []; - newValidatedPromos.push( - ...(revalidatePromoResponse ? revalidatePromoResponse.promoLineItems : []) - ); - pricedLineItemsToTax.push(...newValidatedPromos); // End of promo logic - const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging( + const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos( + lineItemsForCart.promos ?? [], + availableVaps, + lineItemsForCart + ); + // Add prices to all line items, sometimes they are not there when they come back from heritage + // See CASH-1713 for details + lineItemsForCart.glassParts = addPricesToLineItems( + lineItemsForCart.glassParts ?? [], + pricedLineItems + ); + lineItemsForCart.supportingItems = addPricesToLineItems( + lineItemsForCart.supportingItems ?? [], + pricedLineItems + ); + lineItemsForCart.vaps = addPricesToLineItems(lineItemsForCart.vaps ?? [], pricedLineItems); + lineItemsForCart.vaps.push(...vapsToAddToCart); + // Tax items on order + lineItemsForCart = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA, { billToAccountNumber: store.getters.payment.billToAccountNumber, @@ -322,31 +309,17 @@ export default { serviceLocationCity: store.getters.order.serviceLocation.city, serviceLocationState: store.getters.order.serviceLocation.state, serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, - pricedLineItems: pricedLineItemsToTax, + pricedLineItems: lineItemsForCart, }, "payment-method", false ); - // Match all line items to the line items as they are in the store - // and rebuild the original structure. - const lineItems = mapTaxedLineItemsToStoreFormat(taxedLineItems, lineItemsFromStore); - const taxedVaps = mapTaxedLineItemsToStoreFormat(taxedLineItems, availableVaps); - - lineItems.promos = newValidatedPromos ?? []; - const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos( - newValidatedPromos, - taxedVaps, - lineItems - ); - lineItems.vaps = lineItems.vaps ?? []; - lineItems.vaps.push(...vapsToAddToCart); - // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.availableVaps = taxedVaps; - vm.lineItems = lineItems; + vm.availableVaps = pricedAvailableVaps; + vm.lineItems = lineItemsForCart; vm.inactivePromos = removeCurrentlyActivePromoCodesFromInactivePromos( vm.lineItems.promos, vm.inactivePromos @@ -641,6 +614,28 @@ export default { this.pageName ); }, + async evaluatePromosAndTaxItemsOnOrder() { + //Revalidate promos if there are any inactive, or active promos + const hasInactivePromos = this.inactivePromos.length > 0; + const hasActivePromos = this.lineItems.promos.length > 0; + if (hasInactivePromos || hasActivePromos) { + await this.revalidatePromos(); + } + this.lineItems = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA, + { + billToAccountNumber: store.getters.payment.billToAccountNumber, + providerNumber: store.getters.order.serviceLocation.provider.providerNumber, + appointmentType: store.getters.order.serviceLocation.appointmentType, + serviceLocationCity: store.getters.order.serviceLocation.city, + serviceLocationState: store.getters.order.serviceLocation.state, + serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, + pricedLineItems: this.lineItems, + }, + "payment-method", + false + ); + }, hasSubmittedOrder() { return baseMixin.methods.hasSubmittedOrder(); }, @@ -861,7 +856,6 @@ export default { ) { return; } - if (oldValue.promos.length < newValue.promos.length) { const oldPromoCodes = oldValue.promos.map( (promoObject) => promoObject.promoCode diff --git a/src/layouts/payment/payment.spec.js b/src/layouts/payment/payment.spec.js index ad98814d2..1b36d0644 100644 --- a/src/layouts/payment/payment.spec.js +++ b/src/layouts/payment/payment.spec.js @@ -319,7 +319,6 @@ describe("payment.vue", () => { ); // Assert - expect(vmMock.availableVaps).not.toBeUndefined(); expect(vmMock.lineItems).not.toBeUndefined(); expect(vmMock.setCmsContent).toBeCalled(); diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 992795321..b8e481174 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -42,7 +42,7 @@ { vm.setCmsContent(resultMap.cmsContent); - vm.availableVaps = taxedVaps; - vm.lineItems = lineItems; + vm.lineItems = deepClone(store.getters.order.lineItems); vm.$nextTick(() => { if (vm.$refs.cart) { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index c76222cac..8bab96d5f 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -65,6 +65,7 @@ pageName="quote" :taxPromos="false" :useDefaultCashParentAccount="true" + @promoAdded="promoAdded" modalWidgetName="PromoModalWidget" /> @@ -576,9 +577,6 @@ export default { isInsuranceContinueButtonText() { return this.getCmsContent("isInsuranceContinueButtonText", "Text"); }, - lineItemsCloneForWatcher() { - return Object.assign({}, this.lineItems); - }, isRecalibrationOnOrder() { return store.getters.isRecalibrationOnOrder; }, @@ -854,31 +852,9 @@ export default { await this.forwardButtonAction(); } }, - }, - watch: { - lineItemsCloneForWatcher: { - handler(newValue, oldValue) { - if ( - !oldValue || - oldValue.length == 0 || - !oldValue.vaps || - !newValue || - newValue.length == 0 - ) { - return; - } - 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); - } - }, - deep: true, + promoAdded(lineItems) { + const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode); + this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade); }, }, components: { diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 716c819e2..7c9d9f444 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -460,111 +460,47 @@ export default { }; }, async beforeRouteEnter(to, from, next) { - const isPricingByDayExperiment = experimentMixin.methods.hasSettingEqualTo( - experimentSettings.PRICING_BY_DAY, - "true" - ); - const showPricingByDay = !store.getters.payment.isInsurance && isPricingByDayExperiment; + const serviceZipCode = store.getters.order.serviceLocation.zipCode; - // Get pricingByDayBasePrice needed for Pricing By Day - const lineItems = deepClone(store.getters.order.lineItems); - const isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder; - const shouldHideRecalibration = - experimentMixin.methods.hasSettingEqualTo( - experimentSettings.RECAL_PRICE_REMOVE, - "true" - ) && isRecalibrationOnOrder; - const glassParts = - isRecalibrationOnOrder && shouldHideRecalibration - ? getItemsWithoutRecalParts(lineItems.glassParts) - : (lineItems.glassParts ?? []); - const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers( - lineItems.supportingItems, - { - partNumbersToRemove: [ - partNumberStrings.RECYCLE_FEE, - partNumberStrings.PRICING_BY_DAY_UPCHARGE, - ], - } - ); - const lineItemsToBePriced = { - glassParts: glassParts, - supportingItems: supportingItemsWithoutFees, - vaps: lineItems.vaps ?? [], - promos: lineItems.promos ?? [], - }; - const priceString = getAmountDue(lineItemsToBePriced, false); // pass the IncludeTax param as false - const priceStringIntegerRoundedDown = priceString?.split(".")[0]; // same method used as getDisplayPrice() in service-package-radio used on /quote - const pricingByDayBasePrice = parseInt(priceStringIntegerRoundedDown); - let includePricingByDayUpcharge = false; - let appointmentType = store.getters.order.serviceLocation.appointmentType; - - // Check to see if date should be pre-selected - let scheduleFromStore = await store.getters.order.schedule; - let preSelectedDate; - - if (scheduleFromStore.date && scheduleFromStore.date.length > 0) { - preSelectedDate = scheduleFromStore.date; - - if (appointmentType === AppointmentTypeStrings.MOBILE) { - preSelectedDate += "-mobile"; - } - - // Check to see if pre-selected date should have pricing by day upcharge - if (showPricingByDay) { - // is this preSelectedDate a higher priced pricingByDay day? - const dayIndex = convertDateStringToDate(scheduleFromStore?.date).getDay(); - const dayObject = DAYS_OF_WEEK[dayIndex]; - if (dayObject.isPricingByDayUpchargeDay) { - includePricingByDayUpcharge = true; - } - } - } - - // Set up promises const cmsContentPromise = fetchCmsContentForPage(to.name); - const alertReasonsPromise = locationAlerts.methods.loadInitialData( store.getters.order.serviceLocation.zipCodeCtu, store.getters.order.serviceLocation.provider?.address?.zipCodeCtu ); - - const serviceZipCode = store.getters.order.serviceLocation.zipCode; - const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name); const serviceabilityDetailsPromise = getServiceabilityDetails( serviceZipCode, null, - to.name + "schedule" + ); + let zipCodeData; + const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name); + let shopProviderData; + const shopProviderDataPromise = getShopProviderData(serviceZipCode, to.name); + const zipCodeDataAndShopProviderDataPromise = Promise.all([ + zipCodeDataPromise, + shopProviderDataPromise, + ]); + const mobileFeePartPromise = zipCodeDataAndShopProviderDataPromise.then( + ([zipCodeDataResult, shopProviderDataResult]) => { + zipCodeData = zipCodeDataResult; + shopProviderData = shopProviderDataResult; + return baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.GET_MOBILE_FEE_PART, + { + serviceZipCode: serviceZipCode, + serviceZipCodeCtu: zipCodeData.zipCodeCtu, + mobileProviderNumber: shopProviderData.data.mobileProviderNumber, + }, + to.name, + false + ); + } ); - const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, to.name); - const shopProviderData = await getShopProviderData(serviceZipCode, to.name); - const providerNumber = shopProviderData?.data?.shopProviders[0]?.providerNumber; - - // Get pricingByDayUpcharge needed for Pricing By Day - const pricingByDayUpchargePartPromise = showPricingByDay - ? getPricingByDayPartWithPrice() - : null; - const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_MOBILE_PREMIUM_FEE, null, to.name ); - const premiumFeeWithPricePromise = premiumFeePromise.then((result) => { - if (result.data) { - return baseMixin.methods.dispatchStoreActionWithLogging( - storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, - { - availableLineItems: [result.data], - }, - to.name, - false - ); - } else { - return result.data; - } - }); - // Settle promises and get results const promiseResultMap = [ { @@ -575,18 +511,6 @@ export default { resultKey: "alertReasons", promise: alertReasonsPromise, }, - { - resultKey: "pricingByDayUpchargePart", - promise: pricingByDayUpchargePartPromise, - }, - { - resultKey: "premiumFeeWithPrice", - promise: premiumFeeWithPricePromise, - }, - { - resultKey: "zipCodeData", - promise: zipCodeDataPromise, - }, { resultKey: "mobileFeePart", promise: mobileFeePartPromise, @@ -595,39 +519,70 @@ export default { resultKey: "serviceabilityDetails", promise: serviceabilityDetailsPromise, }, + { + resultKey: "premiumFee", + promise: premiumFeePromise, + }, ]; - const resultMap = await settleAllPromises(promiseResultMap); + const itemsToPrice = []; + if (resultMap.mobileFeePart) { + itemsToPrice.push(resultMap.mobileFeePart); + } + if (resultMap.premiumFee) { + itemsToPrice.push(resultMap.premiumFee); + } - // add pricing by day data - const pricingByDayUpcharge = - showPricingByDay && resultMap.pricingByDayUpchargePart - ? await baseMixin.methods.getTotalLineItemPrice( - resultMap.pricingByDayUpchargePart, - false - ) - : null; + let pricedMobileFeePart = null; + let pricedPremiumFee = null; + + if (itemsToPrice.length) { + const pricedItems = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, + { + availableLineItems: itemsToPrice, + }, + to.name, + false + ); + pricedMobileFeePart = pricedItems.find( + (item) => item.partNumber === resultMap.mobileFeePart.partNumber + ); + pricedPremiumFee = pricedItems.find( + (item) => item.partNumber === resultMap.premiumFee.partNumber + ); + } + + let appointmentType = store.getters.order.serviceLocation.appointmentType; + // Check to see if date should be pre-selected + let scheduleFromStore = await store.getters.order.schedule; + let preSelectedDate; + if (scheduleFromStore.date && scheduleFromStore.date.length > 0) { + preSelectedDate = scheduleFromStore.date; + + if (appointmentType === AppointmentTypeStrings.MOBILE) { + preSelectedDate += "-mobile"; + } + } // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); - vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice - ? resultMap.premiumFeeWithPrice[0] - : null; + vm.mobilePremiumAppointmentFee = pricedPremiumFee; vm.updateFooterButtonText(vm.selectedTimeSlotInfo); - vm.pricingByDayUpchargeLineItem = resultMap.pricingByDayUpchargePart; - vm.includePricingByDayUpcharge = includePricingByDayUpcharge; - vm.isPricingByDayExperiment = isPricingByDayExperiment; - vm.pricingByDayBasePrice = pricingByDayBasePrice; - vm.pricingByDayUpcharge = pricingByDayUpcharge; - vm.showPricingByDay = showPricingByDay; + vm.pricingByDayUpchargeLineItem = null; // Pricing By Day Upcharge Line Item is not used in this version + vm.includePricingByDayUpcharge = false; // Pricing By Day Upcharge is not used in this version + vm.isPricingByDayExperiment = false; // Pricing By Day Experiment is not used in this version + vm.pricingByDayBasePrice = null; // Pricing By Day Base Price is not used in this version + vm.pricingByDayUpcharge = null; // Pricing By Day Upcharge is not used in this version + vm.showPricingByDay = false; // Pricing By Day is not used in this version vm.preSelectedDate = preSelectedDate; vm.appointmentType = appointmentType; vm.setData( - resultMap.zipCodeData, + zipCodeData, resultMap.serviceabilityDetails, - resultMap.mobileFeePart, + pricedMobileFeePart, shopProviderData.data ); vm.initializeDatePicker(); diff --git a/src/store/index.js b/src/store/index.js index 475076c86..dbee4f357 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -59,6 +59,7 @@ import { } from "@/helpers/recal-helper"; import { externalParameterStatus } from "@/constants/external-parameters"; import { experimentSettings } from "@/constants/experiments"; +import { addPricesToLineItems } from "@/helpers/pricing-helper"; // Export State const getDefaultState = () => { @@ -3063,8 +3064,14 @@ export const actions = { pageNameToLog, } ) { + const arrayOfLineItems = [ + ...(pricedLineItems.glassParts ?? []), + ...(pricedLineItems.promos ?? []), + ...(pricedLineItems.supportingItems ?? []), + ...(pricedLineItems.vaps ?? []), + ]; const flattenedLineItemsWithChildParts = - getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems); + getFlattenedArrayOfLineItemsWithChildParts(arrayOfLineItems); const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({ partNumber: lineItem.partNumber, @@ -3113,8 +3120,12 @@ export const actions = { }); context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData); - - pricedLineItems = addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems); + Object.keys(pricedLineItems).forEach((key) => { + pricedLineItems[key] = addTaxesToPricedLineItems( + pricedLineItems[key] ?? [], + response.data.taxedLineItems + ); + }); return pricedLineItems; }, @@ -3796,26 +3807,6 @@ function convertGlassPieceNamingFromApi(glassArray) { return glassArray; } -function addPricesToLineItems(lineItems, pricingLineItems) { - lineItems.forEach((lineItem) => { - const lineItemIndex = pricingLineItems.findIndex( - (pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber - ); - - if (lineItem.childParts) { - addPricesToLineItems(lineItem.childParts, pricingLineItems); - } - - const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0]; - lineItem.laborAmount = pricedLineItem.laborAmount; - lineItem.sellingPrice = pricedLineItem.sellingPrice; - lineItem.kitPrice = pricedLineItem.kitPrice; - lineItem.salesTax = pricedLineItem.salesTax; - }); - - return lineItems; -} - function addTaxesToPricedLineItems(pricedLineItems, taxingLineItems = []) { pricedLineItems.forEach((pricedLineItem) => { const lineItemIndex = taxingLineItems.findIndex( From 87d83ee21b4bb1a6f17c9f8753b4ac48f2887a11 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Mon, 27 Oct 2025 11:37:43 -0400 Subject: [PATCH 05/21] Make lineItems pricing lineup safer --- src/helpers/pricing-helper.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js index 0a8b85d7e..68de04515 100644 --- a/src/helpers/pricing-helper.js +++ b/src/helpers/pricing-helper.js @@ -110,11 +110,13 @@ export function addPricesToLineItems(lineItems, pricingLineItems) { addPricesToLineItems(lineItem.childParts, pricingLineItems); } - const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0]; - lineItem.laborAmount = pricedLineItem.laborAmount; - lineItem.sellingPrice = pricedLineItem.sellingPrice; - lineItem.kitPrice = pricedLineItem.kitPrice; - lineItem.salesTax = pricedLineItem.salesTax; + if (lineItemIndex !== -1) { + const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0]; + lineItem.laborAmount = pricedLineItem.laborAmount; + lineItem.sellingPrice = pricedLineItem.sellingPrice; + lineItem.kitPrice = pricedLineItem.kitPrice; + lineItem.salesTax = pricedLineItem.salesTax; + } }); return lineItems; From e29a2a00aef3e6062fee84511e71831dfce84792 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Mon, 27 Oct 2025 16:33:41 -0400 Subject: [PATCH 06/21] Fix cart modification in getter Move it to the removeItem method where it's easier to follow the code flow --- src/fmg-components/cart/cart.vue | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index e9152dd9d..3961c4d4f 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -312,10 +312,23 @@ export default { this.lineItems[category] = this.lineItems[category].filter( (lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType ); + let shouldSaveSupportingItems = category == cartItemCategories.SUPPORTING_ITEMS; if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) { this.saveVaps(this.lineItems); + // Check if service package discount should be removed after vaps change + if (this.servicePackageDiscountCartItem && this.discountPackageNames != this.packageLevel) { + this.lineItems.supportingItems = this.lineItems.supportingItems.filter( + (lineItemsToKeep) => lineItemsToKeep.cartItemType != this.servicePackageDiscountCartItem.cartItemType + ); + await this.dispatchStoreAction( + storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, + this.lineItems.supportingItems, + false + ); + shouldSaveSupportingItems = true; + } } - if (category == cartItemCategories.SUPPORTING_ITEMS) { + if (shouldSaveSupportingItems) { await this.dispatchStoreAction( storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, this.lineItems.supportingItems, @@ -471,14 +484,7 @@ export default { } if (this.servicePackageDiscountCartItem) { - if (this.discountPackageNames != this.packageLevel) { - this.removeItem( - this.servicePackageDiscountCartItem.cartItemType, - cartItemCategories.SUPPORTING_ITEMS - ); - } else { - cartItems.push(this.servicePackageDiscountCartItem); - } + cartItems.push(this.servicePackageDiscountCartItem); } if (this.premiumAppointmentDiscountCartItem) { From 78aa17361499b07df5c86c3c9d2cfdc2189b52b6 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Tue, 28 Oct 2025 08:12:39 -0400 Subject: [PATCH 07/21] Formatting --- src/fmg-components/cart/cart.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 3961c4d4f..9fb5bba92 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -316,9 +316,14 @@ export default { if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) { this.saveVaps(this.lineItems); // Check if service package discount should be removed after vaps change - if (this.servicePackageDiscountCartItem && this.discountPackageNames != this.packageLevel) { + if ( + this.servicePackageDiscountCartItem && + this.discountPackageNames != this.packageLevel + ) { this.lineItems.supportingItems = this.lineItems.supportingItems.filter( - (lineItemsToKeep) => lineItemsToKeep.cartItemType != this.servicePackageDiscountCartItem.cartItemType + (lineItemsToKeep) => + lineItemsToKeep.cartItemType != + this.servicePackageDiscountCartItem.cartItemType ); await this.dispatchStoreAction( storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, From 66b2efdbb4eb2c151eeaac71f2560721e22b52d2 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Tue, 28 Oct 2025 09:26:51 -0400 Subject: [PATCH 08/21] Remove redundant save call --- src/fmg-components/cart/cart.vue | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 9fb5bba92..d1bd2a939 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -325,11 +325,6 @@ export default { lineItemsToKeep.cartItemType != this.servicePackageDiscountCartItem.cartItemType ); - await this.dispatchStoreAction( - storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, - this.lineItems.supportingItems, - false - ); shouldSaveSupportingItems = true; } } From 90370f8a96ec8432fb6ad65077e32b4251ec9e2a Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Tue, 28 Oct 2025 13:31:30 -0400 Subject: [PATCH 09/21] Remove outdated unit test There shouldn't be logic that modifies the cartItems inside the getter --- src/fmg-components/cart/cart.spec.js | 34 ---------------------------- 1 file changed, 34 deletions(-) diff --git a/src/fmg-components/cart/cart.spec.js b/src/fmg-components/cart/cart.spec.js index 6cec9ed3b..f06a49a90 100644 --- a/src/fmg-components/cart/cart.spec.js +++ b/src/fmg-components/cart/cart.spec.js @@ -309,40 +309,6 @@ describe("cart.vue", () => { expect(found).toBe(true); }); - // Service package discount Fee Cart Item - test("only if there is service package discount fee for the package, a service package discount cart item should be added to the cart", () => { - // Arrange - const lineItems = { - glassParts: [], - supportingItems: [ - { - description: null, - id: "bc00294e-6baa-403e-866e-52c267187a15", - kitPrice: 0, - laborAmount: 0, - partNumber: "DISC CASHSAVE70", - partType: "SERVICE PACKAGE DISCOUNT", - salesTax: null, - sellingPrice: -70, - }, - ], - vaps: [], - promos: [], - }; - - const availableVaps = []; - - // Act - const { wrapper } = setupMocks({ - props: { - modelValue: lineItems, - availableVaps: availableVaps, - }, - }); - // Assert - expect(wrapper.vm.servicePackageDiscountCartItem).toBeNull(); - }); - // Other Supporting Items Cart Item test("if there are other supporting items on the order, an other supporting items cart item should be added to the cart but should not be displayed", () => { // Arrange From e693f9e449f42e29639be073701d292031cc3f12 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Tue, 28 Oct 2025 15:52:07 -0400 Subject: [PATCH 10/21] CASH-1712: more precise approach to fixing forced refresh of date picker --- src/layouts/schedule/schedule.vue | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e882a2652..a670d7c5d 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1669,15 +1669,10 @@ export default { this.selectedProvider = new Provider(); this.updateSelectedProvider(); }, - updateSelectedProvider(newProvider) { - if (newProvider) { - const didProviderNumberChange = - this.selectedProvider.providerNumber !== newProvider.providerNumber; - this.selectedProvider = newProvider; + updateSelectedProvider(newShopProvider) { - if (didProviderNumberChange) { - this.initializeDatePicker(); - } + if (newShopProvider) { + this.selectedProvider = newShopProvider; } else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) { this.selectedProvider = { providerNumber: this.shopProviderData.mobileProviderNumber.toString(), @@ -1743,6 +1738,7 @@ export default { this.lastSelectedInshopOrDropoffProvider = this.selectedProvider; } this.appointmentType = AppointmentTypeStrings.MOBILE; + this.updateSelectedProvider(); } else if (newAppointmentType) { if (this.appointmentType != AppointmentTypeStrings.MOBILE) { // Clear last shop selected if appointment type was changed in any manner other than from Mobile @@ -1756,6 +1752,8 @@ export default { } else { this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; } + // make sure a selectedProvider exists + this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider); } else { this.appointmentType = null; } From cdeede1ed68124b059aa5288e6ad55fc8de63d82 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Tue, 28 Oct 2025 16:05:50 -0400 Subject: [PATCH 11/21] CASH-1712: missed save for merge conflict; committing conflict resolve --- src/layouts/schedule/schedule.vue | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 1690d831d..106602bdd 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -579,13 +579,8 @@ export default { vm.showPricingByDay = false; // Pricing By Day is not used in this version vm.preSelectedDate = preSelectedDate; vm.appointmentType = appointmentType; -<<<<<<< HEAD vm.setDataOnLoad( resultMap.zipCodeData, -======= - vm.setData( - zipCodeData, ->>>>>>> release/2025.11.06 resultMap.serviceabilityDetails, pricedMobileFeePart, shopProviderData.data @@ -1630,7 +1625,6 @@ export default { this.updateSelectedProvider(); }, updateSelectedProvider(newShopProvider) { - if (newShopProvider) { this.selectedProvider = newShopProvider; } else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) { From c32494309ccd95a2db524424e0593413e8a19486 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Tue, 28 Oct 2025 17:41:27 -0400 Subject: [PATCH 12/21] CASH-1712: correct misunderstood use of zipCodeData var (not part of resultMap) --- src/layouts/schedule/schedule.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 106602bdd..8b275f924 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -580,7 +580,7 @@ export default { vm.preSelectedDate = preSelectedDate; vm.appointmentType = appointmentType; vm.setDataOnLoad( - resultMap.zipCodeData, + zipCodeData, resultMap.serviceabilityDetails, pricedMobileFeePart, shopProviderData.data From cf0816dca33757a5a1cf5f445d60e345d586b815 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Wed, 29 Oct 2025 09:23:08 -0400 Subject: [PATCH 13/21] CASH-1712: remove old obsolete files that never should've been committed --- src/layouts/schedule/schedule-june-2025.vue | 782 ------------------ src/layouts/schedule/schedule.specDISABLED.js | 616 -------------- 2 files changed, 1398 deletions(-) delete mode 100644 src/layouts/schedule/schedule-june-2025.vue delete mode 100644 src/layouts/schedule/schedule.specDISABLED.js diff --git a/src/layouts/schedule/schedule-june-2025.vue b/src/layouts/schedule/schedule-june-2025.vue deleted file mode 100644 index 0464b2914..000000000 --- a/src/layouts/schedule/schedule-june-2025.vue +++ /dev/null @@ -1,782 +0,0 @@ - - - - - - - - - - - diff --git a/src/layouts/schedule/schedule.specDISABLED.js b/src/layouts/schedule/schedule.specDISABLED.js deleted file mode 100644 index 9d25152c8..000000000 --- a/src/layouts/schedule/schedule.specDISABLED.js +++ /dev/null @@ -1,616 +0,0 @@ -// Components -import schedule from "@/layouts/schedule/schedule.vue"; - -// Supporting Files -import { shallowMount } from "@vue/test-utils"; -import { getMountOptions } from "@/helpers/unit-test-helper.js"; -import store from "@/store"; -import router from "@/router"; -import baseMixin from "../../mixins/base-mixin"; - -// Mock basemixin -jest.mock("@/mixins/base-mixin.js", () => ({ - methods: { - dispatchStoreAction: jest.fn().mockImplementation((storeAction) => { - if (storeAction === "getShopTimeSlots" || storeAction === "getMobileTimeSlots") { - return { - data: { - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120, - days: [ - { - date: "2023-12-01", - timeSlots: [ - { - id: "06747-01820-S-B*20424*7 AM", - startTime: "07:00", - endTime: "08:00", - offerPremium: false, - }, - ], - }, - ], - }, - }; - } - if (storeAction === "getMobilePremiumFee") { - return Promise.resolve({ - data: { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 14.99, - kitPrice: 0, - }, - }); - } - if (storeAction === "priceOrderItemsAndSaveServerData") { - return Promise.resolve([ - { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 14.99, - kitPrice: 0, - }, - ]); - } - if (storeAction === "saveSupportingItemsSuppressingStateResetting") { - return Promise.resolve([ - { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 14.99, - kitPrice: 0, - }, - ]); - } - }), - dispatchStoreActionWithLogging: jest.fn().mockImplementation((storeAction) => { - if (storeAction === "getShopTimeSlots" || storeAction === "getMobileTimeSlots") { - return { - data: { - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120, - days: [ - { - date: "2023-12-01", - timeSlots: [ - { - id: "06747-01820-S-B*20424*7 AM", - startTime: "07:00", - endTime: "08:00", - offerPremium: false, - }, - ], - }, - ], - }, - }; - } - if (storeAction === "getMobilePremiumFee") { - return Promise.resolve({ - data: { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 14.99, - kitPrice: 0, - }, - }); - } - if (storeAction === "priceOrderItemsAndSaveServerData") { - return Promise.resolve([ - { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 14.99, - kitPrice: 0, - }, - ]); - } - if (storeAction === "saveSupportingItemsSuppressingStateResetting") { - return Promise.resolve([ - { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 14.99, - kitPrice: 0, - }, - ]); - } - }), - filterOutCertainPartTypesOrNumbers: jest.fn(), - hasSubmittedOrder: jest.fn(), - getTotalPriceOfAllLineItemsAndChildParts: jest.fn(), - getTotalLineItemPrice: jest.fn(), - }, -})); - -// Mock fetchCmsContentForPage -jest.mock("@/helpers/cms-content-helper", () => ({ - fetchCmsContentForPage: () => Promise.resolve("content"), - splitCopyOnCMSPlaceHolder: jest.fn(() => ["A", "B"]), -})); - -beforeEach(() => { - jest.restoreAllMocks(); - jest.clearAllMocks(); - store.getters = { - applicationUser: { - experiments: [], - }, - order: { - schedule: { - date: "2019-01-01", - startTime: "09:00", - endTime: "10:00", - routeCode: "000", - }, - lineItems: { - glassParts: [ - { - partNumber: "ABC123", - }, - ], - supportingItems: [], - }, - serviceLocation: { - appointmentType: "Inshop", - zipCode: "12345", - zipCodeCtu: "01234", - provider: { - providerNumber: "123", - }, - }, - damage: { - isRepair: false, - }, - referralNumber: "1234567", - policy: { - policyNumber: "123", - }, - }, - payment: { - isInsurance: true, - }, - lineItems: { - glassParts: [], - supportingItems: [], - }, - experimentSettings: {}, - vehicle: { - carId: "123", - }, - }; -}); -afterEach(() => { - store.getters = {}; - jest.restoreAllMocks(); - jest.clearAllMocks(); -}); - -describe("schedule.vue...", () => { - describe("initial load", () => { - test("should pass arePagePrerequisitesValid with a mobile CASH order and no providerNumber", () => { - // Arrange - const { wrapper } = setupMocks({}); - store.getters.order.serviceLocation.appointmentType = "Mobile"; - store.getters.order.serviceLocation.provider.policyNumber = null; - store.getters.payment.isInsurance = false; - - // Act - const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); - - // Assert - expect(arePagePrerequisitesValid).toBe(true); - }); - - test("should pass arePagePrerequisitesValid with an inshop order and providerNumber", () => { - // Arrange - const { wrapper } = setupMocks({}); - - // Act - const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); - - // Assert - expect(arePagePrerequisitesValid).toBe(true); - }); - - test("should fail arePagePrerequisitesValid with a replace with no glass parts", async () => { - // Arrange - const { wrapper } = setupMocks({}); - store.getters.order.lineItems.glassParts = []; - - // Act - const arePagePrerequisitesValid2 = await wrapper.vm.arePagePrerequisitesValid(); - - // Assert - expect(arePagePrerequisitesValid2).toBe(false); - }); - - test("should fail arePagePrerequisitesValid without isInsurance", () => { - // Arrange - const { wrapper } = setupMocks({}); - store.getters.payment.isInsurance = null; - - // Act - const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); - - // Assert - expect(arePagePrerequisitesValid).toBe(false); - }); - - test("should return timeslots when getMoreScheduleData is called", async () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [], - }; - wrapper.vm.selectableDatesMobile = { - days: [], - }; - - // Act - const newShopTimeSlots = await wrapper.vm.getMoreScheduleData( - "2023-01-01", - "2023-01-31" - ); - - // Assert - expect(newShopTimeSlots).toStrictEqual({ - inshopTimeSlotsData: { - days: [ - { - date: "2023-12-01", - timeSlots: [ - { - endTime: "08:00", - id: "06747-01820-S-B*20424*7 AM", - offerPremium: false, - startTime: "07:00", - }, - ], - }, - ], - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120, - }, - mobileTimeSlotsData: { - days: [ - { - date: "2023-12-01", - timeSlots: [ - { - endTime: "08:00", - id: "06747-01820-S-B*20424*7 AM", - offerPremium: false, - startTime: "07:00", - }, - ], - }, - ], - estimatedServiceMinutesMinimum: 90, - estimatedServiceMinutesMaximum: 120, - }, - }); - }); - - test("should call API service in day ranges of 34 or less when getMoreScheduleData is called with large date ranges", async () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [], - }; - wrapper.vm.selectableDatesMobile = { - days: [], - }; - - // Act - await wrapper.vm.getMoreScheduleData.call( - wrapper.vm, - "2023-01-01", - "2023-03-31", - "Inshop", - "123" - ); - - // Assert - expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledTimes(6); - expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith( - "getShopTimeSlots", - expect.anything(), - expect.anything(), - expect.anything() - ); - }); - - describe("beforeRouteEnter function... ", () => { - // TODO: restore this test (temporarily removed it until CASH-845 is in QA then looping back) - xtest("should call next() and call all functions within next", async () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [], - }; - wrapper.vm.updateFooterButtonText = jest.fn(); - wrapper.vm.setDisplayWaitList = jest.fn(); - const nextFunction = jest.fn((c) => { - c(wrapper.vm); - }); - - // Act - await schedule.beforeRouteEnter.call( - wrapper.vm, - { query: { fmgPage: "schedule" } }, - undefined, - nextFunction - ); - - // Assert - expect(nextFunction).toHaveBeenCalled(); - expect(wrapper.vm.setCmsContent).toHaveBeenCalledWith("content"); - expect(wrapper.vm.$refs.datePicker.initializeComponent).toHaveBeenCalledWith( - expect.objectContaining({ - calendarViewDirection: "future", - }) - ); - expect(wrapper.vm.$refs.locationAlerts.initializeComponent).toHaveBeenCalled(); - expect(wrapper.vm.selectableDatesInshop).toStrictEqual( - expect.objectContaining({ - days: expect.any(Array), - estimatedServiceMinutesMaximum: expect.any(Number), - estimatedServiceMinutesMinimum: expect.any(Number), - }) - ); - expect(wrapper.vm.mobilePremiumAppointmentFee).toStrictEqual( - expect.objectContaining({ - partNumber: expect.any(String), - }) - ); - expect(wrapper.vm.updateFooterButtonText).toHaveBeenCalled(); - expect(wrapper.vm.setDisplayWaitList).toHaveBeenCalled(); - }); - }); - - describe("computed properties...", () => { - test("timeSlotsForSelectedDate should return timeslots if selected date is available", () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [ - { - date: "2022-11-11", - timeSlots: [ - { - id: "1820I-01820-M-I*20425*AM", - startTime: "08:00", - endTime: "12:00", - offerPremium: true, - }, - { - id: "1820I-01820-M-I*20425*PM", - startTime: "12:00", - endTime: "17:00", - offerPremium: false, - }, - ], - }, - ], - }; - wrapper.setData({ - selectedDate: "2022-11-11", - }); - - // Act - const testValue = wrapper.vm.timeSlotsForSelectedDate; - - // Assert - expect(testValue).toStrictEqual( - expect.objectContaining({ - date: "2022-11-11", - }) - ); - }); - - test("timeSlotsForSelectedDate should be null if no date has been selected", () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [ - { - date: "2022-11-11", - timeSlots: [ - { - id: "1820I-01820-M-I*20425*AM", - startTime: "08:00", - endTime: "12:00", - offerPremium: true, - }, - { - id: "1820I-01820-M-I*20425*PM", - startTime: "12:00", - endTime: "17:00", - offerPremium: false, - }, - ], - }, - ], - }; - wrapper.setData({ - selectedDate: undefined, - }); - - // Act - const testValue = wrapper.vm.timeSlotsForSelectedDate; - - // Assert - expect(testValue).toBe(null); - }); - }); - }); - - describe("schedule page methods...", () => { - test("getServiceZipCtuCodeFromStore should return zipCodeCtu", () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [], - }; - - // Act - const testValue = wrapper.vm.getServiceZipCtuCodeFromStore(); - - // Assert - expect(testValue).toStrictEqual("01234"); - }); - - test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [], - }; - const timeInput1 = "15:00"; - const timeInput2 = "15:30"; - - // Act - const testOutput1 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1); - const testOutput2 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2); - const testOutput3 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1, true); - const testOutput4 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2, true); - - // Assert - expect(testOutput1).toBe("3:00 PM"); - expect(testOutput2).toBe("3:30 PM"); - expect(testOutput3).toBe("3 PM"); - expect(testOutput4).toBe("3:30 PM"); - }); - - test("Clicking back should fire correct navigation", () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.selectableDatesInshop = { - days: [], - }; - wrapper.vm.$router.navigateWithoutSaving = jest.fn(); - - // Act - wrapper.vm.backButtonAction(); - - // Assert - expect(wrapper.vm.$router.navigateWithoutSaving).toBeCalledWith( - "CLICKED_BACK", - "schedule" - ); - }); - }); - - // TODO: restore this test (temporarily removed it until CASH-845 is in QA then looping back) - xtest("forwardButtonAction should call route method navigateWithoutSaving", async () => { - // Arrange - const { wrapper } = setupMocks({}); - wrapper.vm.dispatchStoreAction = jest.fn(() => { - return { - data: [], - }; - }); - wrapper.vm.$router.navigateWithSaving = jest.fn(() => { - return {}; - }); - - // Act - await wrapper.vm.forwardButtonAction(); - - // Assert - expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled(); - }); - - test("for Inshop appts, updateSupportingItems should call store action to save supporting items WITHOUT the EARLY BIRD supporting item", async () => { - // Arrange - store.getters.order.serviceLocation.appointmentType = "Inshop"; - store.getters.lineItems.supportingItems = [ - { - partNumber: "EARLY BIRD", - description: null, - partType: "EARLY BIRD", - laborAmount: 0, - sellingPrice: 0, - kitPrice: 0, - }, - ]; - const { wrapper } = setupMocks({}); - wrapper.vm.dispatchStoreAction = jest.fn(() => { - return { - data: [], - }; - }); - wrapper.vm.mobilePremiumAppointmentFee = 14.99; - wrapper.setData({ - selectedTimeSlot: { - date: "2019-01-01", - startTime: "09:00", - endTime: "10:00", - routeCode: null, - isPremiumAppointment: true, - }, - }); - - // Act - await wrapper.vm.updateSupportingItems(); - - // Assert - expect(wrapper.vm.dispatchStoreAction).toBeCalledWith( - "saveSupportingItemsSuppressingStateResetting", - expect.not.arrayContaining([ - expect.objectContaining({ - partType: "EARLY BIRD", - }), - ]), - expect.anything() - ); - }); -}); - -const mockCmsContent = {}; - -function setupMocks({ customMountOptions }) { - const mountOptions = getMountOptions({ - ...customMountOptions, - route: { name: "schedule" }, - }); - - mountOptions.global.mocks["$store"] = store; - mountOptions.global.mocks["$router"] = router; - mountOptions["attachTo"] = document.body; - mountOptions.mixins = [ - { - methods: { - getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { - if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) - return mockCmsContent[widgetName][fieldName]; - }), - }, - }, - ]; - mountOptions.global.mocks.pageName = "schedule"; - - const wrapper = shallowMount(schedule, mountOptions); - - wrapper.vm.setCmsContent = jest.fn(); - wrapper.vm.$refs.datePicker.initializeComponent = jest.fn(); - wrapper.vm.$refs.datePicker.loadInitialData = jest.fn(); - wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn(); - wrapper.vm.$refs.navbar.updateButtonText = jest.fn(); - - return { wrapper }; -} From fb7204d986dce34d3b3a20de7b2ef57359defb2a Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Wed, 29 Oct 2025 09:55:32 -0400 Subject: [PATCH 14/21] CASH-1712: move the providedNumberChange check into the specific condition where we need to reinit --- src/layouts/schedule/schedule.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 8b275f924..40968a4e3 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1133,6 +1133,7 @@ export default { this.resetWaitlist(); this.preSelectedDate = null; this.shopProviderData = shopQuestionPopUpData.shopProviderData; + const oldProvider = this.selectedProvider; const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => { return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber; }); @@ -1144,7 +1145,12 @@ export default { selectedProvider ); } else { - this.updateSelectedProvider(selectedProvider); + const didProviderNumberChange = + oldProvider.providerNumber !== selectedProvider.providerNumber; + if (didProviderNumberChange) { + this.updateSelectedProvider(selectedProvider); + this.initializeDatePicker(); + } } }, async getMoreScheduleData(startDate, endDate) { From d09a30ae4f1af8c42e58eebf5b6e7b3c05e02bd2 Mon Sep 17 00:00:00 2001 From: AdamCaouetteSafelite Date: Wed, 29 Oct 2025 10:15:58 -0400 Subject: [PATCH 15/21] CASH-1712: add null check --- src/layouts/schedule/schedule.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 40968a4e3..0a0a78d68 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1146,7 +1146,7 @@ export default { ); } else { const didProviderNumberChange = - oldProvider.providerNumber !== selectedProvider.providerNumber; + oldProvider?.providerNumber !== selectedProvider.providerNumber; if (didProviderNumberChange) { this.updateSelectedProvider(selectedProvider); this.initializeDatePicker(); From 0f207d8aa92aa60aa032ae7843b5c8588244ea04 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 29 Oct 2025 16:27:01 -0400 Subject: [PATCH 16/21] Grab from correct order rather than going around. --- src/mixins/analytics-mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index d6b25b8dc..78b76caea 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -469,7 +469,7 @@ export default { } // Cash Quote or Cash Price Sub Total - payload.cashPriceSubTotal = store.getters.order?.cashPriceSubTotal ?? ""; + payload.cashPriceSubTotal = order?.cashPriceSubTotal ?? ""; //unverified (in scenarios we don’t display the price) if ( From cd03722642c01d7a7320dd4c19a0b2809c2d8677 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Thu, 30 Oct 2025 08:09:56 -0400 Subject: [PATCH 17/21] CASH-1760 | Fix revalidate tax call It wasn't sending all items --- src/layouts/payment-method/payment-method.vue | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 680d060dc..21c16d80b 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -489,24 +489,6 @@ export default { }); } - if (revalidatePromoResponse.promoLineItems.length > 0) { - await baseMixin.methods.dispatchStoreActionWithLogging( - storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA, - { - billToAccountNumber: store.getters.payment.billToAccountNumber, - providerNumber: - this.$store.getters.order.serviceLocation.provider.providerNumber, - appointmentType: this.$store.getters.order.serviceLocation.appointmentType, - serviceLocationCity: this.$store.getters.order.serviceLocation.city, - serviceLocationState: this.$store.getters.order.serviceLocation.state, - serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode, - pricedLineItems: revalidatePromoResponse.promoLineItems, - }, - "payment-method", - false - ); - } - const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos( revalidatePromoResponse.promoLineItems, this.availableVaps, @@ -520,6 +502,24 @@ export default { getPromoCodeWithoutBundleIdentifier(x.promoCode) ); + if (revalidatePromoResponse.promoLineItems.length > 0) { + await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA, + { + billToAccountNumber: store.getters.payment.billToAccountNumber, + providerNumber: + this.$store.getters.order.serviceLocation.provider.providerNumber, + appointmentType: this.$store.getters.order.serviceLocation.appointmentType, + serviceLocationCity: this.$store.getters.order.serviceLocation.city, + serviceLocationState: this.$store.getters.order.serviceLocation.state, + serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode, + pricedLineItems: this.lineItems, + }, + "payment-method", + false + ); + } + // SAVE PROMO CHANGES TO STORE this.dispatchStoreAction( storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, From 387906baa8bd824fc5a0437cdd47b0422abe757c Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Thu, 30 Oct 2025 10:08:30 -0400 Subject: [PATCH 18/21] CASH-1535: Mobile First Appointment --- src/constants/experiments.js | 5 + src/digital-components/modal/modal.vue | 1 + .../mobile-first-modal/mobile-first-modal.vue | 214 ++++++++++++++++++ src/layouts/schedule/schedule.vue | 126 ++++++++++- src/styles/ux-variables.scss | 5 +- 5 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue diff --git a/src/constants/experiments.js b/src/constants/experiments.js index 6a2796da9..f358fc89f 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -4,6 +4,7 @@ const experimentUniverses = { MSR: "MSR", IGQ_SkipQuote: "NextGen_IGQSkipToInsurance", AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay", + MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment", }; const experimentSettings = { @@ -28,6 +29,10 @@ const experimentSettings = { DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay", AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD: "AfterPayExtendedPayOptionThreshold", DYNAMO_LOGGING: "DynamoLogging", + SHOW_MOBILE_FIRST_APPT: "ShowMobileFirstAppt", + SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays", + SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays", + SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays", }; const experimentTriggers = { diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index 9167c03b3..867e73c75 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -14,6 +14,7 @@