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/helpers/page-prerequisites-helper.js b/src/helpers/page-prerequisites-helper.js new file mode 100644 index 000000000..9a7a4fcbc --- /dev/null +++ b/src/helpers/page-prerequisites-helper.js @@ -0,0 +1,281 @@ +import { debugLog } from "@/helpers/debug-log-helper.js"; +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 + { + debugLog(`--- ${source} pagePrereqs start ---`, null, !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 + { + debugLog(`--- ${source} pagePrereqs end ---`, null, !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); + + 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; +} + +/** + * 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 = !!( + 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; +} + +/** + * 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; + const currentDeductible = order.policy?.currentDeductible; + const parentAccountNumber = order.payment?.parentAccountNumber; + const policyNumber = order.policy?.policyNumber; + + let result = false; + if (isInsurance !== null) { + if (isInsurance === false) { + result = true; // Cash selected - no need for policy number or parent account + } else { + 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; + } + } + } + + queueLogging(logQueue, () => { + // 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); + } + debugLog("", null, !result); + } + }); + + 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 = !!( + 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; +} + +/** + * 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 = !!( + 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; +} + +/** + * 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; + 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; +} + +/** + * 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); + + 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/helpers/page-prerequisites-helper.spec.js b/src/helpers/page-prerequisites-helper.spec.js new file mode 100644 index 000000000..a619720c2 --- /dev/null +++ b/src/helpers/page-prerequisites-helper.spec.js @@ -0,0 +1,716 @@ +import * as pagePrereqsHelper from "@/helpers/page-prerequisites-helper"; +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("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); + + 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", () => { + 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 (cash selected)", () => { + const order = { + payment: { + isInsurance: false, + insuranceCoverage: { coverageStatus: coverageStatus.PENDING }, + }, + }; + + 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, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", 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, + 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, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", 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, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", 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, + insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED }, + parentAccountNumber: 999999, + }, + policy: { policyNumber: "POL123", 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/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/mobile-details/mobile-details.vue b/src/layouts/mobile-details/mobile-details.vue index b6978a7ff..cb98386e6 100644 --- a/src/layouts/mobile-details/mobile-details.vue +++ b/src/layouts/mobile-details/mobile-details.vue @@ -78,6 +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"; export default { name: "MobileDetails", data() { @@ -110,6 +111,9 @@ export default { }, methods: { arePagePrerequisitesValid() { + const logQueue = []; + const order = store.getters.order; + const serviceLocation = store.getters.order.serviceLocation; const serviceLocationPreReqs = serviceLocation.zipCode && @@ -118,16 +122,12 @@ 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; + + flushPagePrereqsLogs("mobile-details.vue", preReqResult, logQueue); - const preReqResult = serviceLocationPreReqs && scheduleReqs; return preReqResult; }, getServiceAddressFromStore() { 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..26d3a6aec --- /dev/null +++ b/src/layouts/payment-adyen/payment-adyen.spec.js @@ -0,0 +1,214 @@ +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, + damage: { isRepair: false }, + lineItems: { + glassParts: [{ id: "part1", partType: "WINDSHIELD" }], + }, +}); + +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 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; + 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-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index cddb2189c..5ae3e25cc 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 { + flushPagePrereqsLogs, + hasServiceLocationInfo, + hasInsuranceInfo, + hasSchedulingInfo, + hasCustomerInfo, + hasPaymentMethodInfo, + hasGlassPartsOrRepairInfo, +} from "@/helpers/page-prerequisites-helper.js"; export default { name: "payment-adyen", @@ -136,110 +144,17 @@ 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) && + hasGlassPartsOrRepairInfo(order, logQueue); + flushPagePrereqsLogs("payment-adyen.vue", result, logQueue); + return result; }, async initializeAdyen() { diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index 1bc123d3f..f539017d4 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: [{ id: "part1", partType: "WINDSHIELD" }], + 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,63 @@ 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); + }); + + 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); + }); + }); }); function setupMocks() { @@ -65,35 +179,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-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index d333b4bbf..0ee3ad8ce 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 { + flushPagePrereqsLogs, + hasServiceLocationInfo, + hasInsuranceInfo, + hasSchedulingInfo, + hasCustomerInfo, + hasGlassPartsOrRepairInfo, +} 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,16 @@ 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) && + hasGlassPartsOrRepairInfo(order, logQueue); + flushPagePrereqsLogs("payment-method.vue", result, logQueue); + return result; }, getPaymentMethodFromStore() { const piaType = store.getters.order.payment.piaType; diff --git a/src/layouts/payment/payment.spec.js b/src/layouts/payment/payment.spec.js index 1b36d0644..6f655fa0a 100644 --- a/src/layouts/payment/payment.spec.js +++ b/src/layouts/payment/payment.spec.js @@ -258,6 +258,52 @@ 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); + }); + + 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 6c547f3d2..0fd452a9d 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 { + flushPagePrereqsLogs, + hasServiceLocationInfo, + hasInsuranceInfo, + 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"; @@ -435,110 +443,17 @@ 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) && + hasGlassPartsOrRepairInfo(order, logQueue); + flushPagePrereqsLogs("payment.vue", result, logQueue); + return result; }, getAnswersNullSafe(widgetName) { const rawData = this.getCmsContent(widgetName, "Answers"); 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/quote/quote.vue b/src/layouts/quote/quote.vue index 851f912ed..b0fda113c 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -189,6 +189,11 @@ 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 { + flushPagePrereqsLogs, + hasServiceZipInfo, + hasGlassPartsOrRepairInfo, +} from "@/helpers/page-prerequisites-helper.js"; import { savePageData } from "@/router/methods/helpers/save-page-data"; import { quotePageDiscountTable } from "../../constants/quote-page-discounts"; @@ -735,28 +740,26 @@ 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 + ); + }); + flushPagePrereqsLogs("quote.vue", preReqResult, logQueue); return preReqResult; }, vapsItemsSelectedAction(vapsItemsSelected) { 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; +} 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..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: { @@ -103,6 +102,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", { @@ -123,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; @@ -142,7 +154,7 @@ export default { this.mobileAppointmentOption.date, true, null, - this.experimentSettings + this.selectedSlotEventValue() ); } else if ( this.selectedValue?.toLowerCase() === "inshop" && @@ -152,11 +164,11 @@ export default { this.GaCategories.INSHOP_CONFIRMATION_CLICKED, gaAction, this.inshopAppointmentOption.timeSlot.startTime + - "-" + + " " + this.inshopAppointmentOption.date, true, null, - this.experimentSettings + this.selectedSlotEventValue() ); } }, @@ -300,6 +312,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.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 = { diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 1f64318ce..77f9db033 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -254,6 +254,8 @@ import { sumDateString, isDropOffRouteCode, getTodayDate, + getTodayDateString, + getInitialViewWeeks, } from "@/layouts/schedule/helpers/schedule-helper"; import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper"; @@ -263,6 +265,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, @@ -487,6 +495,7 @@ export default { selectedMobileFirstAppointment: false, selectedMultiLocationAppointment: false, calendarLoadingStatus: "none", + isLoadingMultiLocationPopup: true, }; }, async beforeRouteEnter(to, from, next) { @@ -617,9 +626,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(); @@ -811,13 +820,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 && @@ -826,18 +828,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 []; }, @@ -909,6 +904,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; @@ -979,80 +978,47 @@ 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, 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) { @@ -1942,8 +1908,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; } @@ -2069,7 +2035,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 ); @@ -2079,18 +2052,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 + - "|" + - "NoPm:" + - maxDayRangeToShowNoPmTimeslot + - "|" + - "NoAvail:" + - maxDayRangeToShowAnyTimeslot; let promotedMobileAppointment = null; let promotedInshopAppointment = null; const todaysDate = getTodayDate(); @@ -2104,25 +2065,23 @@ 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"), - ]); + 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 = @@ -2136,10 +2095,7 @@ export default { (firstInshopPMDate && numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot); - if (showMultiLocationAppointment?.toLowerCase() === "true") { - return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; - } - return false; + return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; }; if (shouldExposeMultiLocationModal()) { @@ -2173,8 +2129,8 @@ export default { // Selects the first available inshop time slot promotedInshopAppointment = numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate - ? firstInshopAMAppt - : firstInshopPMAppt; + ? firstInshopAppts[0] + : firstInshopAppts[1]; } else { if ( firstMobilePMDate && @@ -2197,15 +2153,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]; } } @@ -2217,23 +2173,15 @@ export default { promotedInshopAppointment ); this.$refs.multiLocationModal.openModal(); - this.$refs.multiLocationModal.captureExperimentSettings( - experimentSettingsString - ); } } } } + this.isLoadingMultiLocationPopup = false; }, - async getFirstAvailableApptByTOD(timeOfDay, appointmentType) { - let selectableDays; - if (appointmentType?.toLowerCase() == "mobile") { - selectableDays = this.selectableDatesMobile?.days; - } else if (appointmentType?.toLowerCase() == "inshop") { - selectableDays = this.selectableDatesInshop?.days; - } else { - return; - } + + async getFirstAvailableMobileApptByTOD(timeOfDay) { + const selectableDays = this.selectableDatesMobile?.days; if (!selectableDays?.length) { return null; } else { @@ -2245,7 +2193,6 @@ export default { }, timeSlot: null, date: null, - addressCopy: null, }; const isMatchingApptDay = dateObj.timeSlots.some( (slot) => @@ -2253,16 +2200,6 @@ export default { (matchingTimeSlot.timeSlot = slot) && (matchingTimeSlot.date = dateObj.date) ); - if ( - appointmentType?.toLowerCase() == "inshop" && - this.selectedShopAnswer?.address1 && - this.selectedShopAnswer?.address2 - ) { - matchingTimeSlot.addressCopy = - this.selectedShopAnswer?.address1 + - ", " + - this.selectedShopAnswer?.address2; - } if (isMatchingApptDay && matchingTimeSlot) { return matchingTimeSlot; } @@ -2270,6 +2207,136 @@ export default { return null; } }, + + 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; + }, + + 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; this.appointmentType = AppointmentTypeStrings.MOBILE; @@ -2279,21 +2346,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: { 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,