From f51c3ab305c407e0006efdca586aa24658d423cc Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Mon, 13 Jul 2026 07:57:24 -0400 Subject: [PATCH 01/21] CASH-3029 | Afterpay copy change --- .../quote/afterpay-modal-banner/afterpay-modal-banner.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue index 98c6844b8..a1b92c90d 100644 --- a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue +++ b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue @@ -15,8 +15,8 @@ {{ getInlineAltText(token) }} -   + Date: Mon, 13 Jul 2026 07:58:40 -0400 Subject: [PATCH 02/21] Formatting --- .../quote/afterpay-modal-banner/afterpay-modal-banner.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue index a1b92c90d..cc708dc6a 100644 --- a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue +++ b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue @@ -197,7 +197,6 @@ export default { } @include media-breakpoint-up(md) { padding-left: 1rem; - } } } From 47a6baa407912ee74ccc690ea94e07dc147c22e7 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 14 Jul 2026 09:16:16 -0400 Subject: [PATCH 03/21] CASH-3044: YMM bailout feature toggle --- src/constants/experiments.js | 3 +++ src/mixins/vin-pages-mixin.js | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index 1cd1260a7..0caec2080 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -58,6 +58,9 @@ const experimentSettings = { TIER_ONE: "TierOne", TIER_TWO: "TierTwo", TIER_THREE: "TierThree", + + // YMM + BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles", }; const experimentTriggers = { diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index 78cb134cf..6f43626b6 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -6,6 +6,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import { experimentSettings } from "@/constants/experiments"; import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js"; import { bailoutCodes } from "@/constants/bailout-codes"; +import experimentMixin from "@/mixins/experiment-mixin.js"; export default { computed: { @@ -37,7 +38,7 @@ export default { pageNameToLog: pageName, }); - if (result.PartNotFound) { + if (result.PartNotFound || this.bailoutVinRequiredVehicles()) { return bailoutMixin.methods.navigateToBailoutPage( this, bailoutCodes.PART_NOT_FOUND @@ -75,5 +76,11 @@ export default { const phoneRegex = /^\(?(\d{3})\)?[-. ]?(\d{3})[-. ]?(\d{4})$/; return phoneRegex.test(input); }, + bailoutVinRequiredVehicles() { + return experimentMixin.methods.hasSettingEqualTo( + experimentSettings.BAILOUT_VIN_REQUIRED_VEHICLES, + "true" + ); + }, }, }; From eeae577458f5a8114d8e460a1b6db6da2ebbfdaa Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 14 Jul 2026 10:55:43 -0400 Subject: [PATCH 04/21] CASH-3041: Fix loading spinner --- src/layouts/vin-lookup/vin-lookup.spec.js | 28 +++++++++++++++++++---- src/layouts/vin-lookup/vin-lookup.vue | 11 +++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js index 39b1108f2..439becc60 100644 --- a/src/layouts/vin-lookup/vin-lookup.spec.js +++ b/src/layouts/vin-lookup/vin-lookup.spec.js @@ -168,6 +168,23 @@ describe("vin-lookup.vue", () => { expect(wrapper.vm.navigateForward).not.toHaveBeenCalled(); }); + it("Should keep the continue button loader spinning for vinRequired vehicles when VIN lookup fails", async () => { + // Arrange + store.getters.vehicle.vinRequired = true; + const { wrapper } = setupMocks({}); + mockOutPromises({ vehicleLookupFailed: true }); + wrapper.vm.continueWithFailedVin = jest.fn().mockResolvedValue(); + wrapper.setData({ vin: "1HGCM82633A123456", vinPopulatedOnPageLoad: false }); + + // Act + await wrapper.vm.forwardButtonAction(); + + // Assert + expect(wrapper.vm.continueWithFailedVin).toHaveBeenCalled(); + expect(wrapper.vm.$refs.navbar.removeLoader).not.toHaveBeenCalled(); + store.getters.vehicle.vinRequired = false; + }); + describe("navigateForward", () => { test("carId is different from returned vehicle and selected glass isn't available => continue with different glass", async () => { // Arrange @@ -394,11 +411,14 @@ function setupMocks({ customMountOptions }) { return { wrapper }; } -function mockOutPromises({ carId, isZipValid = true, isZipServiceable = true }) { +function mockOutPromises({ + carId, + isZipValid = true, + isZipServiceable = true, + vehicleLookupFailed = false, +}) { const apiResponses = { - vehicleLookupResponse: { - carId: carId, - }, + vehicleLookupResponse: vehicleLookupFailed ? undefined : { carId: carId }, zipCodeData: { isValid: isZipValid, isServiceable: isZipServiceable, diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 35e2a626e..a8a700de0 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -295,7 +295,14 @@ export default { const isVinLookupRequired = this.$store.getters.vehicle.vinRequired; if (isVinLookupRequired) { this.requiredVinNotFound = true; - this.continueWithFailedVin(this.vin, resultMap.zipCodeData); + + // Check if Service Zip entered is serviceable, if not display an alert + if (!resultMap.zipCodeData.isServiceable) { + this.displayNonServiceableZipAlert = true; + } + + await this.continueWithFailedVin(this.vin, resultMap.zipCodeData); + return; } else { this.displayVinNotFoundAlert = true; } @@ -521,7 +528,7 @@ export default { }, false ); - this.navigateForwardWithSingleCarMatch(); + await this.navigateForwardWithSingleCarMatch(); }, getRequiredVinNotFound() { // Prevents success alert from showing From 0df2bad6a11bdea15f1acc50f2aa311eb3783789 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 14 Jul 2026 11:29:46 -0400 Subject: [PATCH 05/21] CASH-3044: Add VIN required vehicle check for bailout --- src/mixins/vin-pages-mixin.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index 6f43626b6..a14a956ba 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -77,6 +77,10 @@ export default { return phoneRegex.test(input); }, bailoutVinRequiredVehicles() { + var isVinRequiredVehicle = store.getters.order.vehicle?.vinRequired; + if (!isVinRequiredVehicle) { + return false; + } return experimentMixin.methods.hasSettingEqualTo( experimentSettings.BAILOUT_VIN_REQUIRED_VEHICLES, "true" From daa98bab70056e0185c7fe50696073178a9a22f1 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 14 Jul 2026 12:49:01 -0400 Subject: [PATCH 06/21] Prettier --- src/constants/experiments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index ccc56604b..4fab998af 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -61,7 +61,7 @@ const experimentSettings = { // Consent Management SHOW_CONSENT_MANAGEMENT: "ShowConsentManagement", - + // YMM BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles", }; From cd2be9c384fc55d2fdf13b60c1a91e65522745d3 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 08:21:39 -0400 Subject: [PATCH 07/21] CASH-2815 | Waitlist working Pre personal tech review --- src/layouts/scheduling/scheduling.spec.js | 3 + src/layouts/scheduling/scheduling.vue | 7 + .../waitlist-question.spec.js | 146 ++++++++++++++++++ .../waitlist-question/waitlist-question.vue | 136 ++++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 src/layouts/scheduling/waitlist-question/waitlist-question.spec.js create mode 100644 src/layouts/scheduling/waitlist-question/waitlist-question.vue diff --git a/src/layouts/scheduling/scheduling.spec.js b/src/layouts/scheduling/scheduling.spec.js index d64ffa779..c76a0985f 100644 --- a/src/layouts/scheduling/scheduling.spec.js +++ b/src/layouts/scheduling/scheduling.spec.js @@ -15,6 +15,9 @@ jest.mock("@/store", () => ({ lineItems: { glassParts: [] }, policy: { isItac: false, isNoComp: false }, }, + applicationUser: { + experiments: [], + }, }, })); diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue index 3ace9edf7..2b2a8e803 100644 --- a/src/layouts/scheduling/scheduling.vue +++ b/src/layouts/scheduling/scheduling.vue @@ -48,6 +48,10 @@ @address-clicked="onInshopAddressClicked(provider)" /> + diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js new file mode 100644 index 000000000..e3bcc3e07 --- /dev/null +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js @@ -0,0 +1,146 @@ +import { mount } from "@vue/test-utils"; +import waitlistQuestion from "./waitlist-question"; +import store from "@/store"; +import { experimentSettings } from "@/constants/experiments"; + +jest.mock("@/store", () => ({ + getters: { + applicationUser: { + experiments: [], + }, + }, +})); + +const MOCK_CMS_CONTENT = { + WaitListLabelWidget: { Text: "Want to be notified sooner?" }, + WaitListQuestionWidget: { QuestionText: "Add me to the waitlist" }, +}; + +function dateStringOffsetFromToday(offsetDays) { + const d = new Date(); + d.setDate(d.getDate() + offsetDays); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String( + d.getDate() + ).padStart(2, "0")}`; +} + +function enableWaitlistExperiment(thresholdDays = 0) { + store.getters.applicationUser.experiments = [ + { + isActive: true, + settings: { + [experimentSettings.DISPLAY_WAITLIST]: "true", + [experimentSettings.WAITLIST_THRESHOLD_DAYS]: String(thresholdDays), + }, + }, + ]; +} + +function mountComponent(props = {}) { + const cmsMixin = { + methods: { + getCmsContent: jest.fn((widgetName, fieldName) => { + return MOCK_CMS_CONTENT[widgetName]?.[fieldName] ?? ""; + }), + }, + }; + + return mount(waitlistQuestion, { + props: { + modelValue: false, + availableDates: [dateStringOffsetFromToday(10)], + ...props, + }, + global: { + mixins: [cmsMixin], + }, + }); +} + +describe("waitlist-question.vue", () => { + beforeEach(() => { + enableWaitlistExperiment(); + }); + + afterEach(() => { + store.getters.applicationUser.experiments = []; + }); + + it("renders the label and checkbox CMS content", () => { + const wrapper = mountComponent(); + + expect(wrapper.text()).toContain("Want to be notified sooner?"); + expect(wrapper.text()).toContain("Add me to the waitlist"); + }); + + it("reflects the modelValue prop on the checkbox", () => { + const wrapper = mountComponent({ modelValue: true }); + + expect(wrapper.find("input[type='checkbox']").element.checked).toBe(true); + }); + + it("emits update:modelValue with true when the checkbox is checked", async () => { + const wrapper = mountComponent({ modelValue: false }); + + const input = wrapper.find("input[type='checkbox']"); + await input.setValue(true); + + expect(wrapper.emitted("update:modelValue")).toEqual([[true]]); + }); + + it("emits update:modelValue with false when the checkbox is unchecked", async () => { + const wrapper = mountComponent({ modelValue: true }); + + const input = wrapper.find("input[type='checkbox']"); + await input.setValue(false); + + expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); + }); + + it("renders nothing when shouldDisplay is false", () => { + store.getters.applicationUser.experiments = []; + + const wrapper = mountComponent(); + + expect(wrapper.find("input[type='checkbox']").exists()).toBe(false); + }); + + describe("shouldDisplay", () => { + it("is false when the DISPLAY_WAITLIST experiment is off", () => { + store.getters.applicationUser.experiments = [ + { + isActive: true, + settings: { [experimentSettings.WAITLIST_THRESHOLD_DAYS]: "3" }, + }, + ]; + + const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(10)] }); + + expect(wrapper.vm.shouldDisplay).toBe(false); + }); + + it("is false when the earliest available date is within the threshold", () => { + enableWaitlistExperiment(3); + + const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(2)] }); + + expect(wrapper.vm.shouldDisplay).toBe(false); + }); + + it("is true when the experiment is on and the earliest available date exceeds the threshold", () => { + enableWaitlistExperiment(3); + + const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(10)] }); + + expect(wrapper.vm.shouldDisplay).toBe(true); + }); + + it("is false when there are no available dates", () => { + enableWaitlistExperiment(0); + + const wrapper = mountComponent({ availableDates: [] }); + + expect(wrapper.vm.shouldDisplay).toBe(false); + }); + }); +}); diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue new file mode 100644 index 000000000..aba80f23b --- /dev/null +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -0,0 +1,136 @@ + + + + + From 2e15c6120a2c677497a2cccbbf675e81172a53c7 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 08:32:39 -0400 Subject: [PATCH 08/21] CASH-2815 | Pass clicks on the component through to the checkbox --- .../waitlist-question.spec.js | 39 +++++++++++++++++++ .../waitlist-question/waitlist-question.vue | 13 ++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js index e3bcc3e07..83582df3a 100644 --- a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js @@ -105,6 +105,45 @@ describe("waitlist-question.vue", () => { expect(wrapper.find("input[type='checkbox']").exists()).toBe(false); }); + describe("clicking the container", () => { + it("toggles localValue to true when clicking outside the checkbox", async () => { + const wrapper = mountComponent({ modelValue: false }); + + await wrapper.find(".waitlist-label").trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toEqual([[true]]); + }); + + it("toggles localValue to false when clicking outside the checkbox", async () => { + const wrapper = mountComponent({ modelValue: true }); + + await wrapper.find(".waitlist-question").trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); + }); + + it("does not toggle when the click target is the checkbox input itself", () => { + // jsdom doesn't reliably run a checkbox's native activation behavior + // (toggling + firing "change") for script-dispatched clicks, so this + // calls the handler directly with the real input element as the + // event target to verify the guard is skipped in that case. + const wrapper = mountComponent({ modelValue: false }); + const inputElement = wrapper.find("input[type='checkbox']").element; + + wrapper.vm.handleContainerClick({ target: inputElement }); + + expect(wrapper.emitted("update:modelValue")).toBeUndefined(); + }); + + it("does not toggle when the click lands inside the checkbox wrapper but not on the input", async () => { + const wrapper = mountComponent({ modelValue: false }); + + await wrapper.find(".ui-checkbox").trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toBeUndefined(); + }); + }); + describe("shouldDisplay", () => { it("is false when the DISPLAY_WAITLIST experiment is off", () => { store.getters.applicationUser.experiments = [ diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue index aba80f23b..61c54dd86 100644 --- a/src/layouts/scheduling/waitlist-question/waitlist-question.vue +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -1,6 +1,9 @@ @@ -66,7 +65,7 @@ export default { }, waitlistThresholdDays() { return this.hasSetting(experimentSettings.WAITLIST_THRESHOLD_DAYS) - ? parseInt(this.getSettingValue(experimentSettings.WAITLIST_THRESHOLD_DAYS)) + ? parseInt(this.getSettingValue(experimentSettings.WAITLIST_THRESHOLD_DAYS), 10) : 0; }, daysUntilEarliestAvailableDate() { @@ -88,7 +87,10 @@ export default { handleContainerClick(event) { // The checkbox's own label/input already toggles localValue natively, // so ignore clicks that originate inside it to avoid double-toggling. - if (event.target.closest(".ui-checkbox")) return; + // Checking against our own wrapper element (rather than an internal + // class name owned by checkboxQuestion) keeps this decoupled from + // that component's markup. + if (this.$refs.checkboxWrapper?.contains(event.target)) return; this.localValue = !this.localValue; }, }, From ae9a9c860418fb046f2cc24208fd5fdf96fe42bd Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Wed, 15 Jul 2026 08:49:44 -0400 Subject: [PATCH 10/21] CASH-2815 | Clean up CSS that isn't used --- src/layouts/scheduling/waitlist-question/waitlist-question.vue | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue index 9ae96eaa7..c6a81c8b0 100644 --- a/src/layouts/scheduling/waitlist-question/waitlist-question.vue +++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue @@ -104,10 +104,7 @@ export default { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index cad68ef0f..42e3845ab 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -5,6 +5,14 @@
+
+
+ +
+
@@ -102,7 +110,12 @@ + class="quote-disclaimer text-left text-md-center" /> +
@@ -133,6 +146,7 @@ import textBlock from "@/digital-components/text-block/text-block"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner"; +import promoBanner from "@/layouts/quote/promo-banner/promo-banner"; import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue"; import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question"; import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question"; @@ -683,6 +697,12 @@ export default { showAfterpayBanner() { return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration; }, + showPromoBanner() { + return experimentMixin.methods.hasSettingEqualTo( + experimentSettings.SHOW_PROMO_BANNER, + "true" + ); + }, isRecalPriceRemove() { return ( experimentMixin.methods @@ -1034,6 +1054,7 @@ export default { contentGroupModal, loadingModal, afterpayModalBanner, + promoBanner, promoModalQuestion, recalDisclaimer, saveProgressModalQuestion, @@ -1159,4 +1180,7 @@ export default { :deep(.promo-modal-question a) { @include responsive-font-size-md(0.875rem, 1rem); } +.quote-disclaimer:last-child { + margin-bottom: 1.5rem; +} From 96c288a1ca70c22917a2dc6e0b93a792a86b4576 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Mon, 20 Jul 2026 13:09:33 -0400 Subject: [PATCH 16/21] CASH-1911: Add scrollbar and scroll to error message for short viewports --- .../save-progress-popup-question.spec.js | 24 +++++++++++++++++++ .../save-progress-popup-question.vue | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js index 273f39f11..29d102d01 100644 --- a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js +++ b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js @@ -159,13 +159,37 @@ describe("save-progress-popup-question ", () => { wrapper.vm.selectContactMethod("PhoneAnswer"); wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true }); const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle"); + const scrollSpy = jest + .spyOn(wrapper.vm, "scrollConsentIntoView") + .mockResolvedValue(undefined); await wrapper.vm.validatePhoneAndSave(); expect(wrapper.vm.showConsentErrors).toBe(true); + expect(scrollSpy).toHaveBeenCalled(); expect(resetSpy).toHaveBeenCalled(); }); + test("should scroll consent checkboxes into view", async () => { + const { wrapper } = setupMocks({ + props: { + modalWidgetName: "SaveProgressPopupWidget", + }, + }); + const mockScrollIntoView = jest.fn(); + const consentEl = document.createElement("fieldset"); + consentEl.className = "save-progress-popup-sms-consent"; + consentEl.scrollIntoView = mockScrollIntoView; + wrapper.element.appendChild(consentEl); + + await wrapper.vm.scrollConsentIntoView(); + + expect(mockScrollIntoView).toHaveBeenCalledWith({ + behavior: "smooth", + block: "nearest", + }); + }); + test("should reset the send button loader when phone validation fails", async () => { const { wrapper } = setupMocks({ props: { diff --git a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue index 172f81718..fc4e95530 100644 --- a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue +++ b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue @@ -43,6 +43,7 @@ isRequired validationRules="phone-number-required" /> Date: Tue, 21 Jul 2026 15:16:23 -0400 Subject: [PATCH 17/21] CASH-3065 - Pass billToAccountNumber to v2 parts endpoint --- src/store/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/store/index.js b/src/store/index.js index d9ca5c6fe..ea09dfb84 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2015,6 +2015,7 @@ export const actions = { const vin = vehicle.vin; const serviceType = damage.isRepair ? "Repair" : "Install"; const parentAccountNumber = context.getters.payment.parentAccountNumber; + const billToAccountNumber = context.getters.payment.billToAccountNumber; const referralSeqNumber = order.referralSequenceNumber; // create a new array to avoid mutating state @@ -2036,6 +2037,7 @@ export const actions = { serviceType: serviceType, referralSeqNumber: referralSeqNumber, parentAccountNumber: parentAccountNumber, + billToAccountNumber: billToAccountNumber, }, logApiCall: true, pageNameToLog: pageNameToLog, From cc8448752e7da99e9e37b8c621bd9afc8f219c0e Mon Sep 17 00:00:00 2001 From: mvalaiyapathi Date: Wed, 22 Jul 2026 09:11:16 -0400 Subject: [PATCH 18/21] Revert "CASH-3065 - Pass billToAccountNumber to v2 parts endpoint" --- src/store/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/store/index.js b/src/store/index.js index ea09dfb84..d9ca5c6fe 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2015,7 +2015,6 @@ export const actions = { const vin = vehicle.vin; const serviceType = damage.isRepair ? "Repair" : "Install"; const parentAccountNumber = context.getters.payment.parentAccountNumber; - const billToAccountNumber = context.getters.payment.billToAccountNumber; const referralSeqNumber = order.referralSequenceNumber; // create a new array to avoid mutating state @@ -2037,7 +2036,6 @@ export const actions = { serviceType: serviceType, referralSeqNumber: referralSeqNumber, parentAccountNumber: parentAccountNumber, - billToAccountNumber: billToAccountNumber, }, logApiCall: true, pageNameToLog: pageNameToLog, From adb149f22e01f94a095db46f511500b69340b4b0 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Wed, 22 Jul 2026 11:35:07 -0400 Subject: [PATCH 19/21] CASH-3052: Group VAPS in adyen cart --- src/layouts/payment-adyen/payment-adyen.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index fc3ad668d..213d21cb1 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -24,7 +24,7 @@ Date: Fri, 24 Jul 2026 07:45:26 -0400 Subject: [PATCH 20/21] CASH-3075 add validation for phone extenstion CASH-3075 add validation for phone extenstion --- src/constants/error-messages.js | 1 + src/layouts/policy-info/policy-info.vue | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 58f22ebe2..262979ed4 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -32,6 +32,7 @@ const errorMessages = { DATE_REQUIRED: "Please select a date", PHONE_REQUIRED: "Please enter your phone number", PHONE_FORMAT: "Phone number must be 10 digits", + PHONE_EXTENSION_FORMAT: "The specified extension is invalid", SMS_CONSENT_REQUIRED_1: "Please select checkbox to receive text messages", SMS_CONSENT_REQUIRED_2: "Please select at least one consent option", YEAR_REQUIRED: "Please select your vehicle year", diff --git a/src/layouts/policy-info/policy-info.vue b/src/layouts/policy-info/policy-info.vue index 14c7b93ce..f246b9387 100644 --- a/src/layouts/policy-info/policy-info.vue +++ b/src/layouts/policy-info/policy-info.vue @@ -87,7 +87,9 @@ class="mb-4" cmsWidgetName="PhoneExtensionWidget" v-model="phoneExtension" - inputId="phoneExtension" /> + inputId="phoneExtension" + maxLength="5" + validationRules="phone-extension-format" />
@@ -321,6 +323,7 @@ defineRule("policy-number-format", (value) => { defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED)); defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT)); defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED)); +defineRule("phone-extension-format", regex(/^\d{1,5}$/, errorMessages.PHONE_EXTENSION_FORMAT)); defineRule("date-of-loss-required", required(errorMessages.DATE_OF_LOSS_REQUIRED)); defineRule("damage-types-required", required(errorMessages.DAMAGE_TYPES_REQUIRED)); defineRule("state-required", required(errorMessages.STATE_REQUIRED)); From 5b6211301cd58f1a2905a5423622e66662f788dc Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Fri, 24 Jul 2026 13:28:39 -0400 Subject: [PATCH 21/21] CASH-3078 - Fix question-chain resolving when answerResult has a part number --- .../question-chain/question-chain.spec.js | 77 +++++++++++++++++++ .../question-chain/question-chain.vue | 14 +++- src/helpers/question-chain-helper.js | 10 +++ src/helpers/question-chain-helper.spec.js | 16 ++++ 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/digital-components/question-chain/question-chain.spec.js b/src/digital-components/question-chain/question-chain.spec.js index 1dad2d5de..d0e54a895 100644 --- a/src/digital-components/question-chain/question-chain.spec.js +++ b/src/digital-components/question-chain/question-chain.spec.js @@ -482,9 +482,86 @@ describe("Question Chain component", () => { expect(wrapper.vm.currentQuestionNum).toBe(3); expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true); }); + + test("should resolve on Q1 Yes and not show Q2 for Honda HUD question", async () => { + const { wrapper } = setupMocks({ + questionDataProp: getHondaAccordWindshieldQuestions(), + }); + await nextTick(); + + const q1 = wrapper.vm.questions[0]; + wrapper.vm.handleAnswer(q1, "1|answer|FW04796|Yes"); + + expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({ + answerResult: "FW04796", + index: 0, + }); + expect(wrapper.vm.currentQuestionNum).toBe(0); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(false); + }); + + test("should resolve on Q3 No and not continue after remount", async () => { + const questionDataProp = getHondaAccordWindshieldQuestions(); + questionDataProp[0].answerSelected = "1|nextQuestion|2|No"; + questionDataProp[1].answerSelected = "2|nextQuestion|3|No"; + questionDataProp[2].answerSelected = "3|answer|FW04793|No"; + + const { wrapper } = setupMocks({ questionDataProp }); + await nextTick(); + + expect(wrapper.vm.currentQuestionNum).toBe(0); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true); + expect( + wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question)) + ).toHaveLength(3); + }); + + test("should show Q2 after answering No on Honda HUD question", async () => { + const { wrapper } = setupMocks({ + questionDataProp: getHondaAccordWindshieldQuestions(), + }); + await nextTick(); + + const q1 = wrapper.vm.questions[0]; + wrapper.vm.handleAnswer(q1, "1|nextQuestion|2|No"); + + expect(wrapper.vm.currentQuestionNum).toBe(2); + expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(true); + }); }); }); +function getHondaAccordWindshieldQuestions() { + return [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with a Heads-up Display which projects vehicle information, such as speed, onto the windshield?", + answers: [ + { answerResult: "FW04796", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "", answerText: "No", nextQuestionSequence: 2 }, + ], + }, + { + questionSequence: 2, + questionText: + "Is your vehicle equipped with an auto-dimming rearview mirror which will darken automatically when a vehicles approaches from the rear at night?", + answers: [ + { answerResult: "FW04795", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "", answerText: "No", nextQuestionSequence: 3 }, + ], + }, + { + questionSequence: 3, + questionText: "Is your vehicle equipped with a power moonroof?", + answers: [ + { answerResult: "FW04794", answerText: "Yes", nextQuestionSequence: null }, + { answerResult: "FW04793", answerText: "No", nextQuestionSequence: null }, + ], + }, + ]; +} + function getCherokeeWindshieldQuestions() { return [ { diff --git a/src/digital-components/question-chain/question-chain.vue b/src/digital-components/question-chain/question-chain.vue index b37755ee7..467e9619e 100644 --- a/src/digital-components/question-chain/question-chain.vue +++ b/src/digital-components/question-chain/question-chain.vue @@ -20,6 +20,7 @@ import buttonQuestion from "@/digital-components/button-question/button-question"; import { useValidateForm } from "vee-validate"; import { + isTerminalQuestionChainAnswer, mapQuestionAnswersForChain, normalizeAnswerSelectedValue, } from "@/helpers/question-chain-helper"; @@ -74,13 +75,18 @@ export default { return !!question.answerSelected || this.isCurrentQuestion(question); }, initializeCurrentQuestionNum() { - const pendingNextQuestion = [...this.questions] + const lastAnsweredQuestion = [...this.questions] .reverse() - .find((question) => question.answerSelected?.includes("|nextQuestion|")); + .find((question) => question.answerSelected); - if (pendingNextQuestion) { + if (isTerminalQuestionChainAnswer(lastAnsweredQuestion?.answerSelected)) { + this.currentQuestionNum = 0; + return; + } + + if (lastAnsweredQuestion?.answerSelected?.includes("|nextQuestion|")) { const nextQuestionSequence = Number( - pendingNextQuestion.answerSelected.split("|")[2] + lastAnsweredQuestion.answerSelected.split("|")[2] ); if ( diff --git a/src/helpers/question-chain-helper.js b/src/helpers/question-chain-helper.js index b5ba8c168..fdf7f194c 100644 --- a/src/helpers/question-chain-helper.js +++ b/src/helpers/question-chain-helper.js @@ -32,6 +32,16 @@ export function mapQuestionAnswersForChain(question) { })); } +export function isTerminalQuestionChainAnswer(answerSelected) { + if (!answerSelected) { + return false; + } + + const [, questionType, answerResult] = answerSelected.split("|"); + + return questionType?.toLowerCase() === "answer" && !!answerResult; +} + export function normalizeAnswerSelectedValue(answerSelected, answers) { if (!answerSelected) { return ""; diff --git a/src/helpers/question-chain-helper.spec.js b/src/helpers/question-chain-helper.spec.js index fa8db3714..f3a31acc1 100644 --- a/src/helpers/question-chain-helper.spec.js +++ b/src/helpers/question-chain-helper.spec.js @@ -1,5 +1,6 @@ import { buildQuestionChainAnswerValue, + isTerminalQuestionChainAnswer, mapQuestionAnswersForChain, normalizeAnswerSelectedValue, } from "./question-chain-helper"; @@ -29,6 +30,21 @@ describe("question-chain-helper", () => { }); }); + describe("isTerminalQuestionChainAnswer", () => { + test("returns true when answer has a part number and no next question", () => { + expect(isTerminalQuestionChainAnswer("1|answer|FW04796|Yes")).toBe(true); + expect(isTerminalQuestionChainAnswer("3|answer|FW04793|No")).toBe(true); + }); + + test("returns false when answer continues to the next question", () => { + expect(isTerminalQuestionChainAnswer("1|nextQuestion|2|No")).toBe(false); + }); + + test("returns false when answer type is answer but part number is missing", () => { + expect(isTerminalQuestionChainAnswer("1|answer||No")).toBe(false); + }); + }); + describe("normalizeAnswerSelectedValue", () => { const answers = [{ value: "1|answer|DW01705|Yes" }, { value: "1|answer|DW01591|No" }];