From bbfaf75c872f4d5f6bf0067fe3353fc7357bda1b Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Sun, 8 Mar 2026 14:08:18 -0400 Subject: [PATCH 01/16] CASH-2378 - split back out mobile and inshop instances of getFirstAvailableApptByTOD --- src/layouts/schedule/schedule.vue | 47 ++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 89ed21a08..c5a5f9ac6 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1927,8 +1927,8 @@ export default { let promotedMobileAppointment = null; const todaysDate = getTodayDate(); - let firstMobileAMAppt = await this.getFirstAvailableApptByTOD("AM", "mobile"); - let firstMobilePMAppt = await this.getFirstAvailableApptByTOD("PM", "mobile"); + let firstMobileAMAppt = await this.getFirstAvailableMobileApptByTOD("AM"); + let firstMobilePMAppt = await this.getFirstAvailableMobileApptByTOD("PM"); if (!firstMobileAMAppt && !firstMobilePMAppt) { return; } @@ -2091,10 +2091,10 @@ export default { // Fetch all appointments const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAMAppt, firstInshopPMAppt] = await Promise.all([ - this.getFirstAvailableApptByTOD("AM", "mobile"), - this.getFirstAvailableApptByTOD("PM", "mobile"), - this.getFirstAvailableApptByTOD("AM", "inshop"), - this.getFirstAvailableApptByTOD("PM", "inshop"), + this.getFirstAvailableMobileApptByTOD("AM"), + this.getFirstAvailableMobileApptByTOD("PM"), + this.getFirstAvailableInshopApptByTOD("AM"), + this.getFirstAvailableInshopApptByTOD("PM"), ]); // Extract dates @@ -2210,15 +2210,35 @@ export default { } } }, - async getFirstAvailableApptByTOD(timeOfDay, appointmentType) { - let selectableDays; - if (appointmentType?.toLowerCase() == "mobile") { - selectableDays = this.selectableDatesMobile?.days; - } else if (appointmentType?.toLowerCase() == "inshop") { - selectableDays = this.selectableDatesInshop?.days; + async getFirstAvailableMobileApptByTOD(timeOfDay) { + const selectableDays = this.selectableDatesMobile?.days; + if (!selectableDays?.length) { + return null; } else { - return; + for (const dateObj of selectableDays) { + let matchingTimeSlot = { + estimatedServiceMinutes: { + minimum: this.estimatedServiceMinutesMinimum, + maximum: this.estimatedServiceMinutesMaximum, + }, + timeSlot: null, + date: null, + }; + const isMatchingApptDay = dateObj.timeSlots.some( + (slot) => + slot.id.includes(timeOfDay) && + (matchingTimeSlot.timeSlot = slot) && + (matchingTimeSlot.date = dateObj.date) + ); + if (isMatchingApptDay && matchingTimeSlot) { + return matchingTimeSlot; + } + } + return null; } + }, + async getFirstAvailableInshopApptByTOD(timeOfDay) { + const selectableDays = this.selectableDatesInshop?.days; if (!selectableDays?.length) { return null; } else { @@ -2239,7 +2259,6 @@ export default { (matchingTimeSlot.date = dateObj.date) ); if ( - appointmentType?.toLowerCase() == "inshop" && this.selectedShopAnswer?.address1 && this.selectedShopAnswer?.address2 ) { From f686b684ce1476bd2d55f5f09da4d06ac35ed07c Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Sun, 8 Mar 2026 17:00:04 -0400 Subject: [PATCH 02/16] CASH-2378: refactoring date picker to move several functions into helper so they can be used elsewhere --- .../date-picker/date-picker.vue | 140 +----------------- .../schedule/helpers/schedule-helper.js | 134 +++++++++++++++++ 2 files changed, 138 insertions(+), 136 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 59297ae05..ae6ca6229 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -153,6 +153,8 @@ import { convertDateToDateString, convertDateStringToDate, getTodayDateString, + getInitialViewWeeks, + getMonthEnd, } from "@/layouts/schedule/helpers/schedule-helper"; import { useField, ErrorMessage } from "vee-validate"; import { deepClone } from "@/helpers/object-helper"; @@ -291,140 +293,6 @@ export default { } this.$emit("date-selected", date); }, - getWeekStartDate(dateString) { - const date = convertDateStringToDate(dateString); - const dayOfWeek = date.getDay(); - // Subtract the day of the week from date to get the date of Sunday - const sunday = new Date(date); - sunday.setDate(sunday.getDate() - dayOfWeek); - return convertDateToDateString(sunday); - }, - getWeekEndDate(dateString) { - const date = convertDateStringToDate(dateString); - const dayOfWeek = date.getDay(); - const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday - // Clone the given date and add the remaining days until Saturday - const saturday = new Date(date); - saturday.setDate(date.getDate() + daysUntilSaturday); - return convertDateToDateString(saturday); - }, - getNextWeekSunday(dateString) { - const date = convertDateStringToDate(dateString); - const dayOfWeek = date.getDay(); - const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday - // Clone the given date and add the remaining days until Sunday - const nextSunday = new Date(date); - nextSunday.setDate(date.getDate() + daysUntilNextSunday); - return convertDateToDateString(nextSunday); - }, - getMonthEnd(dateStr) { - // convert string to date, do date calc, then return a string back - const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0); - return convertDateToDateString(date); - }, - getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDate) { - // TODO: this only is for future direction; need to create logic for past direction - const weeks = []; - let weekStartDate = this.getWeekStartDate(todayString); - let weekEndDate = this.getWeekEndDate(todayString); - - if (preSelectedDate) { - let preSelectedDateMonthEnd = this.getMonthEnd( - preSelectedDate.replace("-mobile", "") - ); - let monthEndsByThisWeek = false; - let i = 0; - while (!monthEndsByThisWeek && i < 52) { - if (i > 0) { - weekStartDate = this.getNextWeekSunday(weekEndDate); - weekEndDate = this.getWeekEndDate(weekStartDate); - - if ( - (preSelectedDateMonthEnd > weekStartDate && - preSelectedDateMonthEnd < weekEndDate) || - preSelectedDateMonthEnd === weekStartDate || - preSelectedDateMonthEnd === weekEndDate - ) { - weekEndDate = preSelectedDateMonthEnd; - monthEndsByThisWeek = true; - } else if (preSelectedDateMonthEnd < weekEndDate) { - // Additional case: if month ends before this week (but not necessarily during it), - // still cut the loop here, but don't truncate the week. - - monthEndsByThisWeek = true; - } - } - weeks.push({ - weekNum: i + 1, - weekStartDate: weekStartDate, - weekEndDate: weekEndDate, - }); - i++; - } - } else { - for (let i = 0; i < initialViewRowsToShow; i++) { - if (i > 0) { - weekStartDate = this.getNextWeekSunday(weekEndDate); - weekEndDate = this.getWeekEndDate(weekStartDate); - } - weeks.push({ - weekNum: i + 1, - weekStartDate: weekStartDate, - weekEndDate: weekEndDate, - }); - } - // If any of these weeks is split between two months, then make them 2 separate "weeks" - // (a week split between two months is considered 2 weeks per business requirements) - const hasSplitWeek = (week) => { - return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1] - ? true - : false; - }; - const splitWeekIndex = weeks.findIndex(hasSplitWeek); - - if (splitWeekIndex > -1) { - const week1 = []; - const week2 = []; - let switchToWeek2 = false; - - for (let j = 0; j < 7; j++) { - const newDate = convertDateStringToDate( - weeks[splitWeekIndex].weekStartDate - ); - newDate.setDate(newDate.getDate() + j); - if (newDate.getDate() === 1) switchToWeek2 = true; - if (switchToWeek2) { - week2.push(convertDateToDateString(newDate)); - } else { - week1.push(convertDateToDateString(newDate)); - } - } - - const week1EndDate = week1[week1.length - 1]; - const week2StartDate = week2[0]; - - if (week1EndDate < todayString) { - // replace week 1 with week 2 - weeks[splitWeekIndex].weekStartDate = week2StartDate; - } else { - const newWeek = { - weekNum: weeks[splitWeekIndex].weekNum, - weekStartDate: week2StartDate, - weekEndDate: weeks[splitWeekIndex].weekEndDate, - }; - weeks[splitWeekIndex].weekEndDate = week1EndDate; - weeks.splice(splitWeekIndex + 1, 0, newWeek); - weeks.pop(); - weeks.forEach((item, index) => { - if (index > splitWeekIndex) { - item.weekNum = item.weekNum + 1; - } - }); - } - } - } - return weeks; - }, async loadInitialData(config) { /* ** NOTE: this _could_ be called by a parent before fully loaded, so FYI component data or computeds might not be available @@ -444,10 +312,10 @@ export default { if (config.selectableDatesSetting === "past") calendarViewDirection = "past"; if (config.selectableDatesSetting === "custom") calendarViewDirection = "future"; - const currentMonthEnd = this.getMonthEnd(todayDateString); + const currentMonthEnd = getMonthEnd(todayDateString); // TODO - set up currentMonthStart if direction is PAST: // let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1); - const initialViewWeeks = this.getInitialViewWeeks( + const initialViewWeeks = getInitialViewWeeks( todayDateString, config.initialViewRowsToShow, config.preSelectedDate diff --git a/src/layouts/schedule/helpers/schedule-helper.js b/src/layouts/schedule/helpers/schedule-helper.js index 4952ce2a9..799d4b92f 100644 --- a/src/layouts/schedule/helpers/schedule-helper.js +++ b/src/layouts/schedule/helpers/schedule-helper.js @@ -79,3 +79,137 @@ export function getTodayDate() { export function getTodayDateString() { return convertDateToDateString(getTodayDate()); } + +export function getMonthEnd(dateStr) { + // convert string to date, do date calc, then return a string back + const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0); + return convertDateToDateString(date); +} + +export function getWeekStartDate(dateString) { + const date = convertDateStringToDate(dateString); + const dayOfWeek = date.getDay(); + // Subtract the day of the week from date to get the date of Sunday + const sunday = new Date(date); + sunday.setDate(sunday.getDate() - dayOfWeek); + return convertDateToDateString(sunday); +} + +export function getWeekEndDate(dateString) { + const date = convertDateStringToDate(dateString); + const dayOfWeek = date.getDay(); + const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday + // Clone the given date and add the remaining days until Saturday + const saturday = new Date(date); + saturday.setDate(date.getDate() + daysUntilSaturday); + return convertDateToDateString(saturday); +} + +export function getNextWeekSunday(dateString) { + const date = convertDateStringToDate(dateString); + const dayOfWeek = date.getDay(); + const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday + // Clone the given date and add the remaining days until Sunday + const nextSunday = new Date(date); + nextSunday.setDate(date.getDate() + daysUntilNextSunday); + return convertDateToDateString(nextSunday); +} + +export function getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDate) { + const weeks = []; + let weekStartDate = getWeekStartDate(todayString); + let weekEndDate = getWeekEndDate(todayString); + + if (preSelectedDate) { + let preSelectedDateMonthEnd = getMonthEnd(preSelectedDate.replace("-mobile", "")); + let monthEndsByThisWeek = false; + let i = 0; + while (!monthEndsByThisWeek && i < 52) { + if (i > 0) { + weekStartDate = getNextWeekSunday(weekEndDate); + weekEndDate = getWeekEndDate(weekStartDate); + + if ( + (preSelectedDateMonthEnd > weekStartDate && + preSelectedDateMonthEnd < weekEndDate) || + preSelectedDateMonthEnd === weekStartDate || + preSelectedDateMonthEnd === weekEndDate + ) { + weekEndDate = preSelectedDateMonthEnd; + monthEndsByThisWeek = true; + } else if (preSelectedDateMonthEnd < weekEndDate) { + // Additional case: if month ends before this week (but not necessarily during it), + // still cut the loop here, but don't truncate the week. + + monthEndsByThisWeek = true; + } + } + weeks.push({ + weekNum: i + 1, + weekStartDate: weekStartDate, + weekEndDate: weekEndDate, + }); + i++; + } + } else { + for (let i = 0; i < initialViewRowsToShow; i++) { + if (i > 0) { + weekStartDate = getNextWeekSunday(weekEndDate); + weekEndDate = getWeekEndDate(weekStartDate); + } + weeks.push({ + weekNum: i + 1, + weekStartDate: weekStartDate, + weekEndDate: weekEndDate, + }); + } + // If any of these weeks is split between two months, then make them 2 separate "weeks" + // (a week split between two months is considered 2 weeks per business requirements) + const hasSplitWeek = (week) => { + return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1] + ? true + : false; + }; + const splitWeekIndex = weeks.findIndex(hasSplitWeek); + + if (splitWeekIndex > -1) { + const week1 = []; + const week2 = []; + let switchToWeek2 = false; + + for (let j = 0; j < 7; j++) { + const newDate = convertDateStringToDate(weeks[splitWeekIndex].weekStartDate); + newDate.setDate(newDate.getDate() + j); + if (newDate.getDate() === 1) switchToWeek2 = true; + if (switchToWeek2) { + week2.push(convertDateToDateString(newDate)); + } else { + week1.push(convertDateToDateString(newDate)); + } + } + + const week1EndDate = week1[week1.length - 1]; + const week2StartDate = week2[0]; + + if (week1EndDate < todayString) { + // replace week 1 with week 2 + weeks[splitWeekIndex].weekStartDate = week2StartDate; + } else { + const newWeek = { + weekNum: weeks[splitWeekIndex].weekNum, + weekStartDate: week2StartDate, + weekEndDate: weeks[splitWeekIndex].weekEndDate, + }; + weeks[splitWeekIndex].weekEndDate = week1EndDate; + weeks.splice(splitWeekIndex + 1, 0, newWeek); + weeks.pop(); + weeks.forEach((item, index) => { + if (index > splitWeekIndex) { + item.weekNum = item.weekNum + 1; + } + }); + } + } + } + return weeks; +} From a8694c726847753b095ca1310f2021e94a2208d7 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 11:21:38 -0400 Subject: [PATCH 03/16] First round of page-prerequesting-helper refactors for later pages in fmg where a lot of duplication --- src/helpers/page-prerequisites-helper.spec.js | 641 ++++++++++++++++++ src/layouts/payment-adyen/payment-adyen.vue | 126 +--- src/layouts/payment-method/payment-method.vue | 105 +-- src/layouts/payment/payment.vue | 126 +--- src/layouts/quote/quote.vue | 42 +- 5 files changed, 726 insertions(+), 314 deletions(-) create mode 100644 src/helpers/page-prerequisites-helper.spec.js diff --git a/src/helpers/page-prerequisites-helper.spec.js b/src/helpers/page-prerequisites-helper.spec.js new file mode 100644 index 000000000..9a85c2c75 --- /dev/null +++ b/src/helpers/page-prerequisites-helper.spec.js @@ -0,0 +1,641 @@ +import * as pagePrereqsHelper from "@/helpers/page-prerequisites-helper"; +import store from "@/store"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; +import { coverageStatus } from "@/constants/insurance"; + +jest.mock("@/helpers/debug-log-helper.js", () => ({ + debugLog: jest.fn(), +})); + +const { debugLog } = require("@/helpers/debug-log-helper.js"); + +describe("page-prerequisites-helper.js", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("logPagePrereqsStart", () => { + test("calls debugLog with source and forceLog when preReqResult is false", () => { + pagePrereqsHelper.logPagePrereqsStart("payment.vue", false); + + expect(debugLog).toHaveBeenCalledWith( + "--- payment.vue pagePrereqs start ---", + null, + true + ); + }); + + test("calls debugLog with forceLog false when preReqResult is true", () => { + pagePrereqsHelper.logPagePrereqsStart("payment.vue", true); + + expect(debugLog).toHaveBeenCalledWith( + "--- payment.vue pagePrereqs start ---", + null, + false + ); + }); + }); + + describe("logPagePrereqsEnd", () => { + test("calls debugLog with source and forceLog when preReqResult is false", () => { + pagePrereqsHelper.logPagePrereqsEnd("payment-method.vue", false); + + expect(debugLog).toHaveBeenCalledWith( + "--- payment-method.vue pagePrereqs end ---", + null, + true + ); + }); + + test("calls debugLog with forceLog false when preReqResult is true", () => { + pagePrereqsHelper.logPagePrereqsEnd("payment-method.vue", true); + + expect(debugLog).toHaveBeenCalledWith( + "--- payment-method.vue pagePrereqs end ---", + null, + false + ); + }); + }); + + describe("hasServiceZipInfo", () => { + test("returns true when zipCode and zipCodeCtu are set", () => { + const order = { + serviceLocation: { zipCode: "12345", zipCodeCtu: "12345" }, + }; + + const result = pagePrereqsHelper.hasServiceZipInfo(order); + + expect(result).toBe(true); + }); + + test("returns false when zipCode is missing", () => { + const order = { + serviceLocation: { zipCode: null, zipCodeCtu: "12345" }, + }; + + const result = pagePrereqsHelper.hasServiceZipInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when zipCodeCtu is missing", () => { + const order = { + serviceLocation: { zipCode: "12345", zipCodeCtu: null }, + }; + + const result = pagePrereqsHelper.hasServiceZipInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when both are empty strings", () => { + const order = { + serviceLocation: { zipCode: "", zipCodeCtu: "" }, + }; + + const result = pagePrereqsHelper.hasServiceZipInfo(order); + + expect(result).toBe(false); + }); + + test("does not add to logQueue when logQueue is null", () => { + const order = { + serviceLocation: { zipCode: "12345", zipCodeCtu: "12345" }, + }; + + pagePrereqsHelper.hasServiceZipInfo(order, null); + + expect(debugLog).not.toHaveBeenCalled(); + }); + + test("adds log functions to logQueue when logQueue is provided", () => { + const order = { + serviceLocation: { zipCode: "12345", zipCodeCtu: "12345" }, + }; + const logQueue = []; + + pagePrereqsHelper.hasServiceZipInfo(order, logQueue); + + expect(logQueue).toHaveLength(1); + logQueue[0](); + expect(debugLog).toHaveBeenCalled(); + }); + }); + + describe("hasServiceLocationInfo", () => { + const baseOrder = { + serviceLocation: { + provider: { + address: { + streetAddress: "123 Provider St", + city: "ProviderCity", + state: "OH", + zipCode: "00000", + }, + }, + }, + }; + + test("returns true for mobile when address, city, state, zipCode are set", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + address: "123 Main St", + city: "Anytown", + state: "OH", + zipCode: "12345", + appointmentType: AppointmentTypeStrings.MOBILE, + provider: baseOrder.serviceLocation.provider, + }, + }; + + const result = pagePrereqsHelper.hasServiceLocationInfo(order); + + expect(result).toBe(true); + }); + + test("returns false for mobile when address is missing", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + address: null, + city: "Anytown", + state: "OH", + zipCode: "12345", + appointmentType: AppointmentTypeStrings.MOBILE, + provider: baseOrder.serviceLocation.provider, + }, + }; + + const result = pagePrereqsHelper.hasServiceLocationInfo(order); + + expect(result).toBe(false); + }); + + test("returns true for drop-off when provider address has all fields", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + appointmentType: AppointmentTypeStrings.DROP_OFF, + }, + }; + + const result = pagePrereqsHelper.hasServiceLocationInfo(order); + + expect(result).toBe(true); + }); + + test("returns false for drop-off when provider streetAddress is missing", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + appointmentType: AppointmentTypeStrings.DROP_OFF, + provider: { + address: { + streetAddress: null, + city: "ProviderCity", + state: "OH", + zipCode: "00000", + }, + }, + }, + }; + + const result = pagePrereqsHelper.hasServiceLocationInfo(order); + + expect(result).toBe(false); + }); + + test("returns true for in-shop when provider address has all fields", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + appointmentType: AppointmentTypeStrings.IN_SHOP, + }, + }; + + const result = pagePrereqsHelper.hasServiceLocationInfo(order); + + expect(result).toBe(true); + }); + + test("does not add to logQueue when logQueue is null", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + appointmentType: AppointmentTypeStrings.MOBILE, + address: "123", + city: "City", + state: "OH", + zipCode: "12345", + provider: baseOrder.serviceLocation.provider, + }, + }; + + pagePrereqsHelper.hasServiceLocationInfo(order, null); + + expect(debugLog).not.toHaveBeenCalled(); + }); + + test("adds log functions to logQueue when logQueue is provided", () => { + const order = { + ...baseOrder, + serviceLocation: { + ...baseOrder.serviceLocation, + appointmentType: AppointmentTypeStrings.MOBILE, + address: "123", + city: "City", + state: "OH", + zipCode: "12345", + provider: baseOrder.serviceLocation.provider, + }, + }; + const logQueue = []; + + pagePrereqsHelper.hasServiceLocationInfo(order, logQueue); + + expect(logQueue).toHaveLength(1); + expect(typeof logQueue[0]).toBe("function"); + logQueue[0](); + expect(debugLog).toHaveBeenCalled(); + }); + }); + + describe("hasInsuranceInfo", () => { + const baseOrder = { + payment: { + isInsurance: false, + }, + }; + + beforeEach(() => { + store.getters = { + payment: { + insuranceCoverage: { + coverageStatus: null, + }, + }, + policy: { + currentDeductible: 100, + }, + }; + }); + + test("returns false when isInsurance is null", () => { + const order = { payment: { isInsurance: null } }; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(false); + }); + + test("returns true when isInsurance is false and coverage is not verified", () => { + // Arrange + store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.PENDING; + + const result = pagePrereqsHelper.hasInsuranceInfo(baseOrder); + + expect(result).toBe(true); + }); + + test("returns true when isInsurance is true and coverage is verified with valid deductible", () => { + const order = { payment: { isInsurance: true } }; + store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; + store.getters.policy.currentDeductible = 100; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(true); + }); + + test("returns true when coverage is verified and deductible is 0", () => { + const order = { payment: { isInsurance: true } }; + store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; + store.getters.policy.currentDeductible = 0; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(true); + }); + + test("returns false when coverage is verified but deductible is null", () => { + const order = { payment: { isInsurance: true } }; + store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; + store.getters.policy.currentDeductible = null; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when coverage is verified but deductible is undefined", () => { + const order = { payment: { isInsurance: true } }; + store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; + store.getters.policy.currentDeductible = undefined; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when coverage is verified but deductible is negative", () => { + const order = { payment: { isInsurance: true } }; + store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; + store.getters.policy.currentDeductible = -1; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(false); + }); + + test("adds log functions to logQueue when logQueue is provided", () => { + const order = { payment: { isInsurance: null } }; + const logQueue = []; + + pagePrereqsHelper.hasInsuranceInfo(order, logQueue); + + expect(logQueue).toHaveLength(1); + logQueue[0](); + expect(debugLog).toHaveBeenCalled(); + }); + }); + + describe("hasSchedulingInfo", () => { + const validSchedule = { + date: "2024-01-15", + startTime: "09:00", + endTime: "17:00", + jobMinMinutes: "30", + jobMaxMinutes: "45", + }; + + test("returns true when all schedule fields are set", () => { + const order = { schedule: validSchedule }; + + const result = pagePrereqsHelper.hasSchedulingInfo(order); + + expect(result).toBe(true); + }); + + test("returns false when date is missing", () => { + const order = { + schedule: { ...validSchedule, date: null }, + }; + + const result = pagePrereqsHelper.hasSchedulingInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when startTime is missing", () => { + const order = { + schedule: { ...validSchedule, startTime: null }, + }; + + const result = pagePrereqsHelper.hasSchedulingInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when jobMinMinutes is missing", () => { + const order = { + schedule: { ...validSchedule, jobMinMinutes: null }, + }; + + const result = pagePrereqsHelper.hasSchedulingInfo(order); + + expect(result).toBe(false); + }); + + test("does not add to logQueue when logQueue is null", () => { + const order = { schedule: validSchedule }; + + pagePrereqsHelper.hasSchedulingInfo(order, null); + + expect(debugLog).not.toHaveBeenCalled(); + }); + + test("adds log functions to logQueue when logQueue is provided", () => { + const order = { schedule: validSchedule }; + const logQueue = []; + + pagePrereqsHelper.hasSchedulingInfo(order, logQueue); + + expect(logQueue).toHaveLength(1); + logQueue[0](); + expect(debugLog).toHaveBeenCalled(); + }); + }); + + describe("hasCustomerInfo", () => { + const validCustomer = { + firstName: "John", + lastName: "Doe", + phoneNumber: "555-555-5555", + emailAddress: "john@example.com", + }; + + test("returns true when all customer fields are set", () => { + const order = { customer: validCustomer }; + + const result = pagePrereqsHelper.hasCustomerInfo(order); + + expect(result).toBe(true); + }); + + test("returns false when firstName is missing", () => { + const order = { + customer: { ...validCustomer, firstName: null }, + }; + + const result = pagePrereqsHelper.hasCustomerInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when emailAddress is missing", () => { + const order = { + customer: { ...validCustomer, emailAddress: "" }, + }; + + const result = pagePrereqsHelper.hasCustomerInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when phoneNumber is empty", () => { + const order = { + customer: { ...validCustomer, phoneNumber: "" }, + }; + + const result = pagePrereqsHelper.hasCustomerInfo(order); + + expect(result).toBe(false); + }); + + test("does not add to logQueue when logQueue is null", () => { + const order = { customer: validCustomer }; + + pagePrereqsHelper.hasCustomerInfo(order, null); + + expect(debugLog).not.toHaveBeenCalled(); + }); + }); + + describe("hasGlassPartsOrRepairInfo", () => { + test("returns true when isRepair is true", () => { + const order = { + damage: { isRepair: true }, + lineItems: { glassParts: [] }, + }; + + const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order); + + expect(result).toBe(true); + }); + + test("returns true when glassParts has items", () => { + const order = { + damage: { isRepair: false }, + lineItems: { glassParts: [{ id: "1" }] }, + }; + + const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order); + + expect(result).toBe(true); + }); + + test("returns false when isRepair is false and glassParts is empty", () => { + const order = { + damage: { isRepair: false }, + lineItems: { glassParts: [] }, + }; + + const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when isRepair is false and glassParts is null", () => { + const order = { + damage: { isRepair: false }, + lineItems: { glassParts: null }, + }; + + const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when damage and lineItems are undefined", () => { + const order = {}; + + const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order); + + expect(result).toBe(false); + }); + + test("does not add to logQueue when logQueue is null", () => { + const order = { + damage: { isRepair: true }, + lineItems: { glassParts: [] }, + }; + + pagePrereqsHelper.hasGlassPartsOrRepairInfo(order, null); + + expect(debugLog).not.toHaveBeenCalled(); + }); + + test("adds log functions to logQueue when logQueue is provided", () => { + const order = { + damage: { isRepair: true }, + lineItems: { glassParts: [] }, + }; + const logQueue = []; + + pagePrereqsHelper.hasGlassPartsOrRepairInfo(order, logQueue); + + expect(logQueue).toHaveLength(1); + logQueue[0](); + expect(debugLog).toHaveBeenCalled(); + }); + }); + + describe("hasPaymentMethodInfo", () => { + test("returns true when isPia is true and piaType is set", () => { + const order = { + payment: { isPia: true, piaType: "Afterpay" }, + }; + + const result = pagePrereqsHelper.hasPaymentMethodInfo(order); + + expect(result).toBe(true); + }); + + test("returns true when isPia is false and piaType is set", () => { + const order = { + payment: { isPia: false, piaType: "PayLater" }, + }; + + const result = pagePrereqsHelper.hasPaymentMethodInfo(order); + + expect(result).toBe(true); + }); + + test("returns false when isPia is null", () => { + const order = { + payment: { isPia: null, piaType: "Afterpay" }, + }; + + const result = pagePrereqsHelper.hasPaymentMethodInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when isPia is false and piaType is null", () => { + const order = { + payment: { isPia: false, piaType: null }, + }; + + const result = pagePrereqsHelper.hasPaymentMethodInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when isPia is false and piaType is empty string", () => { + const order = { + payment: { isPia: false, piaType: "" }, + }; + + const result = pagePrereqsHelper.hasPaymentMethodInfo(order); + + expect(result).toBe(false); + }); + + test("does not add to logQueue when logQueue is null", () => { + const order = { payment: { isPia: true, piaType: "Afterpay" } }; + + pagePrereqsHelper.hasPaymentMethodInfo(order, null); + + expect(debugLog).not.toHaveBeenCalled(); + }); + + test("adds log functions to logQueue when logQueue is provided", () => { + const order = { payment: { isPia: true, piaType: "Afterpay" } }; + const logQueue = []; + + pagePrereqsHelper.hasPaymentMethodInfo(order, logQueue); + + expect(logQueue).toHaveLength(1); + logQueue[0](); + expect(debugLog).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index cddb2189c..8abd3ae95 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -83,7 +83,15 @@ import cart from "@/fmg-components/cart/cart"; import { coverageStatus } from "@/constants/insurance"; import { deepClone } from "@/helpers/object-helper"; import store from "@/store"; -import { debugLog } from "@/helpers/debug-log-helper.js"; +import { + logPagePrereqsStart, + logPagePrereqsEnd, + hasServiceLocationInfo, + hasInsuranceInfo, + hasSchedulingInfo, + hasCustomerInfo, + hasPaymentMethodInfo, +} from "@/helpers/page-prerequisites-helper.js"; export default { name: "payment-adyen", @@ -136,110 +144,18 @@ export default { }, arePagePrerequisitesValid() { - // Service Location - const serviceLocation = store.getters.order.serviceLocation; - const mobileReqs = !!( - serviceLocation.address && - serviceLocation.city && - serviceLocation.state && - serviceLocation.zipCode - ); - - const providerLocation = serviceLocation.provider.address; - const dropOffInshopReqs = !!( - providerLocation.streetAddress && - providerLocation.city && - providerLocation.state && - providerLocation.zipCode - ); - - const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; - - const serviceLocationReqs = - (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs); - - // Insurance - const isInsuranceSet = store.getters.order.payment.isInsurance !== null; - - // Schedule - const schedule = store.getters.order.schedule; - const scheduleReqs = !!( - schedule.date && - schedule.startTime && - schedule.endTime && - schedule.jobMaxMinutes && - schedule.jobMinMinutes - ); - - // Customer - const customer = store.getters.order.customer; - const customerReqs = !!( - customer.firstName && - customer.lastName && - customer.phoneNumber && - customer.emailAddress - ); - - const paymentMethodReqs = - store.getters.order.payment.isPia !== null && - (store.getters.order.payment.isPia || !!store.getters.order.payment.piaType); - - const preReqResult = - serviceLocationReqs && - isInsuranceSet && - scheduleReqs && - customerReqs && - paymentMethodReqs; - - // prettier-ignore - { - debugLog("--- payment-adyen.vue pagePrereqs start ---", null, !preReqResult); - debugLog("mobileReqs:", mobileReqs, !preReqResult); - debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult); - debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult); - debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult); - debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult); - debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult); - debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult); - debugLog("providerLocation.city:", providerLocation.city, !preReqResult); - debugLog("providerLocation.state:", providerLocation.state, !preReqResult); - debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("scheduleReqs:", scheduleReqs, !preReqResult); - debugLog("schedule.date:", schedule.date, !preReqResult); - debugLog("schedule.startTime:", schedule.startTime, !preReqResult); - debugLog("schedule.endTime:", schedule.endTime, !preReqResult); - debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult); - debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("customerReqs:", customerReqs, !preReqResult); - debugLog("customer.firstName:", customer.firstName, !preReqResult); - debugLog("customer.lastName:", customer.lastName, !preReqResult); - debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult); - debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult); - debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult); - debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult); - - debugLog("--- payment-adyen.vue pagePrereqs end ---", null, !preReqResult); - } - - return preReqResult; + const order = store.getters.order; + const logQueue = []; + const result = + hasServiceLocationInfo(order, logQueue) && + hasInsuranceInfo(order, logQueue) && + hasSchedulingInfo(order, logQueue) && + hasCustomerInfo(order, logQueue) && + hasPaymentMethodInfo(order, logQueue); + logPagePrereqsStart("payment-adyen.vue", result); + logQueue.forEach((fn) => fn()); + logPagePrereqsEnd("payment-adyen.vue", result); + return result; }, async initializeAdyen() { diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index d333b4bbf..a9a507690 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -178,7 +178,14 @@ import { containsRecalParts } from "@/helpers/recal-helper"; import { getBoolFromString } from "@/helpers/boolean-helper"; import { getAmountDue, addPricesToLineItems } from "@/helpers/pricing-helper.js"; import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash"; -import { debugLog } from "@/helpers/debug-log-helper"; +import { + logPagePrereqsStart, + logPagePrereqsEnd, + hasServiceLocationInfo, + hasInsuranceInfo, + hasSchedulingInfo, + hasCustomerInfo, +} from "@/helpers/page-prerequisites-helper.js"; import { ErrorMessage } from "vee-validate"; import { Field } from "vee-validate"; import experimentMixin from "../../mixins/experiment-mixin"; @@ -381,91 +388,17 @@ export default { this.$router.navigateWithoutSaving(scenarioName, this.pageName); }, arePagePrerequisitesValid() { - // Service Location - const serviceLocation = store.getters.order.serviceLocation; - const mobileReqs = !!( - serviceLocation.address && - serviceLocation.city && - serviceLocation.state && - serviceLocation.zipCode - ); - - const providerLocation = serviceLocation.provider.address; - const dropOffInshopReqs = !!( - providerLocation.streetAddress && - providerLocation.city && - providerLocation.state && - providerLocation.zipCode - ); - - const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; - - const serviceLocationReqs = - (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs); - - // Insurance - const isInsurance = store.getters.order.payment.isInsurance; - const insuranceCoverageStatus = store.getters.payment.insuranceCoverage.coverageStatus; - const currentDeductible = store.getters.policy.currentDeductible; - const isInsuranceSet = () => { - if (isInsurance !== null) { - if ( - insuranceCoverageStatus === coverageStatus.VERIFIED && - !(currentDeductible === 0 || currentDeductible > 0) - ) { - return false; // if coverageStatus is verified there must be a valid deductible also - } - return true; - } - return false; - }; - - // Schedule - const schedule = store.getters.order.schedule; - const scheduleReqs = !!( - schedule.date && - schedule.startTime && - schedule.endTime && - schedule.jobMaxMinutes && - schedule.jobMinMinutes - ); - - // Customer - const customer = store.getters.order.customer; - const customerReqs = !!( - customer.firstName && - customer.lastName && - customer.phoneNumber && - customer.emailAddress - ); - - const preReqResult = - serviceLocationReqs && isInsuranceSet() && scheduleReqs && customerReqs; - - // prettier-ignore - { - debugLog("--- payment-method.vue pagePrereqs start ---", null, !preReqResult); - debugLog("serviceLocationRequs::isMobile:", isMobile, !preReqResult); - debugLog("serviceLocationReqs::mobileReqs:", mobileReqs, !preReqResult); - debugLog("serviceLocationReqs::dropOffInshopReqs:", dropOffInshopReqs, !preReqResult); - debugLog("serviceLocationReqs:: result", serviceLocationReqs, !preReqResult); - - debugLog("isInsuranceSet::", isInsuranceSet(), !preReqResult); - - debugLog("scheduleReqs::schedule.date:", schedule.date, !preReqResult); - debugLog("scheduleReqs::schedule.startTime:", schedule.startTime, !preReqResult); - debugLog("scheduleReqs::schedule.endTime:", schedule.endTime, !preReqResult); - debugLog("scheduleReqs::schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult); - debugLog("scheduleReqs::schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult); - - debugLog("customerReqs::customer.firstName:", customer.firstName, !preReqResult); - debugLog("customerReqs::customer.lastName:", customer.lastName, !preReqResult); - debugLog("customerReqs::customer.phoneNumber:", customer.phoneNumber, !preReqResult); - debugLog("customerReqs::customer.emailAddress:", customer.emailAddress, !preReqResult); - debugLog("--- payment-method.vue pagePrereqs end ---", null, !preReqResult); - } - - return preReqResult; + const order = store.getters.order; + const logQueue = []; + const result = + hasServiceLocationInfo(order, logQueue) && + hasInsuranceInfo(order, logQueue) && + hasSchedulingInfo(order, logQueue) && + hasCustomerInfo(order, logQueue); + logPagePrereqsStart("payment-method.vue", result); + logQueue.forEach((fn) => fn()); + logPagePrereqsEnd("payment-method.vue", result); + return result; }, getPaymentMethodFromStore() { const piaType = store.getters.order.payment.piaType; diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 6c547f3d2..894c0e497 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -235,7 +235,6 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; import { applicationConfig } from "@/constants/application-config"; import { paymentMethods } from "@/constants/payment-method-constants"; -import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import baseMixin from "@/mixins/base-mixin.js"; import { deepClone } from "@/helpers/object-helper"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; @@ -243,6 +242,15 @@ import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer. import { coverageStatus } from "@/constants/insurance"; import { getDisplayAmountDue, getAmountDue } from "@/helpers/pricing-helper.js"; import { debugLog } from "@/helpers/debug-log-helper.js"; +import { + logPagePrereqsStart, + logPagePrereqsEnd, + hasServiceLocationInfo, + hasInsuranceInfo, + hasSchedulingInfo, + hasCustomerInfo, + hasPaymentMethodInfo, +} from "@/helpers/page-prerequisites-helper.js"; import buttonQuestion from "@/digital-components/button-question/button-question"; import paymentMethodListButton from "@/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button"; @@ -435,110 +443,18 @@ export default { }, methods: { arePagePrerequisitesValid() { - // Service Location - const serviceLocation = store.getters.order.serviceLocation; - const mobileReqs = !!( - serviceLocation.address && - serviceLocation.city && - serviceLocation.state && - serviceLocation.zipCode - ); - - const providerLocation = serviceLocation.provider.address; - const dropOffInshopReqs = !!( - providerLocation.streetAddress && - providerLocation.city && - providerLocation.state && - providerLocation.zipCode - ); - - const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; - - const serviceLocationReqs = - (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs); - - // Insurance - const isInsuranceSet = store.getters.order.payment.isInsurance !== null; - - // Schedule - const schedule = store.getters.order.schedule; - const scheduleReqs = !!( - schedule.date && - schedule.startTime && - schedule.endTime && - schedule.jobMaxMinutes && - schedule.jobMinMinutes - ); - - // Customer - const customer = store.getters.order.customer; - const customerReqs = !!( - customer.firstName && - customer.lastName && - customer.phoneNumber && - customer.emailAddress - ); - - const paymentMethodReqs = - store.getters.order.payment.isPia !== null && - (store.getters.order.payment.isPia || !!store.getters.order.payment.piaType); - - const preReqResult = - serviceLocationReqs && - isInsuranceSet && - scheduleReqs && - customerReqs && - paymentMethodReqs; - - // prettier-ignore - { - debugLog("--- payment.vue pagePrereqs start ---", null, !preReqResult); - debugLog("mobileReqs:", mobileReqs, !preReqResult); - debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult); - debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult); - debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult); - debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult); - debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult); - debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult); - debugLog("providerLocation.city:", providerLocation.city, !preReqResult); - debugLog("providerLocation.state:", providerLocation.state, !preReqResult); - debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("scheduleReqs:", scheduleReqs, !preReqResult); - debugLog("schedule.date:", schedule.date, !preReqResult); - debugLog("schedule.startTime:", schedule.startTime, !preReqResult); - debugLog("schedule.endTime:", schedule.endTime, !preReqResult); - debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult); - debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("customerReqs:", customerReqs, !preReqResult); - debugLog("customer.firstName:", customer.firstName, !preReqResult); - debugLog("customer.lastName:", customer.lastName, !preReqResult); - debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult); - debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult); - - debugLog("", null, !preReqResult); - - debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult); - debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult); - debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult); - - debugLog("--- payment.vue pagePrereqs end ---", null, !preReqResult); - } - - return preReqResult; + const order = store.getters.order; + const logQueue = []; + const result = + hasServiceLocationInfo(order, logQueue) && + hasInsuranceInfo(order, logQueue) && + hasSchedulingInfo(order, logQueue) && + hasCustomerInfo(order, logQueue) && + hasPaymentMethodInfo(order, logQueue); + logPagePrereqsStart("payment.vue", result); + logQueue.forEach((fn) => fn()); + logPagePrereqsEnd("payment.vue", result); + return result; }, getAnswersNullSafe(widgetName) { const rawData = this.getCmsContent(widgetName, "Answers"); diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 0bd095b77..b36e4ddc3 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -183,6 +183,12 @@ import { routeData } from "@/router/constants/routes"; import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash"; import { storeMutations } from "@/constants/store-mutations"; import { debugLog } from "@/helpers/debug-log-helper"; +import { + logPagePrereqsStart, + logPagePrereqsEnd, + hasServiceZipInfo, + hasGlassPartsOrRepairInfo, +} from "@/helpers/page-prerequisites-helper.js"; import { savePageData } from "@/router/methods/helpers/save-page-data"; import { quotePageDiscountTable } from "../../constants/quote-page-discounts"; @@ -728,28 +734,28 @@ export default { this.$refs[modalName].openModal(); }, arePagePrerequisitesValid() { - const payment = store.getters.order.payment; + const order = store.getters.order; + const logQueue = []; + const isVerifiedOk = + order.payment?.insuranceCoverage?.isVerified == null || + order.payment?.insuranceCoverage?.isVerified === false; const preReqResult = - store.getters.order.serviceLocation.zipCode && - store.getters.order.serviceLocation.zipCodeCtu && - (store.getters.order.damage.isRepair || - (store.getters.order.lineItems?.glassParts != null && - store.getters.order.lineItems.glassParts.length > 0)) && - (payment.insuranceCoverage?.isVerified == null || - payment.insuranceCoverage?.isVerified === false); + hasServiceZipInfo(order, logQueue) && + hasGlassPartsOrRepairInfo(order, logQueue) && + isVerifiedOk; - // prettier-ignore - { - debugLog("--- quote.vue pagePrereqs start ---", null, !preReqResult); - debugLog("store.getters.order.serviceLocation.zipCode:", store.getters.order.serviceLocation?.zipCode, !preReqResult); - debugLog("store.getters.order.serviceLocation.zipCodeCtu:", store.getters.order.serviceLocation?.zipCodeCtu, !preReqResult); - debugLog("store.getters.order.damage.isRepair:", store.getters.order.damage?.isRepair, !preReqResult); - debugLog("store.getters.order.lineItems?.glassParts:", store.getters.order.lineItems?.glassParts, !preReqResult); - debugLog("store.getters.order.payemt.insuranceCoverage?.isVerified:", store.getters.order.payment?.insuranceCoverage?.isVerified, !preReqResult); - debugLog("--- quote.vue pagePrereqs end ---", null, !preReqResult); - } + logQueue.push(() => { + debugLog( + "payment.insuranceCoverage?.isVerified:", + order.payment?.insuranceCoverage?.isVerified, + !preReqResult + ); + }); + logPagePrereqsStart("quote.vue", preReqResult); + logQueue.forEach((fn) => fn()); + logPagePrereqsEnd("quote.vue", preReqResult); return preReqResult; }, vapsItemsSelectedAction(vapsItemsSelected) { From 6997c2160dcea54d28cf359de59679b883de781a Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 11:22:57 -0400 Subject: [PATCH 04/16] 2 additl files part of page prereqs refactor --- src/helpers/page-prerequisites-helper.js | 199 ++++++++++++++++++ src/layouts/mobile-details/mobile-details.vue | 24 ++- 2 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 src/helpers/page-prerequisites-helper.js diff --git a/src/helpers/page-prerequisites-helper.js b/src/helpers/page-prerequisites-helper.js new file mode 100644 index 000000000..c6ad21ff2 --- /dev/null +++ b/src/helpers/page-prerequisites-helper.js @@ -0,0 +1,199 @@ +import { debugLog } from "@/helpers/debug-log-helper.js"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; +import { coverageStatus } from "@/constants/insurance"; +import store from "@/store"; + +export function logPagePrereqsStart(source, preReqResult) { + // prettier-ignore + { + debugLog(`--- ${source} pagePrereqs start ---`, null, !preReqResult); + } +} + +export function logPagePrereqsEnd(source, preReqResult) { + // prettier-ignore + { + debugLog(`--- ${source} pagePrereqs end ---`, null, !preReqResult); + } +} + +function queueLogging(logQueue, logFn) { + if (logQueue) { + logQueue.push(logFn); + } +} + +export function hasServiceZipInfo(order, logQueue = null) { + const serviceLocation = order.serviceLocation; + const result = !!(serviceLocation.zipCode && serviceLocation.zipCodeCtu); + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasServiceZipInfo:", result, !result); + debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !result); + debugLog("serviceLocation.zipCodeCtu:", serviceLocation.zipCodeCtu, !result); + debugLog("", null, !result); + } + }); + + return result; +} + +export function hasServiceLocationInfo(order, logQueue = null) { + const serviceLocation = order.serviceLocation; + const mobileReqs = !!( + serviceLocation.address && + serviceLocation.city && + serviceLocation.state && + serviceLocation.zipCode + ); + + const providerLocation = serviceLocation.provider.address; + const dropOffInshopReqs = !!( + providerLocation.streetAddress && + providerLocation.city && + providerLocation.state && + providerLocation.zipCode + ); + + const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; + const result = (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs); + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasServiceLocationInfo mobileReqs:", mobileReqs, !result); + debugLog("serviceLocation.address:", serviceLocation.address, !result); + debugLog("serviceLocation.city:", serviceLocation.city, !result); + debugLog("serviceLocation.state:", serviceLocation.state, !result); + debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !result); + debugLog("dropOffInshopReqs:", dropOffInshopReqs, !result); + debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !result); + debugLog("providerLocation.city:", providerLocation.city, !result); + debugLog("providerLocation.state:", providerLocation.state, !result); + debugLog("providerLocation.zipCode:", providerLocation.zipCode, !result); + debugLog("", null, !result); + } + }); + + return result; +} + +export function hasInsuranceInfo(order, logQueue = null) { + const isInsurance = order.payment.isInsurance; + const insuranceCoverageStatus = store.getters.payment?.insuranceCoverage?.coverageStatus; + const currentDeductible = store.getters.policy?.currentDeductible; + + let result = false; + if (isInsurance !== null) { + if ( + insuranceCoverageStatus === coverageStatus.VERIFIED && + !(currentDeductible === 0 || currentDeductible > 0) + ) { + result = false; // if coverageStatus is verified there must be a valid deductible also + } else { + result = true; + } + } + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasInsuranceInfo:", result, !result); + if (insuranceCoverageStatus === coverageStatus.VERIFIED) { + debugLog("hasInsuranceInfo coverageStatus:", insuranceCoverageStatus, !result); + debugLog("hasInsuranceInfo currentDeductible:", currentDeductible, !result); + } + debugLog("", null, !result); + } + }); + + return result; +} + +export function hasSchedulingInfo(order, logQueue = null) { + const schedule = order.schedule; + const result = !!( + schedule.date && + schedule.startTime && + schedule.endTime && + schedule.jobMaxMinutes && + schedule.jobMinMinutes + ); + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasSchedulingInfo:", result, !result); + debugLog("schedule.date:", schedule.date, !result); + debugLog("schedule.startTime:", schedule.startTime, !result); + debugLog("schedule.endTime:", schedule.endTime, !result); + debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !result); + debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !result); + debugLog("", null, !result); + } + }); + + return result; +} + +export function hasCustomerInfo(order, logQueue = null) { + const customer = order.customer; + const result = !!( + customer.firstName && + customer.lastName && + customer.phoneNumber && + customer.emailAddress + ); + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasCustomerInfo:", result, !result); + debugLog("customer.firstName:", customer.firstName, !result); + debugLog("customer.lastName:", customer.lastName, !result); + debugLog("customer.phoneNumber:", customer.phoneNumber, !result); + debugLog("customer.emailAddress:", customer.emailAddress, !result); + debugLog("", null, !result); + } + }); + + return result; +} + +export function hasGlassPartsOrRepairInfo(order, logQueue = null) { + const isRepair = order.damage?.isRepair; + const glassParts = order.lineItems?.glassParts; + const hasGlassParts = glassParts != null && glassParts.length > 0; + const result = !!isRepair || hasGlassParts; + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasGlassPartsOrRepairInfo:", result, !result); + debugLog("damage.isRepair:", isRepair, !result); + debugLog("lineItems.glassParts:", glassParts, !result); + debugLog("", null, !result); + } + }); + + return result; +} + +export function hasPaymentMethodInfo(order, logQueue = null) { + const payment = order.payment; + const result = payment.isPia !== null && (payment.isPia || !!payment.piaType); + + queueLogging(logQueue, () => { + // prettier-ignore + { + debugLog("hasPaymentMethodInfo:", result, !result); + debugLog("payment.isPia:", payment?.isPia, !result); + debugLog("payment.piaType:", payment?.piaType, !result); + debugLog("", null, !result); + } + }); + + return result; +} diff --git a/src/layouts/mobile-details/mobile-details.vue b/src/layouts/mobile-details/mobile-details.vue index b6978a7ff..609210fc2 100644 --- a/src/layouts/mobile-details/mobile-details.vue +++ b/src/layouts/mobile-details/mobile-details.vue @@ -78,6 +78,11 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import textLink from "@/ux-components/text-link/text-link"; import analyticsMixin from "@/mixins/analytics-mixin"; +import { + logPagePrereqsStart, + logPagePrereqsEnd, + hasSchedulingInfo, +} from "@/helpers/page-prerequisites-helper"; export default { name: "MobileDetails", data() { @@ -110,6 +115,9 @@ export default { }, methods: { arePagePrerequisitesValid() { + const logQueue = []; + const order = store.getters.order; + const serviceLocation = store.getters.order.serviceLocation; const serviceLocationPreReqs = serviceLocation.zipCode && @@ -118,16 +126,14 @@ export default { serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; // Schedule - const schedule = store.getters.order.schedule; - const scheduleReqs = !!( - schedule.date && - schedule.startTime && - schedule.endTime && - schedule.jobMaxMinutes && - schedule.jobMinMinutes - ); + const scheduleInfoPreReqs = hasSchedulingInfo(order, logQueue); + + const preReqResult = serviceLocationPreReqs && scheduleInfoPreReqs; + + logPagePrereqsStart("mobile-details.vue", preReqResult); + logQueue.forEach((fn) => fn()); + logPagePrereqsEnd("mobile-details.vue", preReqResult); - const preReqResult = serviceLocationPreReqs && scheduleReqs; return preReqResult; }, getServiceAddressFromStore() { From e138dfe508d84ac7e93cd7b7031c9db3d5575969 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 11:29:18 -0400 Subject: [PATCH 05/16] Refactor new helper to call common flushPagePrereqsLogs --- src/helpers/page-prerequisites-helper.js | 6 +++++ src/helpers/page-prerequisites-helper.spec.js | 24 +++++++++++++++++++ src/layouts/mobile-details/mobile-details.vue | 7 ++---- src/layouts/payment-adyen/payment-adyen.vue | 7 ++---- src/layouts/payment-method/payment-method.vue | 7 ++---- src/layouts/payment/payment.vue | 7 ++---- src/layouts/quote/quote.vue | 7 ++---- 7 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/helpers/page-prerequisites-helper.js b/src/helpers/page-prerequisites-helper.js index c6ad21ff2..0c0f1226a 100644 --- a/src/helpers/page-prerequisites-helper.js +++ b/src/helpers/page-prerequisites-helper.js @@ -17,6 +17,12 @@ export function logPagePrereqsEnd(source, preReqResult) { } } +export function flushPagePrereqsLogs(source, preReqResult, logQueue) { + logPagePrereqsStart(source, preReqResult); + logQueue.forEach((fn) => fn()); + logPagePrereqsEnd(source, preReqResult); +} + function queueLogging(logQueue, logFn) { if (logQueue) { logQueue.push(logFn); diff --git a/src/helpers/page-prerequisites-helper.spec.js b/src/helpers/page-prerequisites-helper.spec.js index 9a85c2c75..b4c4e88e3 100644 --- a/src/helpers/page-prerequisites-helper.spec.js +++ b/src/helpers/page-prerequisites-helper.spec.js @@ -36,6 +36,30 @@ describe("page-prerequisites-helper.js", () => { }); }); + describe("flushPagePrereqsLogs", () => { + test("calls logPagePrereqsStart, executes logQueue, and logPagePrereqsEnd", () => { + const logQueue = [ + jest.fn(), + jest.fn(), + ]; + + pagePrereqsHelper.flushPagePrereqsLogs("payment.vue", true, logQueue); + + expect(debugLog).toHaveBeenCalledWith( + "--- payment.vue pagePrereqs start ---", + null, + false + ); + expect(debugLog).toHaveBeenCalledWith( + "--- payment.vue pagePrereqs end ---", + null, + false + ); + expect(logQueue[0]).toHaveBeenCalled(); + expect(logQueue[1]).toHaveBeenCalled(); + }); + }); + describe("logPagePrereqsEnd", () => { test("calls debugLog with source and forceLog when preReqResult is false", () => { pagePrereqsHelper.logPagePrereqsEnd("payment-method.vue", false); diff --git a/src/layouts/mobile-details/mobile-details.vue b/src/layouts/mobile-details/mobile-details.vue index 609210fc2..08cad6c56 100644 --- a/src/layouts/mobile-details/mobile-details.vue +++ b/src/layouts/mobile-details/mobile-details.vue @@ -79,8 +79,7 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import textLink from "@/ux-components/text-link/text-link"; import analyticsMixin from "@/mixins/analytics-mixin"; import { - logPagePrereqsStart, - logPagePrereqsEnd, + flushPagePrereqsLogs, hasSchedulingInfo, } from "@/helpers/page-prerequisites-helper"; export default { @@ -130,9 +129,7 @@ export default { const preReqResult = serviceLocationPreReqs && scheduleInfoPreReqs; - logPagePrereqsStart("mobile-details.vue", preReqResult); - logQueue.forEach((fn) => fn()); - logPagePrereqsEnd("mobile-details.vue", preReqResult); + flushPagePrereqsLogs("mobile-details.vue", preReqResult, logQueue); return preReqResult; }, diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 8abd3ae95..4193c024b 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -84,8 +84,7 @@ import { coverageStatus } from "@/constants/insurance"; import { deepClone } from "@/helpers/object-helper"; import store from "@/store"; import { - logPagePrereqsStart, - logPagePrereqsEnd, + flushPagePrereqsLogs, hasServiceLocationInfo, hasInsuranceInfo, hasSchedulingInfo, @@ -152,9 +151,7 @@ export default { hasSchedulingInfo(order, logQueue) && hasCustomerInfo(order, logQueue) && hasPaymentMethodInfo(order, logQueue); - logPagePrereqsStart("payment-adyen.vue", result); - logQueue.forEach((fn) => fn()); - logPagePrereqsEnd("payment-adyen.vue", result); + flushPagePrereqsLogs("payment-adyen.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index a9a507690..e5eaab5d0 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -179,8 +179,7 @@ import { getBoolFromString } from "@/helpers/boolean-helper"; import { getAmountDue, addPricesToLineItems } from "@/helpers/pricing-helper.js"; import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash"; import { - logPagePrereqsStart, - logPagePrereqsEnd, + flushPagePrereqsLogs, hasServiceLocationInfo, hasInsuranceInfo, hasSchedulingInfo, @@ -395,9 +394,7 @@ export default { hasInsuranceInfo(order, logQueue) && hasSchedulingInfo(order, logQueue) && hasCustomerInfo(order, logQueue); - logPagePrereqsStart("payment-method.vue", result); - logQueue.forEach((fn) => fn()); - logPagePrereqsEnd("payment-method.vue", result); + flushPagePrereqsLogs("payment-method.vue", result, logQueue); return result; }, getPaymentMethodFromStore() { diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 894c0e497..4b4f0c3a3 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -243,8 +243,7 @@ import { coverageStatus } from "@/constants/insurance"; import { getDisplayAmountDue, getAmountDue } from "@/helpers/pricing-helper.js"; import { debugLog } from "@/helpers/debug-log-helper.js"; import { - logPagePrereqsStart, - logPagePrereqsEnd, + flushPagePrereqsLogs, hasServiceLocationInfo, hasInsuranceInfo, hasSchedulingInfo, @@ -451,9 +450,7 @@ export default { hasSchedulingInfo(order, logQueue) && hasCustomerInfo(order, logQueue) && hasPaymentMethodInfo(order, logQueue); - logPagePrereqsStart("payment.vue", result); - logQueue.forEach((fn) => fn()); - logPagePrereqsEnd("payment.vue", result); + flushPagePrereqsLogs("payment.vue", result, logQueue); return result; }, getAnswersNullSafe(widgetName) { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index b36e4ddc3..df1a4ab7d 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -184,8 +184,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas import { storeMutations } from "@/constants/store-mutations"; import { debugLog } from "@/helpers/debug-log-helper"; import { - logPagePrereqsStart, - logPagePrereqsEnd, + flushPagePrereqsLogs, hasServiceZipInfo, hasGlassPartsOrRepairInfo, } from "@/helpers/page-prerequisites-helper.js"; @@ -753,9 +752,7 @@ export default { ); }); - logPagePrereqsStart("quote.vue", preReqResult); - logQueue.forEach((fn) => fn()); - logPagePrereqsEnd("quote.vue", preReqResult); + flushPagePrereqsLogs("quote.vue", preReqResult, logQueue); return preReqResult; }, vapsItemsSelectedAction(vapsItemsSelected) { From 8d4d14f4c25a223ef76f6e7ed152c79d051df179 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Mon, 9 Mar 2026 11:53:51 -0400 Subject: [PATCH 06/16] CASH-2378 fix for multi location inshop appts CASH-2451 --- .../multi-location-modal.vue | 1 + src/layouts/schedule/schedule.vue | 301 +++++++++++++----- 2 files changed, 218 insertions(+), 84 deletions(-) diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index effa3c91b..68f8d4f7c 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -103,6 +103,7 @@ export default { timeSlot: this.selectedAppointment.timeSlot, routeCode: this.selectedAppointment.timeSlot.id, date: this.selectedAppointment.date, + provider: this.selectedAppointment.provider, }; this.confirmedAppointment = true; this.$emit("confirm-appointment", { diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c5a5f9ac6..77f79f197 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -252,6 +252,8 @@ import { sumDateString, isDropOffRouteCode, getTodayDate, + getTodayDateString, + getInitialViewWeeks, } from "@/layouts/schedule/helpers/schedule-helper"; import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper"; @@ -809,13 +811,6 @@ export default { }; }, selectedShopAnswer() { - const toTitleCase = (str) => { - if (!str) return ""; - return str.replace(/\w\S*/g, function (txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - }); - }; - if ( this.selectedProvider && this.selectedProvider.address && @@ -824,18 +819,11 @@ export default { const provider = this.shopProviderData?.shopProviders?.find( (p) => p.providerNumber === this.selectedProvider?.providerNumber ); - const streetAddress = toTitleCase(this.selectedProvider.address.streetAddress); - const city = toTitleCase(this.selectedProvider.address.city); - const state = this.selectedProvider.address.state; - const zipCode = this.selectedProvider.address.zipCode; - const distanceInMiles = provider ? Math.round(provider.distanceInMiles * 2) / 2 : 0; + const distanceInMiles = provider?.distanceInMiles + ? Math.round(provider.distanceInMiles * 2) / 2 + : 0; - return { - city: `${city}`, - distance: `${distanceInMiles} mi`, - address1: `${streetAddress}`, - address2: `${city}, ${state} ${zipCode}`, - }; + return this.getFormattedShopAddress(provider?.address, distanceInMiles); } return []; }, @@ -977,6 +965,17 @@ export default { "true" ); }, + initialViewStartDate() { + return getTodayDateString(); + }, + initialViewEndDate() { + const initialViewWeeks = getInitialViewWeeks( + this.initialViewStartDate, + NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW, + this.preSelectedDate + ); + return initialViewWeeks[initialViewWeeks.length - 1].weekEndDate; + }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -2054,7 +2053,14 @@ export default { } }, async showMultiLocationModal() { - if (!this.selectedRouteCodeData?.routeCode) { + const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( + experimentSettings.SHOW_MULTI_LOCATION_APPT + ); + + if ( + !this.selectedRouteCodeData?.routeCode && + showMultiLocationAppointment?.toLowerCase() === "true" + ) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION ); @@ -2064,9 +2070,6 @@ export default { let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION ); - let showMultiLocationAppointment = experimentMixin.methods.getSettingValue( - experimentSettings.SHOW_MULTI_LOCATION_APPT - ); let experimentSettingsString = "ShowPm:" + maxDayRangeToShowPmTimeslot + @@ -2089,25 +2092,23 @@ export default { }; // Fetch all appointments - const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAMAppt, firstInshopPMAppt] = - await Promise.all([ - this.getFirstAvailableMobileApptByTOD("AM"), - this.getFirstAvailableMobileApptByTOD("PM"), - this.getFirstAvailableInshopApptByTOD("AM"), - this.getFirstAvailableInshopApptByTOD("PM"), - ]); + const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAppts] = await Promise.all([ + this.getFirstAvailableMobileApptByTOD("AM"), + this.getFirstAvailableMobileApptByTOD("PM"), + this.getFirstAvailableInshopApptsByTOD(["AM", "PM"]), + ]); // Extract dates const firstMobileAMDate = firstMobileAMAppt?.date; const firstMobilePMDate = firstMobilePMAppt?.date; - const firstInshopAMDate = firstInshopAMAppt?.date; - const firstInshopPMDate = firstInshopPMAppt?.date; + const firstInshopAMDate = firstInshopAppts[0]?.date; + const firstInshopPMDate = firstInshopAppts[1]?.date; // Calculate days until appointments const numberOfDaysToFirstMobileAMDate = calculateDaysUntilDate(firstMobileAMAppt); const numberOfDaysToFirstMobilePMDate = calculateDaysUntilDate(firstMobilePMAppt); - const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAMAppt); - const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopPMAppt); + const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAppts[0]); + const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopAppts[1]); const shouldExposeMultiLocationModal = () => { const isMobileAppointmentInDateRange = @@ -2121,10 +2122,7 @@ export default { (firstInshopPMDate && numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot); - if (showMultiLocationAppointment?.toLowerCase() === "true") { - return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; - } - return false; + return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; }; if (shouldExposeMultiLocationModal()) { @@ -2158,8 +2156,8 @@ export default { // Selects the first available inshop time slot promotedInshopAppointment = numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate - ? firstInshopAMAppt - : firstInshopPMAppt; + ? firstInshopAppts[0] + : firstInshopAppts[1]; } else { if ( firstMobilePMDate && @@ -2182,15 +2180,15 @@ export default { numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowPmTimeslot ) { // Selects the first available PM time slot - promotedInshopAppointment = firstInshopPMAppt; + promotedInshopAppointment = firstInshopAppts[1]; } else if ( firstInshopPMDate && numberOfDaysToFirstInshopPMDate >= maxDayRangeToShowNoPmTimeslot ) { // Selects the first available AM time slot. If none, selects the first available PM time slot - promotedInshopAppointment = firstInshopAMAppt - ? firstInshopAMAppt - : firstInshopPMAppt; + promotedInshopAppointment = firstInshopAppts[0] + ? firstInshopAppts[0] + : firstInshopAppts[1]; } } @@ -2210,6 +2208,7 @@ export default { } } }, + async getFirstAvailableMobileApptByTOD(timeOfDay) { const selectableDays = this.selectableDatesMobile?.days; if (!selectableDays?.length) { @@ -2237,42 +2236,135 @@ export default { return null; } }, - async getFirstAvailableInshopApptByTOD(timeOfDay) { - const selectableDays = this.selectableDatesInshop?.days; - if (!selectableDays?.length) { - return null; - } else { - for (const dateObj of selectableDays) { - let matchingTimeSlot = { - estimatedServiceMinutes: { - minimum: this.estimatedServiceMinutesMinimum, - maximum: this.estimatedServiceMinutesMaximum, - }, - timeSlot: null, - date: null, - addressCopy: null, - }; - const isMatchingApptDay = dateObj.timeSlots.some( - (slot) => - slot.id.includes(timeOfDay) && - (matchingTimeSlot.timeSlot = slot) && - (matchingTimeSlot.date = dateObj.date) - ); - if ( - this.selectedShopAnswer?.address1 && - this.selectedShopAnswer?.address2 - ) { - matchingTimeSlot.addressCopy = - this.selectedShopAnswer?.address1 + - ", " + - this.selectedShopAnswer?.address2; - } - if (isMatchingApptDay && matchingTimeSlot) { - return matchingTimeSlot; - } + + findMatchingInshopAppt(selectableDays, timeOfDay, provider) { + if (!selectableDays?.length) return null; + for (const dateObj of selectableDays) { + const distanceInMiles = provider?.distanceInMiles + ? Math.round(provider.distanceInMiles * 2) / 2 + : 0; + const providerAddress = this.getFormattedShopAddress( + provider?.address, + distanceInMiles + ); + let matchingTimeSlot = { + estimatedServiceMinutes: { + minimum: this.estimatedServiceMinutesMinimum, + maximum: this.estimatedServiceMinutesMaximum, + }, + timeSlot: null, + date: null, + addressCopy: providerAddress.address1 + ", " + providerAddress.address2, + provider: provider, + }; + const isMatchingApptDay = dateObj.timeSlots.some( + (slot) => + slot.id.includes(timeOfDay) && + (matchingTimeSlot.timeSlot = slot) && + (matchingTimeSlot.date = dateObj.date) + ); + if (isMatchingApptDay && matchingTimeSlot) { + return matchingTimeSlot; } - return null; } + return null; + }, + + async getFirstAvailableInshopApptsByTOD(timeOfDayArray) { + let matchingApptNearestAM1 = this.findMatchingInshopAppt( + this.selectableDatesInshop?.days, + timeOfDayArray[0], + this.selectedProvider + ); + let matchingApptNearestPM1 = this.findMatchingInshopAppt( + this.selectableDatesInshop?.days, + timeOfDayArray[1], + this.selectedProvider + ); + let matchingApptNearestAM2; + let matchingApptNearestPM2; + let matchingApptNearestAM3; + let matchingApptNearestPM3; + + const providerNearest2 = this.shopProviderData.shopProviders[1]; + const providerNearest3 = this.shopProviderData.shopProviders[2]; + + if (providerNearest2?.distanceInMiles && providerNearest2.distanceInMiles < 25) { + this.calendarLoadingStatus = "more"; + const selectableDaysFromProviderNearest2 = await getScheduleApiResponse({ + startDateString: this.initialViewStartDate, + endDateString: this.initialViewEndDate, + inshopProviderNumber: providerNearest2.providerNumber, + zipCode: this.zipCode, + includeMobileTimeSlots: false, + includeInshopTimeSlots: true, + }); + matchingApptNearestAM2 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest2?.inshopTimeSlotsData?.days, + "AM", + providerNearest2 + ); + matchingApptNearestPM2 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest2?.inshopTimeSlotsData?.days, + "PM", + providerNearest2 + ); + } + if (providerNearest3?.distanceInMiles && providerNearest3?.distanceInMiles < 25) { + this.calendarLoadingStatus = "more"; + const selectableDaysFromProviderNearest3 = await getScheduleApiResponse({ + startDateString: this.initialViewStartDate, + endDateString: this.initialViewEndDate, + inshopProviderNumber: providerNearest3.providerNumber, + zipCode: this.zipCode, + includeMobileTimeSlots: false, + includeInshopTimeSlots: true, + }); + matchingApptNearestAM3 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest3?.inshopTimeSlotsData?.days, + "AM", + providerNearest3 + ); + matchingApptNearestPM3 = this.findMatchingInshopAppt( + selectableDaysFromProviderNearest3?.inshopTimeSlotsData?.days, + "PM", + providerNearest3 + ); + } + this.calendarLoadingStatus = "none"; + + // WHICH OF THE 3 INSHOP APPTS IS THE EARLIEST? + const compareAppointments = (appt1, appt2) => { + // Handle null/undefined appointments + if (!appt1?.date) return 1; // appt1 is later (or invalid) + if (!appt2?.date) return -1; // appt2 is later (or invalid) + + // Compare dates first (YYYY-MM-DD format allows string comparison) + if (appt1.date < appt2.date) return -1; // appt1 is earlier + if (appt1.date > appt2.date) return 1; // appt2 is earlier + + // Dates are equal, compare times (HH:MM format allows string comparison) + const time1 = appt1.timeSlot?.startTime || "23:59"; + const time2 = appt2.timeSlot?.startTime || "23:59"; + + if (time1 < time2) return -1; // appt1 is earlier + if (time1 > time2) return 1; // appt2 is earlier + + return 0; // Completely equal + }; + // Find earliest AM appointment + const preferredApptAM = + [matchingApptNearestAM1, matchingApptNearestAM2, matchingApptNearestAM3] + .filter((appt) => appt?.date) // Remove null/undefined + .sort(compareAppointments)[0] || null; + + // Find earliest PM appointment + const preferredApptPM = + [matchingApptNearestPM1, matchingApptNearestPM2, matchingApptNearestPM3] + .filter((appt) => appt?.date) + .sort(compareAppointments)[0] || null; + + return [preferredApptAM, preferredApptPM]; }, updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) { this.selectedMobileFirstAppointment = true; @@ -2283,21 +2375,62 @@ export default { this.forwardButtonAction(); }, updateMultiLocationTimeSlotandNavigateForward({ selectedTimeSlot, appointmentType }) { - if (appointmentType?.toLowerCase() == "mobile") { - this.appointmentType = AppointmentTypeStrings.MOBILE; - } else if (appointmentType?.toLowerCase() == "inshop") { - this.appointmentType = AppointmentTypeStrings.IN_SHOP; - } + if (!selectedTimeSlot) return; this.selectedMultiLocationAppointment = true; this.selectedDate = selectedTimeSlot.date; - this.updateSelectedProvider(); - this.updateTimeSlot(selectedTimeSlot); + if (appointmentType?.toLowerCase() == "mobile") { + this.appointmentType = AppointmentTypeStrings.MOBILE; + this.updateSelectedProvider(); + this.updateTimeSlot(selectedTimeSlot); + } else if (appointmentType?.toLowerCase() == "inshop") { + this.appointmentType = AppointmentTypeStrings.IN_SHOP; + + this.updateSelectedProvider(selectedTimeSlot.provider); + + // UPDATE SELECTED TIME SLOT INFO + this.selectedTimeSlotInfo = { + timeSlot: { + date: selectedTimeSlot.date, + routeCode: selectedTimeSlot.routeCode, + startTime: selectedTimeSlot.timeSlot.startTime, + endTime: selectedTimeSlot.timeSlot.endTime, + jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(), + jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(), + }, + isPremiumAppointment: selectedTimeSlot.isPremiumAppointment ? true : false, + }; + + // UPDATE APPT TYPE + this.appointmentType = this.getInShopOrDropOffApptType(selectedTimeSlot.routeCode); + } this.forwardButtonAction(); }, updateRecalAcknowledgedAndNavigateForward(isAcknowledged) { this.isRecalAcknowledgedForScheduling = isAcknowledged; this.forwardButtonAction(); }, + getFormattedShopAddress(providerAddress, distanceInMiles) { + const toTitleCase = (str) => { + if (!str) return ""; + return str.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); + }; + + if (!providerAddress) return {}; + + const streetAddress = toTitleCase(providerAddress.streetAddress); + const city = toTitleCase(providerAddress.city); + const state = providerAddress.state; + const zipCode = providerAddress.zipCode; + + return { + city: city, + distance: `${distanceInMiles} mi`, + address1: streetAddress, + address2: `${city}, ${state} ${zipCode}`, + }; + }, }, watch: { appointmentTypeFromAppointmentTypeQuestion: { From 3b950302ff9d6d3464dc2e968701a81d89088d73 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 12:40:19 -0400 Subject: [PATCH 07/16] Additional schedule page prereqs refactor --- src/helpers/page-prerequisites-helper.js | 36 +++-- src/helpers/page-prerequisites-helper.spec.js | 132 ++++++++++++------ src/layouts/schedule/schedule.vue | 87 ++++-------- 3 files changed, 143 insertions(+), 112 deletions(-) diff --git a/src/helpers/page-prerequisites-helper.js b/src/helpers/page-prerequisites-helper.js index 0c0f1226a..3f7d6583f 100644 --- a/src/helpers/page-prerequisites-helper.js +++ b/src/helpers/page-prerequisites-helper.js @@ -1,7 +1,7 @@ import { debugLog } from "@/helpers/debug-log-helper.js"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { coverageStatus } from "@/constants/insurance"; -import store from "@/store"; +import { applicationConfig } from "@/constants/application-config"; export function logPagePrereqsStart(source, preReqResult) { // prettier-ignore @@ -87,19 +87,31 @@ export function hasServiceLocationInfo(order, logQueue = null) { } export function hasInsuranceInfo(order, logQueue = null) { - const isInsurance = order.payment.isInsurance; - const insuranceCoverageStatus = store.getters.payment?.insuranceCoverage?.coverageStatus; - const currentDeductible = store.getters.policy?.currentDeductible; + const isInsurance = order.payment?.isInsurance; + const insuranceCoverageStatus = order.payment?.insuranceCoverage?.coverageStatus; + const currentDeductible = order.policy?.currentDeductible; + const parentAccountNumber = order.payment?.parentAccountNumber; + const policyNumber = order.policy?.policyNumber; let result = false; if (isInsurance !== null) { - if ( - insuranceCoverageStatus === coverageStatus.VERIFIED && - !(currentDeductible === 0 || currentDeductible > 0) - ) { - result = false; // if coverageStatus is verified there must be a valid deductible also + if (isInsurance === false) { + result = true; // Cash selected - no need for policy number or parent account } else { - result = true; + const parentAccountNotCash = + parentAccountNumber !== applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; + const hasPolicyNumber = !!policyNumber; + + if (!parentAccountNotCash || !hasPolicyNumber) { + result = false; + } else if ( + insuranceCoverageStatus === coverageStatus.VERIFIED && + !(currentDeductible === 0 || currentDeductible > 0) + ) { + result = false; // if coverageStatus is verified there must be a valid deductible also + } else { + result = true; + } } } @@ -107,6 +119,10 @@ export function hasInsuranceInfo(order, logQueue = null) { // prettier-ignore { debugLog("hasInsuranceInfo:", result, !result); + if (isInsurance === true) { + debugLog("hasInsuranceInfo parentAccountNumber:", parentAccountNumber, !result); + debugLog("hasInsuranceInfo policyNumber:", policyNumber, !result); + } if (insuranceCoverageStatus === coverageStatus.VERIFIED) { debugLog("hasInsuranceInfo coverageStatus:", insuranceCoverageStatus, !result); debugLog("hasInsuranceInfo currentDeductible:", currentDeductible, !result); diff --git a/src/helpers/page-prerequisites-helper.spec.js b/src/helpers/page-prerequisites-helper.spec.js index b4c4e88e3..9aabba829 100644 --- a/src/helpers/page-prerequisites-helper.spec.js +++ b/src/helpers/page-prerequisites-helper.spec.js @@ -1,5 +1,4 @@ import * as pagePrereqsHelper from "@/helpers/page-prerequisites-helper"; -import store from "@/store"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { coverageStatus } from "@/constants/insurance"; @@ -293,25 +292,6 @@ describe("page-prerequisites-helper.js", () => { }); describe("hasInsuranceInfo", () => { - const baseOrder = { - payment: { - isInsurance: false, - }, - }; - - beforeEach(() => { - store.getters = { - payment: { - insuranceCoverage: { - coverageStatus: null, - }, - }, - policy: { - currentDeductible: 100, - }, - }; - }); - test("returns false when isInsurance is null", () => { const order = { payment: { isInsurance: null } }; @@ -320,19 +300,43 @@ describe("page-prerequisites-helper.js", () => { expect(result).toBe(false); }); - test("returns true when isInsurance is false and coverage is not verified", () => { - // Arrange - store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.PENDING; + test("returns true when isInsurance is false (cash selected)", () => { + const order = { + payment: { + isInsurance: false, + insuranceCoverage: { coverageStatus: coverageStatus.PENDING }, + }, + }; - const result = pagePrereqsHelper.hasInsuranceInfo(baseOrder); + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(true); + }); + + test("returns true when isInsurance is true and coverage is not verified", () => { + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.PENDING }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123" }, + }; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); expect(result).toBe(true); }); test("returns true when isInsurance is true and coverage is verified with valid deductible", () => { - const order = { payment: { isInsurance: true } }; - store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; - store.getters.policy.currentDeductible = 100; + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", currentDeductible: 100 }, + }; const result = pagePrereqsHelper.hasInsuranceInfo(order); @@ -340,19 +344,59 @@ describe("page-prerequisites-helper.js", () => { }); test("returns true when coverage is verified and deductible is 0", () => { - const order = { payment: { isInsurance: true } }; - store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; - store.getters.policy.currentDeductible = 0; + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", currentDeductible: 0 }, + }; const result = pagePrereqsHelper.hasInsuranceInfo(order); expect(result).toBe(true); }); + test("returns false when isInsurance is true but parentAccountNumber is CASH_PARENT_ACCOUNT_NUMBER", () => { + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.PENDING }, + parentAccountNumber: 167132, + }, + policy: { policyNumber: "POL123" }, + }; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(false); + }); + + test("returns false when isInsurance is true but policyNumber is missing", () => { + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.PENDING }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: null }, + }; + + const result = pagePrereqsHelper.hasInsuranceInfo(order); + + expect(result).toBe(false); + }); + test("returns false when coverage is verified but deductible is null", () => { - const order = { payment: { isInsurance: true } }; - store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; - store.getters.policy.currentDeductible = null; + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", currentDeductible: null }, + }; const result = pagePrereqsHelper.hasInsuranceInfo(order); @@ -360,9 +404,14 @@ describe("page-prerequisites-helper.js", () => { }); test("returns false when coverage is verified but deductible is undefined", () => { - const order = { payment: { isInsurance: true } }; - store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; - store.getters.policy.currentDeductible = undefined; + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", currentDeductible: undefined }, + }; const result = pagePrereqsHelper.hasInsuranceInfo(order); @@ -370,9 +419,14 @@ describe("page-prerequisites-helper.js", () => { }); test("returns false when coverage is verified but deductible is negative", () => { - const order = { payment: { isInsurance: true } }; - store.getters.payment.insuranceCoverage.coverageStatus = coverageStatus.VERIFIED; - store.getters.policy.currentDeductible = -1; + const order = { + payment: { + isInsurance: true, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", currentDeductible: -1 }, + }; const result = pagePrereqsHelper.hasInsuranceInfo(order); diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 89ed21a08..ae593c735 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -261,6 +261,12 @@ import { getAmountDue, getPricingByDayPartWithPrice } from "@/helpers/pricing-he import { getItemsWithoutRecalParts } from "@/helpers/recal-helper"; import { deepClone } from "@/helpers/object-helper"; import { debugLog } from "@/helpers/debug-log-helper"; +import { + flushPagePrereqsLogs, + hasServiceZipInfo, + hasGlassPartsOrRepairInfo, + hasInsuranceInfo, +} from "@/helpers/page-prerequisites-helper.js"; import { getSessionKeyValue, getUserIdValue, @@ -981,76 +987,31 @@ export default { methods: { splitCopyOnCMSPlaceHolder, arePagePrerequisitesValid() { - const paymentInfo = store.getters.payment.isInsurance !== null; - const damageInfo = - store.getters.order.damage.isRepair || - (store.getters.order.lineItems?.glassParts != null && - store.getters.order.lineItems.glassParts.length > 0); - const parentAccountNumberInfo = - store.getters.payment.parentAccountNumber !== - applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; - const policyNumberInfo = store.getters.order.policy.policyNumber; - const zipCodeInfo = store.getters.order.serviceLocation.zipCode; - const supportingItemsInfo = store.getters.lineItems.supportingItems; + const order = store.getters.order; + const logQueue = []; + + const serviceZip = hasServiceZipInfo(order, logQueue); + const glassPartsOrRepair = hasGlassPartsOrRepairInfo(order, logQueue); + const insuranceInfo = hasInsuranceInfo(order, logQueue); // insurance drops the recycle fee on replace orders so it won't be in supportingItems - const insurancePreReqResult = - parentAccountNumberInfo && - policyNumberInfo && - zipCodeInfo && - paymentInfo && - damageInfo; - const cashPreReqResult = - supportingItemsInfo && zipCodeInfo && paymentInfo && damageInfo; + const supportingItemsInfo = store.getters.lineItems?.supportingItems; + const supportingItemsPreReqs = order.payment?.isInsurance + ? true + : !!supportingItemsInfo; - const outputDebugLog = (preReqResult) => { - // prettier-ignore - debugLog("--- schedule.vue pagePrereqs start ---", null, !preReqResult); - debugLog( - "store.getters.payment.isInsurance:", - store.getters.payment?.isInsurance, - !preReqResult - ); - debugLog( - "store.getters.order.damage.isRepair:", - store.getters.order.damage?.isRepair, - !preReqResult - ); - debugLog( - "store.getters.order.lineItems.glassParts:", - store.getters.order.lineItems?.glassParts, - !preReqResult - ); - debugLog( - "store.getters.payment.parentAccountNumber:", - store.getters.payment.parentAccountNumber, - !preReqResult - ); - debugLog( - "store.getters.order.policy.policyNumber:", - store.getters.order.policy.policyNumber, - !preReqResult - ); - debugLog( - "store.getters.order.serviceLocation.zipCode:", - store.getters.order.serviceLocation.zipCode, - !preReqResult - ); + const preReqResult = serviceZip && insuranceInfo && glassPartsOrRepair && supportingItemsPreReqs; + + logQueue.push(() => { debugLog( "store.getters.lineItems.supportingItems:", - store.getters.lineItems.supportingItems, - !preReqResult + store.getters.lineItems?.supportingItems, + !supportingItemsPreReqs ); - debugLog("--- schedule.vue pagePrereqs end ---", null, !preReqResult); - }; + }); - if (store.getters.payment.isInsurance) { - outputDebugLog(insurancePreReqResult); - return insurancePreReqResult; - } else { - outputDebugLog(cashPreReqResult); - return cashPreReqResult; - } + flushPagePrereqsLogs("schedule.vue", preReqResult, logQueue); + return preReqResult; }, setDataOnLoad(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) { From 7071a01a8d2f25fefcfa5c5da5b74448f91d1a19 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Mon, 9 Mar 2026 12:58:36 -0400 Subject: [PATCH 08/16] CASH-2378 fix to keep background from appearing loaded until multi location popup has finished it's logic --- src/layouts/schedule/schedule.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 77f79f197..48126d371 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -487,6 +487,7 @@ export default { selectedMobileFirstAppointment: false, selectedMultiLocationAppointment: false, calendarLoadingStatus: "none", + isLoadingMultiLocationPopup: true, }; }, async beforeRouteEnter(to, from, next) { @@ -895,6 +896,10 @@ export default { : false; }, isMultiLocationModalOpen() { + // Only return false if we're done loading the popup + if (this.isLoadingMultiLocationPopup) { + return true; // Still loading, keep background hidden + } return this.selectableDatesMobile.days && this.selectableDatesInshop.days ? this.$refs.multiLocationModal?.getIsModalOpen() : false; @@ -2207,6 +2212,7 @@ export default { } } } + this.isLoadingMultiLocationPopup = false; }, async getFirstAvailableMobileApptByTOD(timeOfDay) { From 7ae16a5610c9438352676d876f9747cadf2388f1 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 13:13:07 -0400 Subject: [PATCH 09/16] Added a few method comments and make prettier happier --- src/helpers/page-prerequisites-helper.js | 60 +++++++++++++++++++ src/helpers/page-prerequisites-helper.spec.js | 5 +- src/layouts/mobile-details/mobile-details.vue | 5 +- src/layouts/schedule/schedule.vue | 3 +- 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/helpers/page-prerequisites-helper.js b/src/helpers/page-prerequisites-helper.js index 3f7d6583f..2be2804ec 100644 --- a/src/helpers/page-prerequisites-helper.js +++ b/src/helpers/page-prerequisites-helper.js @@ -3,6 +3,12 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { coverageStatus } from "@/constants/insurance"; import { applicationConfig } from "@/constants/application-config"; +/** + * Logs the start of the page prerequisites check. + * Should be used on pages before the page prerequisites check is executed. + * @param {string} source - The source of the page prerequisites check. + * @param {boolean} preReqResult - The result of the page prerequisites check. + */ export function logPagePrereqsStart(source, preReqResult) { // prettier-ignore { @@ -10,6 +16,12 @@ export function logPagePrereqsStart(source, preReqResult) { } } +/** + * Logs the end of the page prerequisites check. + * Should be used on pages after the page prerequisites check is executed. + * @param {string} source - The source of the page prerequisites check. + * @param {boolean} preReqResult - The result of the page prerequisites check. + */ export function logPagePrereqsEnd(source, preReqResult) { // prettier-ignore { @@ -17,18 +29,34 @@ export function logPagePrereqsEnd(source, preReqResult) { } } +/** + * Flushes the page prerequisites logs. + * @param {string} source - The source of the page prerequisites check. This is the page name most places. + * @param {boolean} preReqResult - The result of the page prerequisites check. + * @param {Array} logQueue - The queue of log functions. + */ export function flushPagePrereqsLogs(source, preReqResult, logQueue) { logPagePrereqsStart(source, preReqResult); logQueue.forEach((fn) => fn()); logPagePrereqsEnd(source, preReqResult); } +/** + * Pushes a log function to the log queue. + * @param {Array} logQueue - The queue of log functions. + * @param {Function} logFn - The log function to be executed. + */ function queueLogging(logQueue, logFn) { if (logQueue) { logQueue.push(logFn); } } +/** + * Checks whether the service zip code information is valid for the order. + * Should be used on pages after service zip code collection is complete. + * Returns true if the zip code and zip code CTU are present on serviceLocation. + */ export function hasServiceZipInfo(order, logQueue = null) { const serviceLocation = order.serviceLocation; const result = !!(serviceLocation.zipCode && serviceLocation.zipCodeCtu); @@ -46,6 +74,12 @@ export function hasServiceZipInfo(order, logQueue = null) { return result; } +/** + * Checks whether the service location information is valid for the appointment. + * Returns true if the required info is present for the chosen appointment type. + * For MOBILE: address, city, state, and zipCode must be present on serviceLocation. + * For INSHOP/DROPOFF: all provider address fields must be present. + */ export function hasServiceLocationInfo(order, logQueue = null) { const serviceLocation = order.serviceLocation; const mobileReqs = !!( @@ -86,6 +120,10 @@ export function hasServiceLocationInfo(order, logQueue = null) { return result; } +/** + * Checks whether the insurance information is valid for the order. To be used after all insurance info collection is complete. + * Returns true if cash selected or if insurance is selected and some insurance fields are present. + */ export function hasInsuranceInfo(order, logQueue = null) { const isInsurance = order.payment?.isInsurance; const insuranceCoverageStatus = order.payment?.insuranceCoverage?.coverageStatus; @@ -134,6 +172,13 @@ export function hasInsuranceInfo(order, logQueue = null) { return result; } +/** + * Checks whether the scheduling information is valid for the order. + * Should be used on pages after scheduling info collection is complete. + * Returns true if the required info is present for the chosen appointment type. + * For MOBILE: date, startTime, endTime, jobMaxMinutes, and jobMinMinutes must be present on schedule. + * For INSHOP/DROPOFF: all provider scheduling fields must be present. + */ export function hasSchedulingInfo(order, logQueue = null) { const schedule = order.schedule; const result = !!( @@ -160,6 +205,11 @@ export function hasSchedulingInfo(order, logQueue = null) { return result; } +/** + * Checks whether the customer information is valid for the order. + * Should be used on pages after customer info collection is complete. + * Returns true if the firstName, lastName, phoneNumber, and emailAddress are present on customer. + */ export function hasCustomerInfo(order, logQueue = null) { const customer = order.customer; const result = !!( @@ -184,6 +234,11 @@ export function hasCustomerInfo(order, logQueue = null) { return result; } +/** + * Checks whether the glass parts or repair information is valid for the order. + * Should be used on pages after glass parts or repair info collection is complete. + * Returns true if the isRepair is true or if the glassParts array is not empty. + */ export function hasGlassPartsOrRepairInfo(order, logQueue = null) { const isRepair = order.damage?.isRepair; const glassParts = order.lineItems?.glassParts; @@ -203,6 +258,11 @@ export function hasGlassPartsOrRepairInfo(order, logQueue = null) { return result; } +/** + * Checks whether the payment method information is valid for the order. + * Should only be used on pages after payment method info collection is complete which is only the payment collection pages. + * Returns true if the isPia is true or if the piaType is set. + */ export function hasPaymentMethodInfo(order, logQueue = null) { const payment = order.payment; const result = payment.isPia !== null && (payment.isPia || !!payment.piaType); diff --git a/src/helpers/page-prerequisites-helper.spec.js b/src/helpers/page-prerequisites-helper.spec.js index 9aabba829..a619720c2 100644 --- a/src/helpers/page-prerequisites-helper.spec.js +++ b/src/helpers/page-prerequisites-helper.spec.js @@ -37,10 +37,7 @@ describe("page-prerequisites-helper.js", () => { describe("flushPagePrereqsLogs", () => { test("calls logPagePrereqsStart, executes logQueue, and logPagePrereqsEnd", () => { - const logQueue = [ - jest.fn(), - jest.fn(), - ]; + const logQueue = [jest.fn(), jest.fn()]; pagePrereqsHelper.flushPagePrereqsLogs("payment.vue", true, logQueue); diff --git a/src/layouts/mobile-details/mobile-details.vue b/src/layouts/mobile-details/mobile-details.vue index 08cad6c56..cb98386e6 100644 --- a/src/layouts/mobile-details/mobile-details.vue +++ b/src/layouts/mobile-details/mobile-details.vue @@ -78,10 +78,7 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import textLink from "@/ux-components/text-link/text-link"; import analyticsMixin from "@/mixins/analytics-mixin"; -import { - flushPagePrereqsLogs, - hasSchedulingInfo, -} from "@/helpers/page-prerequisites-helper"; +import { flushPagePrereqsLogs, hasSchedulingInfo } from "@/helpers/page-prerequisites-helper"; export default { name: "MobileDetails", data() { diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ae593c735..38681fb24 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1000,7 +1000,8 @@ export default { ? true : !!supportingItemsInfo; - const preReqResult = serviceZip && insuranceInfo && glassPartsOrRepair && supportingItemsPreReqs; + const preReqResult = + serviceZip && insuranceInfo && glassPartsOrRepair && supportingItemsPreReqs; logQueue.push(() => { debugLog( From 37037ac654e94e25bdddbec6bbd2c1037b0713f5 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Mon, 9 Mar 2026 13:29:47 -0400 Subject: [PATCH 10/16] CASH-2454: MultiLocationModal UI/UX updates --- .../schedule/multi-location-modal/multi-location-modal.vue | 1 + .../multi-location-radio/multi-location-radio.vue | 1 + src/layouts/schedule/schedule.vue | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index 68f8d4f7c..3479d0432 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -301,6 +301,7 @@ export default { flex-direction: column; text-align: center; padding: 0 1rem; + margin-top: 4rem; & > span { color: $red; diff --git a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue index 275c6cd2c..06ba006ac 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue @@ -265,6 +265,7 @@ export default { .location-info-label { display: flex; align-items: center; + line-height: 1.625rem; &:before { content: ""; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 48126d371..634f0c8bf 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -618,9 +618,9 @@ export default { pricedMobileFeePart, shopProviderData.data ); - vm.initializeDatePicker().then(() => { + vm.initializeDatePicker().then(async () => { // vm.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST - vm.showMultiLocationModal(); + await vm.showMultiLocationModal(); vm.hideLoadingModal(); vm.calendarLoadingStatus = "none"; if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable(); From 938ebe6c5616b7c31096eb772eaf956a2f9761e0 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Mon, 9 Mar 2026 13:40:06 -0400 Subject: [PATCH 11/16] CASH-623 revert CASH-623 revert --- src/store/index.js | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/store/index.js b/src/store/index.js index e91ec6c0b..10218892c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2014,27 +2014,52 @@ export const actions = { }, getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) { - const flattenedLineItemsWithChildParts = getFlattenedArrayOfLineItemsWithChildParts( - context.getters.order.lineItems.glassParts + const escapeRecalibrationType = (rt) => rt.split("&").join("%26"); + + const partialLineItemsObjects = context.getters.order.lineItems.glassParts?.map( + (lineItem) => ({ + partNumber: lineItem.partNumber, + recalibrationType: + lineItem.recalibrationType && isRecalPartOrHasChildRecalPart(lineItem) + ? escapeRecalibrationType(lineItem.recalibrationType) + : undefined, + }) ); - const lineItemsList = flattenedLineItemsWithChildParts - .map((item) => item.partNumber) - .join(","); + var lineItems = null; + if (partialLineItemsObjects) { + lineItems = buildQueryStringParameterFromArrayOfComplexObjects( + partialLineItemsObjects, + "lineItems" + ); + } const vehicle = context.getters.vehicle; const carId = vehicle.carId; const damage = context.getters.damage; + const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace); + + const glassPieces = buildQueryStringParameterFromArrayOfComplexObjects( + glassArray, + "glassPieces" + ); const parentAccountNumber = context.getters.order.payment.parentAccountNumber ?? applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; + const referralSequenceNumber = context.getters.order.referralSequenceNumber; - var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&parentAccountNumber=${parentAccountNumber}&isRepair=${damage.isRepair}`; - if (lineItemsList) { - endPoint += `&lineItems=${lineItemsList}`; + var endPoint = `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&parentAccountNumber=${parentAccountNumber}&referralSequenceNumber=${referralSequenceNumber}&applicationName=${applicationConfig.ANALYTICS_APPLICATION_NAME}`; + if (lineItems) { + endPoint += `&${lineItems}`; } + if (glassPieces) { + endPoint += `&${glassPieces}`; + } + + endPoint += `&isHeavyTruckVehicle=${context.getters.order.vehicle.isBigTruck}`; + return globalMethods.callHttpClient({ method: endpoints.GetServiceabilityDetails.method, endpoint: endPoint, From 1fd370e4bf5ee904dbe24de2375911a5fcb56dea Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Mon, 9 Mar 2026 15:37:58 -0400 Subject: [PATCH 12/16] CASH-2448: GA event update --- .../schedule/multi-location-modal/multi-location-modal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index 3479d0432..ef49fa2c2 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -153,7 +153,7 @@ export default { this.GaCategories.INSHOP_CONFIRMATION_CLICKED, gaAction, this.inshopAppointmentOption.timeSlot.startTime + - "-" + + " " + this.inshopAppointmentOption.date, true, null, From 07a184735a98b0c35f7522fbb0267853c4188681 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 17:34:03 -0400 Subject: [PATCH 13/16] Adjusted unit tests on all refactored pages Adjusted or added tests for prereqs on the pages I refactored to use the helper. --- .../mobile-details/mobile-details.spec.js | 53 ++++- .../payment-adyen/payment-adyen.spec.js | 201 ++++++++++++++++++ .../payment-method/payment-method.spec.js | 134 +++++++++--- src/layouts/payment/payment.spec.js | 36 ++++ src/layouts/quote/quote.spec.js | 41 ++++ src/layouts/schedule/schedule.spec.js | 93 ++++++++ 6 files changed, 520 insertions(+), 38 deletions(-) create mode 100644 src/layouts/payment-adyen/payment-adyen.spec.js diff --git a/src/layouts/mobile-details/mobile-details.spec.js b/src/layouts/mobile-details/mobile-details.spec.js index a88dcab3d..a0c548a54 100644 --- a/src/layouts/mobile-details/mobile-details.spec.js +++ b/src/layouts/mobile-details/mobile-details.spec.js @@ -89,18 +89,53 @@ describe("mobile-details.vue", () => { // Assert expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled(); }); - test("arePagePrerequisitesValid should be true ", async () => { - //Arrange - const { wrapper } = setupMocks({ - mixins: [mockMixin], - attachTo: document.body, + describe("arePagePrerequisitesValid", () => { + test("returns true when all prerequisites are valid", () => { + const { wrapper } = setupMocks({ + mixins: [mockMixin], + attachTo: document.body, + }); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(true); }); - //Act - let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + test("returns falsy when service zip info is missing", () => { + store.getters.order.serviceLocation.zipCode = null; + const { wrapper } = setupMocks({ + mixins: [mockMixin], + attachTo: document.body, + }); - //Assert - expect(arePagePrerequisitesValid).toBe(true); + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBeFalsy(); + }); + + test("returns false when appointment type is not mobile", () => { + store.getters.order.serviceLocation.appointmentType = "Inshop"; + const { wrapper } = setupMocks({ + mixins: [mockMixin], + attachTo: document.body, + }); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when scheduling info is missing", () => { + store.getters.order.schedule.date = null; + const { wrapper } = setupMocks({ + mixins: [mockMixin], + attachTo: document.body, + }); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); }); test("if the back button is clicked, navigate back", async () => { diff --git a/src/layouts/payment-adyen/payment-adyen.spec.js b/src/layouts/payment-adyen/payment-adyen.spec.js new file mode 100644 index 000000000..e13b468c7 --- /dev/null +++ b/src/layouts/payment-adyen/payment-adyen.spec.js @@ -0,0 +1,201 @@ +import paymentAdyen from "@/layouts/payment-adyen/payment-adyen.vue"; +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper"; +import store from "@/store"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; + +jest.mock("@/helpers/pricing-helper.js", () => ({ + getAmountDue: jest.fn().mockReturnValue(100), +})); + +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: () => Promise.resolve("content"), +})); + +jest.mock("@/helpers/layout-helper", () => ({ + settleAllPromises: jest.fn().mockResolvedValue({ cmsContent: "content" }), +})); + +jest.mock("@/helpers/loading-modal-helper", () => ({ + showFmgLoadingModal: jest.fn(), +})); + +jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({ + submitWorkOrder: jest.fn(), +})); + +jest.mock("@/helpers/debug-log-helper.js", () => ({ + debugLog: jest.fn(), +})); + +// Component that skips Adyen initialization in mounted (only needed for arePagePrerequisitesValid tests) +const PaymentAdyenTestComponent = { + ...paymentAdyen, + mounted() { + // Stub - skip initializeAdyen to avoid Adyen/API setup + }, +}; + +const createValidOrder = () => ({ + vehicle: { + year: "2020", + make: "acura", + model: "mdx", + carId: "dummyCarId", + }, + serviceLocation: { + address: "123 Main St", + address2: "", + city: "Columbus", + state: "OH", + zipCode: "43235", + zipCodeCtu: "43235", + appointmentType: AppointmentTypeStrings.IN_SHOP, + provider: { + address: { + streetAddress: "456 Provider St", + city: "Columbus", + state: "OH", + zipCode: "43235", + zipCodeCtu: "43235", + }, + }, + }, + customer: { + firstName: "John", + lastName: "Doe", + emailAddress: "john.doe@example.com", + phoneNumber: "555-555-5555", + }, + payment: { + isInsurance: false, + isPia: true, + piaType: "CREDIT_CARD", + insuranceCoverage: {}, + }, + policy: { + currentDeductible: 0, + policyNumber: null, + }, + schedule: { + date: "2024-01-15", + startTime: "09:00", + endTime: "10:00", + jobMinMinutes: "30", + jobMaxMinutes: "45", + }, + workOrderNumber: "WO-123456", + referralCorrelationId: "ref-123", + referralSequenceNumber: 1, +}); + +describe("payment-adyen.vue", () => { + beforeEach(() => { + store.getters = { + order: createValidOrder(), + damage: {}, + lineItems: { glassParts: [] }, + payment: {}, + policy: {}, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("arePagePrerequisitesValid", () => { + test("returns true when all prerequisites are valid", () => { + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(true); + }); + + test("returns false when service location info is missing", () => { + store.getters.order.serviceLocation.address = null; + store.getters.order.serviceLocation.provider.address.streetAddress = null; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when insurance info is invalid", () => { + store.getters.order.payment.isInsurance = null; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when scheduling info is missing", () => { + store.getters.order.schedule.date = null; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when customer info is missing", () => { + store.getters.order.customer.firstName = null; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when payment method info is invalid", () => { + store.getters.order.payment.isPia = null; + store.getters.order.payment.piaType = null; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns true for mobile appointment type when address fields are present", () => { + store.getters.order.serviceLocation.appointmentType = + AppointmentTypeStrings.MOBILE; + store.getters.order.serviceLocation.address = "123 Mobile St"; + store.getters.order.serviceLocation.city = "Columbus"; + store.getters.order.serviceLocation.state = "OH"; + store.getters.order.serviceLocation.zipCode = "43235"; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(true); + }); + }); +}); + +function setupMocks({ customMountOptions } = {}) { + const mountOptions = getMountOptions({ + ...customMountOptions, + store, + route: { name: "payment-adyen" }, + router: { + navigateWithSaving: jest.fn(), + navigateWithoutSaving: jest.fn(), + navigateWithPageData: jest.fn(), + }, + }); + mountOptions.global.mocks["navigationScenarios"] = {}; + mountOptions.mixins = [ + { + methods: { + getCmsContent: jest.fn(), + setCmsContent: jest.fn(), + }, + }, + ]; + + return shallowMount(PaymentAdyenTestComponent, mountOptions); +} diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index 1bc123d3f..353316427 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -8,6 +8,7 @@ import store from "@/store"; import { experimentSettings } from "@/constants/experiments"; import { storeMutations } from "@/constants/store-mutations"; import globalMethods from "@/global-methods"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; globalMethods.callHttpClient = jest.fn(); @@ -29,8 +30,64 @@ jest.mock("@/helpers/pricing-helper.js", () => ({ getAmountDue: jest.fn(), })); +jest.mock("@/helpers/debug-log-helper.js", () => ({ + debugLog: jest.fn(), +})); + let piaDisabledFlag = false; +const createValidOrderForPaymentMethod = () => ({ + payment: { + isPia: false, + isInsurance: false, + insuranceCoverage: { isVerified: true }, + }, + lineItems: { + glassParts: [], + supportingItems: [], + vaps: [], + promos: [], + }, + policy: { + currentDeductible: 123, + isNoComp: false, + isItac: false, + }, + serviceLocation: { + appointmentType: AppointmentTypeStrings.MOBILE, + address: "123 Test Ave", + address2: "", + city: "Anytown", + state: "OH", + zipCode: "00000", + zipCodeCtu: "00000", + provider: { + address: { + streetAddress: "456 Provider St", + city: "Anytown", + state: "OH", + zipCode: "00000", + zipCodeCtu: "00000", + }, + }, + }, + customer: { + firstName: "John", + lastName: "Doe", + emailAddress: "john@example.com", + phoneNumber: "555-555-5555", + }, + schedule: { + date: "2024-01-15", + startTime: "09:00", + endTime: "10:00", + jobMinMinutes: "30", + jobMaxMinutes: "45", + }, + vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" }, + damage: { isRepair: false }, +}); + describe("payment-method.vue", () => { describe("navigation", () => { test("if the back button is clicked, navigate back", async () => { @@ -54,6 +111,53 @@ describe("payment-method.vue", () => { expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled(); }); }); + + describe("arePagePrerequisitesValid", () => { + test("returns true when all prerequisites are valid", () => { + const { wrapper } = setupMocks(); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(true); + }); + + test("returns false when service location info is missing", () => { + const { wrapper } = setupMocks(); + store.getters.order.serviceLocation.address = null; + store.getters.order.serviceLocation.provider.address.streetAddress = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when insurance info is invalid", () => { + const { wrapper } = setupMocks(); + store.getters.order.payment.isInsurance = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when scheduling info is missing", () => { + const { wrapper } = setupMocks(); + store.getters.order.schedule.date = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when customer info is missing", () => { + const { wrapper } = setupMocks(); + store.getters.order.customer.firstName = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + }); }); function setupMocks() { @@ -65,35 +169,7 @@ function setupMocks() { vaps: [], promos: [], }, - order: { - payment: { - isPia: false, - insuranceCoverage: { - isVerified: true, - }, - }, - lineItems: { - glassParts: [], - supportingItems: [], - vaps: [], - promos: [], - }, - policy: { - currentDeductible: 123, - isNoComp: false, - isItac: false, - }, - serviceLocation: { - appointmentType: "Mobile", - address: "123 Test Ave", - city: "Anytown", - state: "OH", - zipCode: "00000", - provider: { providerNumber: "0000567" }, - }, - vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" }, - damage: { isRepair: false }, - }, + order: createValidOrderForPaymentMethod(), applicationUser: { experiments: [], }, diff --git a/src/layouts/payment/payment.spec.js b/src/layouts/payment/payment.spec.js index 1b36d0644..ea5341eb0 100644 --- a/src/layouts/payment/payment.spec.js +++ b/src/layouts/payment/payment.spec.js @@ -258,6 +258,42 @@ describe("payment.vue", () => { expect(result).toBe(true); }); + test("Returns false when service location info is missing", () => { + const wrapper = setupMocks({}); + store.getters.order.serviceLocation.provider.address.streetAddress = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("Returns false when insurance info is invalid", () => { + const wrapper = setupMocks({}); + store.getters.order.payment.isInsurance = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("Returns false when scheduling info is missing", () => { + const wrapper = setupMocks({}); + store.getters.order.schedule.date = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("Returns false when customer info is missing", () => { + const wrapper = setupMocks({}); + store.getters.order.customer.firstName = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + describe("Payment method cases", () => { test("Returns false if pia options are not valid", () => { // Arrange diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index 60dd548ad..d9a36f11d 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -295,6 +295,47 @@ describe("quote.vue", () => { expect(arePagePrerequisitesValid).toBe(false); }); + + test("should fail arePagePrerequisitesValid when service zip info is missing", () => { + store.getters = { + order: { + lineItems: { glassParts: ["item"] }, + serviceLocation: { + zipCode: null, + zipCodeCtu: "value", + }, + damage: { isRepair: false }, + payment: { insuranceCoverage: { isVerified: false } }, + referralNumber: "1234567", + }, + }; + const { wrapper } = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("should fail arePagePrerequisitesValid when insurance coverage is verified", () => { + store.getters = { + order: { + lineItems: { glassParts: ["item"] }, + serviceLocation: { + zipCode: "12345", + zipCodeCtu: "value", + }, + damage: { isRepair: false }, + payment: { insuranceCoverage: { isVerified: true } }, + referralNumber: "1234567", + }, + }; + const { wrapper } = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => { //Arrange store.getters = { diff --git a/src/layouts/schedule/schedule.spec.js b/src/layouts/schedule/schedule.spec.js index 7d5cdcec1..9e7952fd0 100644 --- a/src/layouts/schedule/schedule.spec.js +++ b/src/layouts/schedule/schedule.spec.js @@ -57,6 +57,40 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ navigateToHeritageFunnel: jest.fn(), })); +jest.mock("@/helpers/debug-log-helper.js", () => ({ + debugLog: jest.fn(), +})); + +const createValidOrderForSchedule = () => ({ + payment: { + isInsurance: false, + insuranceCoverage: { isVerified: false }, + }, + referralNumber: "", + serviceLocation: { + address: "", + address2: "", + city: "", + state: "", + zipCode: "00000", + zipCodeCtu: "000", + appointmentType: null, + provider: {}, + isVehicleProtected: false, + }, + schedule: { + date: "", + routeCode: "", + startTime: "", + endTime: "", + jobMinMinutes: null, + jobMaxMinutes: null, + }, + policy: { isItac: false, isNoComp: false }, + damage: { isRepair: true }, + lineItems: { glassParts: [] }, +}); + describe("schedule.vue", () => { describe("navigation", () => { test("backButtonAction => non-insurance: navigateWithoutSaving called", () => { @@ -149,8 +183,67 @@ describe("schedule.vue", () => { expect(wrapper.vm.$router.navigateWithSaving).not.toHaveBeenCalled(); }); }); + + describe("arePagePrerequisitesValid", () => { + test("returns true when all prerequisites are valid", () => { + const { wrapper } = setupMocksForArePagePrerequisitesValid(); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(true); + }); + + test("returns false when service zip info is missing", () => { + const { wrapper } = setupMocksForArePagePrerequisitesValid(); + store.getters.order.serviceLocation.zipCode = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when glass parts or repair info is missing", () => { + const { wrapper } = setupMocksForArePagePrerequisitesValid(); + store.getters.order.damage.isRepair = false; + store.getters.order.lineItems.glassParts = []; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when insurance info is invalid", () => { + const { wrapper } = setupMocksForArePagePrerequisitesValid(); + store.getters.order.payment.isInsurance = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + + test("returns false when supporting items missing for cash order", () => { + const { wrapper } = setupMocksForArePagePrerequisitesValid(); + store.getters.order.payment.isInsurance = false; + store.getters.lineItems.supportingItems = null; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + }); }); +function setupMocksForArePagePrerequisitesValid() { + store.getters = { + ...storeMocked.getters, + order: createValidOrderForSchedule(), + lineItems: { + supportingItems: [], + }, + }; + return setupMocks({ store }); +} + function setupMocks(mountOptionsMockData = {}) { const route = { name: "schedule" }; const defaultMountOptions = { From f00aab4ad81f6d7e73ec971842125c87363d01fb Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 9 Mar 2026 17:41:11 -0400 Subject: [PATCH 14/16] Added hasGlassPartsOrRepairInfo check on the 3 payment pages --- src/layouts/payment-adyen/payment-adyen.spec.js | 14 ++++++++++++++ src/layouts/payment-adyen/payment-adyen.vue | 4 +++- src/layouts/payment-method/payment-method.spec.js | 12 +++++++++++- src/layouts/payment-method/payment-method.vue | 4 +++- src/layouts/payment/payment.spec.js | 10 ++++++++++ src/layouts/payment/payment.vue | 4 +++- 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.spec.js b/src/layouts/payment-adyen/payment-adyen.spec.js index e13b468c7..7ace84b97 100644 --- a/src/layouts/payment-adyen/payment-adyen.spec.js +++ b/src/layouts/payment-adyen/payment-adyen.spec.js @@ -87,6 +87,10 @@ const createValidOrder = () => ({ workOrderNumber: "WO-123456", referralCorrelationId: "ref-123", referralSequenceNumber: 1, + damage: { isRepair: false }, + lineItems: { + glassParts: [{ id: "part1", partType: "WINDSHIELD" }], + }, }); describe("payment-adyen.vue", () => { @@ -160,6 +164,16 @@ describe("payment-adyen.vue", () => { expect(result).toBe(false); }); + test("returns false when glass parts or repair info is missing", () => { + store.getters.order.damage.isRepair = false; + store.getters.order.lineItems.glassParts = []; + const wrapper = setupMocks({}); + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + test("returns true for mobile appointment type when address fields are present", () => { store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 4193c024b..5ae3e25cc 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -90,6 +90,7 @@ import { hasSchedulingInfo, hasCustomerInfo, hasPaymentMethodInfo, + hasGlassPartsOrRepairInfo, } from "@/helpers/page-prerequisites-helper.js"; export default { @@ -150,7 +151,8 @@ export default { hasInsuranceInfo(order, logQueue) && hasSchedulingInfo(order, logQueue) && hasCustomerInfo(order, logQueue) && - hasPaymentMethodInfo(order, logQueue); + hasPaymentMethodInfo(order, logQueue) && + hasGlassPartsOrRepairInfo(order, logQueue); flushPagePrereqsLogs("payment-adyen.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index 353316427..f539017d4 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -43,7 +43,7 @@ const createValidOrderForPaymentMethod = () => ({ insuranceCoverage: { isVerified: true }, }, lineItems: { - glassParts: [], + glassParts: [{ id: "part1", partType: "WINDSHIELD" }], supportingItems: [], vaps: [], promos: [], @@ -157,6 +157,16 @@ describe("payment-method.vue", () => { expect(result).toBe(false); }); + + test("returns false when glass parts or repair info is missing", () => { + const { wrapper } = setupMocks(); + store.getters.order.damage.isRepair = false; + store.getters.order.lineItems.glassParts = []; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); }); }); diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index e5eaab5d0..0ee3ad8ce 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -184,6 +184,7 @@ import { hasInsuranceInfo, hasSchedulingInfo, hasCustomerInfo, + hasGlassPartsOrRepairInfo, } from "@/helpers/page-prerequisites-helper.js"; import { ErrorMessage } from "vee-validate"; import { Field } from "vee-validate"; @@ -393,7 +394,8 @@ export default { hasServiceLocationInfo(order, logQueue) && hasInsuranceInfo(order, logQueue) && hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue); + hasCustomerInfo(order, logQueue) && + hasGlassPartsOrRepairInfo(order, logQueue); flushPagePrereqsLogs("payment-method.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment/payment.spec.js b/src/layouts/payment/payment.spec.js index ea5341eb0..6f655fa0a 100644 --- a/src/layouts/payment/payment.spec.js +++ b/src/layouts/payment/payment.spec.js @@ -294,6 +294,16 @@ describe("payment.vue", () => { expect(result).toBe(false); }); + test("Returns false when glass parts or repair info is missing", () => { + const wrapper = setupMocks({}); + store.getters.order.damage.isRepair = false; + store.getters.order.lineItems.glassParts = []; + + const result = wrapper.vm.arePagePrerequisitesValid(); + + expect(result).toBe(false); + }); + describe("Payment method cases", () => { test("Returns false if pia options are not valid", () => { // Arrange diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 4b4f0c3a3..0fd452a9d 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -249,6 +249,7 @@ import { hasSchedulingInfo, hasCustomerInfo, hasPaymentMethodInfo, + hasGlassPartsOrRepairInfo, } from "@/helpers/page-prerequisites-helper.js"; import buttonQuestion from "@/digital-components/button-question/button-question"; import paymentMethodListButton from "@/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button"; @@ -449,7 +450,8 @@ export default { hasInsuranceInfo(order, logQueue) && hasSchedulingInfo(order, logQueue) && hasCustomerInfo(order, logQueue) && - hasPaymentMethodInfo(order, logQueue); + hasPaymentMethodInfo(order, logQueue) && + hasGlassPartsOrRepairInfo(order, logQueue); flushPagePrereqsLogs("payment.vue", result, logQueue); return result; }, From 801e759f6b55fcd3eee6f0f9e3b42736acecd872 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Tue, 10 Mar 2026 09:17:03 -0400 Subject: [PATCH 15/16] Prettier making some weird edits... --- src/helpers/page-prerequisites-helper.js | 2 +- src/layouts/payment-adyen/payment-adyen.spec.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/helpers/page-prerequisites-helper.js b/src/helpers/page-prerequisites-helper.js index 2be2804ec..9a7a4fcbc 100644 --- a/src/helpers/page-prerequisites-helper.js +++ b/src/helpers/page-prerequisites-helper.js @@ -173,7 +173,7 @@ export function hasInsuranceInfo(order, logQueue = null) { } /** - * Checks whether the scheduling information is valid for the order. + * Checks whether the scheduling information is valid for the order. * Should be used on pages after scheduling info collection is complete. * Returns true if the required info is present for the chosen appointment type. * For MOBILE: date, startTime, endTime, jobMaxMinutes, and jobMinMinutes must be present on schedule. diff --git a/src/layouts/payment-adyen/payment-adyen.spec.js b/src/layouts/payment-adyen/payment-adyen.spec.js index 7ace84b97..26d3a6aec 100644 --- a/src/layouts/payment-adyen/payment-adyen.spec.js +++ b/src/layouts/payment-adyen/payment-adyen.spec.js @@ -175,8 +175,7 @@ describe("payment-adyen.vue", () => { }); test("returns true for mobile appointment type when address fields are present", () => { - store.getters.order.serviceLocation.appointmentType = - AppointmentTypeStrings.MOBILE; + store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; store.getters.order.serviceLocation.address = "123 Mobile St"; store.getters.order.serviceLocation.city = "Columbus"; store.getters.order.serviceLocation.state = "OH"; From 4797eaf649b2b20b86ab3ac4673a999d59316c6f Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 10 Mar 2026 13:32:39 -0400 Subject: [PATCH 16/16] CASH-2458: Data events update for MultiLocationModal --- .../multi-location-modal.vue | 21 ++++++++++++++----- src/layouts/schedule/schedule.vue | 12 ----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index ef49fa2c2..85afe5211 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -80,7 +80,6 @@ export default { confirmedAppointment: false, multiLocationRadioButton: multiLocationRadioButton, selectedValue: null, - experimentSettings: "", }; }, methods: { @@ -124,8 +123,20 @@ export default { setIsModalOpen(isOpen) { this.isModalOpen = isOpen; }, - captureExperimentSettings(settingsString) { - this.experimentSettings = settingsString; + selectedSlotEventValue() { + const daysUntilAppointment = + (new Date(this.selectedAppointment.date) - new Date()) / (1000 * 60 * 60 * 24); + const isAM = this.selectedAppointment.timeSlot.startTime < "12:00:00"; + return ( + "AM: " + + isAM + + " | " + + "PM: " + + !isAM + + " | " + + "Days: " + + Math.ceil(daysUntilAppointment) + ); }, pushEvent() { let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO; @@ -143,7 +154,7 @@ export default { this.mobileAppointmentOption.date, true, null, - this.experimentSettings + this.selectedSlotEventValue() ); } else if ( this.selectedValue?.toLowerCase() === "inshop" && @@ -157,7 +168,7 @@ export default { this.inshopAppointmentOption.date, true, null, - this.experimentSettings + this.selectedSlotEventValue() ); } }, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 634f0c8bf..9aca972bf 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2075,15 +2075,6 @@ export default { let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION ); - let experimentSettingsString = - "ShowPm:" + - maxDayRangeToShowPmTimeslot + - "|" + - "NoPm:" + - maxDayRangeToShowNoPmTimeslot + - "|" + - "NoAvail:" + - maxDayRangeToShowAnyTimeslot; let promotedMobileAppointment = null; let promotedInshopAppointment = null; const todaysDate = getTodayDate(); @@ -2205,9 +2196,6 @@ export default { promotedInshopAppointment ); this.$refs.multiLocationModal.openModal(); - this.$refs.multiLocationModal.captureExperimentSettings( - experimentSettingsString - ); } } }