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 0bd095b77..df1a4ab7d 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -183,6 +183,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"; @@ -728,28 +733,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/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 634f0c8bf..9eb191bc4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -263,6 +263,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, @@ -985,76 +991,32 @@ export default { methods: { splitCopyOnCMSPlaceHolder, arePagePrerequisitesValid() { - const paymentInfo = store.getters.payment.isInsurance !== null; - const damageInfo = - store.getters.order.damage.isRepair || - (store.getters.order.lineItems?.glassParts != null && - store.getters.order.lineItems.glassParts.length > 0); - const parentAccountNumberInfo = - store.getters.payment.parentAccountNumber !== - applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; - const policyNumberInfo = store.getters.order.policy.policyNumber; - const zipCodeInfo = store.getters.order.serviceLocation.zipCode; - const supportingItemsInfo = store.getters.lineItems.supportingItems; + const order = store.getters.order; + const logQueue = []; + + const serviceZip = hasServiceZipInfo(order, logQueue); + const glassPartsOrRepair = hasGlassPartsOrRepairInfo(order, logQueue); + const insuranceInfo = hasInsuranceInfo(order, logQueue); // insurance drops the recycle fee on replace orders so it won't be in supportingItems - const insurancePreReqResult = - parentAccountNumberInfo && - policyNumberInfo && - zipCodeInfo && - paymentInfo && - damageInfo; - const cashPreReqResult = - supportingItemsInfo && zipCodeInfo && paymentInfo && damageInfo; + const supportingItemsInfo = store.getters.lineItems?.supportingItems; + const supportingItemsPreReqs = order.payment?.isInsurance + ? true + : !!supportingItemsInfo; - const outputDebugLog = (preReqResult) => { - // prettier-ignore - debugLog("--- schedule.vue pagePrereqs start ---", null, !preReqResult); - debugLog( - "store.getters.payment.isInsurance:", - store.getters.payment?.isInsurance, - !preReqResult - ); - debugLog( - "store.getters.order.damage.isRepair:", - store.getters.order.damage?.isRepair, - !preReqResult - ); - debugLog( - "store.getters.order.lineItems.glassParts:", - store.getters.order.lineItems?.glassParts, - !preReqResult - ); - debugLog( - "store.getters.payment.parentAccountNumber:", - store.getters.payment.parentAccountNumber, - !preReqResult - ); - debugLog( - "store.getters.order.policy.policyNumber:", - store.getters.order.policy.policyNumber, - !preReqResult - ); - debugLog( - "store.getters.order.serviceLocation.zipCode:", - store.getters.order.serviceLocation.zipCode, - !preReqResult - ); + const preReqResult = + serviceZip && insuranceInfo && glassPartsOrRepair && supportingItemsPreReqs; + + logQueue.push(() => { debugLog( "store.getters.lineItems.supportingItems:", - store.getters.lineItems.supportingItems, - !preReqResult + store.getters.lineItems?.supportingItems, + !supportingItemsPreReqs ); - debugLog("--- schedule.vue pagePrereqs end ---", null, !preReqResult); - }; + }); - if (store.getters.payment.isInsurance) { - outputDebugLog(insurancePreReqResult); - return insurancePreReqResult; - } else { - outputDebugLog(cashPreReqResult); - return cashPreReqResult; - } + flushPagePrereqsLogs("schedule.vue", preReqResult, logQueue); + return preReqResult; }, setDataOnLoad(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) {