diff --git a/playwright-tests/.env b/playwright-tests/.env index 179424e5e..e9de46d32 100644 --- a/playwright-tests/.env +++ b/playwright-tests/.env @@ -9,8 +9,9 @@ SKIP_CONTENT_SITE="false" # Experiments Flag IS_MOBILEFIRST="false" -IS_ADYENPAYMENTS="false" +IS_ADYENPAYMENTS="true" IS_MULTILOCATIONPOPUP="false" +IS_MSRSPLITPAY="false" # Base URLs by environment # qa diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index de2c3c0f2..80841fd14 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -2,15 +2,16 @@ # Environment configuration # Environment type -PLAYWRIGHT_ENV="qa" +PLAYWRIGHT_ENV="QA" # Skip content site SKIP_CONTENT_SITE="false" # Experiments Flag IS_MOBILEFIRST="false" -IS_ADYENPAYMENTS="false" +IS_ADYENPAYMENTS="true" IS_MULTILOCATIONPOPUP="false" +IS_MSRSPLITPAY="false" # Base URLs by environment # qa diff --git a/playwright-tests/framework/TestData.ts b/playwright-tests/framework/TestData.ts index 6a87dfd8f..332e3dc26 100644 --- a/playwright-tests/framework/TestData.ts +++ b/playwright-tests/framework/TestData.ts @@ -11,6 +11,10 @@ export interface ITestData extends base { mockFirstInshopCallNoSchedule?: boolean, handleMobileFirstModal?: boolean, experiments?: IExperiments + isMSRGlassPart?: boolean, + isMSRZip?: boolean, + isPIAEnabled?: boolean, + isDualRecal?: boolean, } @@ -19,5 +23,6 @@ export function getDefaultExperimentsData(): IExperiments { isAdyenPayments: !!process.env.IS_ADYENPAYMENTS && process.env.IS_ADYENPAYMENTS !== "" ? process.env.IS_ADYENPAYMENTS === "true" : false, isMobileFirst: !!process.env.IS_MOBILEFIRST && process.env.IS_MOBILEFIRST !== "" ? process.env.IS_MOBILEFIRST === "true" : false, isMultiLocationPopup: !!process.env.IS_MULTILOCATIONPOPUP && process.env.IS_MULTILOCATIONPOPUP !== "" ? process.env.IS_MULTILOCATIONPOPUP === "true" : false, + isMsrSplitPay: !!process.env.IS_MSRSPLITPAY && process.env.IS_MSRSPLITPAY !== "" ? process.env.IS_MSRSPLITPAY === "true" : false, } } diff --git a/playwright-tests/framework/TestPages.ts b/playwright-tests/framework/TestPages.ts index c2cc04b35..39c2832f9 100644 --- a/playwright-tests/framework/TestPages.ts +++ b/playwright-tests/framework/TestPages.ts @@ -20,6 +20,7 @@ import VehiclePartQuestionsPage from "../pages/VehiclePartsPage" import CapabilityQuestionsPage from "../pages/CapabilityQuestionsPage" import MoldingQuestionsPage from "../pages/MoldingQuestionsPage" import { InsuranceCompanyPage } from "../pages/InsuranceCompanyPage" +import { InsuranceDetailsPage } from "../pages/InsuranceDetailsPage" import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage" import { DuplicateCheckPage } from "../pages/DuplicateCheckPage" import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage" @@ -32,8 +33,12 @@ import { EndorsementsPage } from "../pages/EndorsementsPage" import { PolicyDriverPage } from "../pages/PolicyDriverPage" import { ServiceZipPage } from "../pages/ServiceZipPage" import { MobileDetailsPage } from "pages/MobileDetailsPage"; +import { BailoutPage } from '../pages/BailoutPage'; +import { BailoutSuccessPage } from '../pages/BailoutSuccessPage'; export interface ITestPages { + bailoutPage: BailoutPage, + bailoutSuccessPage: BailoutSuccessPage, capabilityQuestionsPage: CapabilityQuestionsPage, ccPolicyInfoPage: CCPolicyInfoPage, contactDetailsPage: ContactDetailsPage, @@ -42,6 +47,7 @@ export interface ITestPages { estimatePage: EstimatePage, homePage: HomePage, insuranceCompanyPage: InsuranceCompanyPage, + insuranceDetailsPage: InsuranceDetailsPage, leadgenHomePage: LeadgenHomePage, moldingQuestionsPage: MoldingQuestionsPage, orderConfirmationPage: OrderConfirmationPage, @@ -70,6 +76,8 @@ export interface ITestPages { export const createTestPages: TestPagesFactory = (page: Page) => { const pages: ITestPages = { + bailoutPage: new BailoutPage(page), + bailoutSuccessPage: new BailoutSuccessPage(page), capabilityQuestionsPage: new CapabilityQuestionsPage(page), ccPolicyInfoPage: new CCPolicyInfoPage(page), contactDetailsPage: new ContactDetailsPage(page), @@ -78,6 +86,7 @@ export const createTestPages: TestPagesFactory = (page: Page) => { estimatePage: new EstimatePage(page), homePage: new HomePage(page), insuranceCompanyPage: new InsuranceCompanyPage(page), + insuranceDetailsPage: new InsuranceDetailsPage(page), leadgenHomePage: new LeadgenHomePage(page), moldingQuestionsPage: new MoldingQuestionsPage(page), orderConfirmationPage: new OrderConfirmationPage(page), diff --git a/playwright-tests/framework/localTypes/Enums.ts b/playwright-tests/framework/localTypes/Enums.ts index d112f9110..3e69f19e7 100644 --- a/playwright-tests/framework/localTypes/Enums.ts +++ b/playwright-tests/framework/localTypes/Enums.ts @@ -18,6 +18,7 @@ export enum ProgressBarPercentages { VehiclePartsPage = '20%', ServicePackagePage = '60%', InsuranceCompanyPage = '60%', + InsuranceDetailsPage = '55%', ServiceLocationPage = '76%', SchedulePage = '76%', MobileDetailsPage = '76%', diff --git a/playwright-tests/framework/localTypes/IExperiments.ts b/playwright-tests/framework/localTypes/IExperiments.ts index bfe13c864..ec0d15180 100644 --- a/playwright-tests/framework/localTypes/IExperiments.ts +++ b/playwright-tests/framework/localTypes/IExperiments.ts @@ -1,5 +1,6 @@ export interface IExperiments { isMobileFirst: boolean, isAdyenPayments: boolean, - isMultiLocationPopup: boolean + isMultiLocationPopup: boolean, + isMsrSplitPay: boolean } \ No newline at end of file diff --git a/playwright-tests/pages/BailoutPage.ts b/playwright-tests/pages/BailoutPage.ts new file mode 100644 index 000000000..5d8c54428 --- /dev/null +++ b/playwright-tests/pages/BailoutPage.ts @@ -0,0 +1,51 @@ +import { type Locator, type Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { step } from 'framework/localTypes/Step'; +import { ICustomerDetails } from 'safelite-playwright-core'; +import { ITestData } from 'framework/TestData'; + +export class BailoutPage extends BasePage { + readonly page: Page; + readonly firstNameTextBox: Locator; + readonly lastNameTextBox: Locator; + readonly emailAddressTextBox: Locator; + readonly phoneNumberTextBox: Locator; + readonly optInToSMSBox: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.firstNameTextBox = this.page.getByRole('textbox', { name: 'First name' }); + this.lastNameTextBox = this.page.getByRole('textbox', { name: 'Last name' }); + this.emailAddressTextBox = this.page.getByRole('textbox', { name: 'Email address' }); + this.phoneNumberTextBox = this.page.getByRole('textbox', { name: 'Phone number' }); + this.optInToSMSBox = this.page.getByRole('checkbox', { name: 'Opt in to SMS' }); + } + + async fillOutBailoutForm(customerDetails: ICustomerDetails) { + await this.firstNameTextBox.fill(customerDetails!.firstName!); + await this.lastNameTextBox.fill(customerDetails!.lastName!); + await this.emailAddressTextBox.fill(customerDetails!.email!); + await this.phoneNumberTextBox.fill(customerDetails!.phoneNumber!); + } + + async checkOptInToSMSBox() { + await this.optInToSMSBox.check(); + } + + @step("BailoutPage >> Fill out bailout form: ") + async handleBailoutPage(testData: Partial) { + + const { customerDetails } = testData; + + await expect(this.page).toHaveURL(/bailout/); + await this.fillOutBailoutForm(customerDetails!); + + if (testData.isOptedInForTextMessages) { + await this.checkOptInToSMSBox(); + } + + await this.nextPage(); + } +} diff --git a/playwright-tests/pages/BailoutSuccessPage.ts b/playwright-tests/pages/BailoutSuccessPage.ts new file mode 100644 index 000000000..603172bd3 --- /dev/null +++ b/playwright-tests/pages/BailoutSuccessPage.ts @@ -0,0 +1,35 @@ +import { type Locator, type Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { TestSuccessAlert } from 'safelite-playwright-core'; +import { step } from 'framework/localTypes/Step'; + +export class BailoutSuccessPage extends BasePage { + readonly thankYouHeading: Locator; + readonly bodyText: Locator; + readonly returnToHomepageButton: Locator; + + constructor(page: Page) { + super(page); + this.thankYouHeading = page.getByText("Thanks! We'll get back to you soon"); + this.bodyText = page.getByText(/We've got it from here!/); + this.returnToHomepageButton = page.locator('#btn-vehicle-not-listed'); + } + + async validateBailoutSuccessPage() { + await expect(this.page).toHaveURL(/bailout-success/); + await expect(this.thankYouHeading).toBeVisible(); + await expect(this.bodyText).toBeVisible(); + await expect(this.bodyText).toHaveText( + /We've got it from here! One of our experts will be in touch to schedule your appointment\. If you have any questions, please contact 1-888-238-4527/ + ); + await expect(this.returnToHomepageButton).toBeVisible(); + } + + @step("BailoutSuccessPage >> Validate bailout success page and return to vehicle page") + async handleBailoutSuccessPage() { + await this.validateBailoutSuccessPage(); + await this.returnToHomepageButton.click(); + await expect(this.page).toHaveURL(/vehicle/); + throw new TestSuccessAlert('Parts not found bailout validated successfully.'); + } +} diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index e9fe835dc..4e6a9e93c 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -19,6 +19,7 @@ export class BasePage { readonly hamburgerMenu: Locator; readonly progressBar: Locator; readonly loaders: Locator; + readonly yellowAlertMessage: Locator; constructor(page: Page) { this.page = page; @@ -29,6 +30,7 @@ export class BasePage { this.hamburgerMenu = this.page.getByRole('button', { name: 'Hamburger Menu (modal window)' }); this.progressBar = this.page.locator('.progress-bar-outer .progress-bar-inner'); this.loaders = this.page.locator('.spinner-border, .loader'); + this.yellowAlertMessage = this.page.locator('.alert-warning'); } async nextPage() { @@ -135,42 +137,30 @@ export class BasePage { async mockScheduleResponseForFirstInshopCallNoSchedule(customerDetails: ICustomerDetails) { - let fistInshopScheduleCall = true; + // let fistInshopScheduleCall = true; const apiUrl = `https://digitalapi.${process.env['PLAYWRIGHT_ENV']!.replace('sys', 'test').toLowerCase()}.safelite.io/schedule/api/v1/schedule/shop-time-slots`; await this.page.route(apiUrl, async (route) => { - if (!fistInshopScheduleCall) { + /*if (!fistInshopScheduleCall) { return route.continue(); - } - - // const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; - // const startDate = new Date(); - //const beginningOfWeek = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() - startDate.getDay()); - // const endDate = new Date(beginningOfWeek.getFullYear(), beginningOfWeek.getMonth(), beginningOfWeek.getDate() + 13); - // e.g., "2025-07-23" - /*if (route.request().postDataJSON().startDate === fmt(startDate) && route.request().postDataJSON().endDate === fmt(endDate)) { - const response = await route.fetch(); - const responseBody = await response.json(); - - responseBody.days = []; - // Mock the response - await route.fulfill({ - response, - body: JSON.stringify(responseBody), - }); }*/ const response = await route.fetch(); const responseBody = await response.json(); - responseBody.days = []; + // Remove up to the first five items from the days array, keeping the rest; if empty, leave as is + if (Array.isArray(responseBody.days) && responseBody.days.length > 0) { + responseBody.days = responseBody.days.slice(5); + } else { + responseBody.days = []; + } // Mock the response await route.fulfill({ response, body: JSON.stringify(responseBody), }); - fistInshopScheduleCall = false; + // fistInshopScheduleCall = false; }); } @@ -252,9 +242,9 @@ export class BasePage { if (experiments !== undefined) { experimentsURLExtension += "?cns=all&experiments=" - experimentsURLExtension += experiments?.isMobileFirst + /*experimentsURLExtension += experiments?.isMobileFirst ? "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_TEST=true" - : "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true"; + : "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true";*/ experimentsURLExtension += experiments?.isAdyenPayments ? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)=true" @@ -263,11 +253,15 @@ export class BasePage { experimentsURLExtension += experiments?.isMultiLocationPopup ? ",MultiLocationPopup=MultiLocationPopup_V1=MultiLocationPopup_TEST=true" : ",MultiLocationPopup=MultiLocationPopup_V1=MultiLocationPopup_CONTROL=true"; + + experimentsURLExtension += experiments?.isMsrSplitPay + ? ",MSR=MSR_With_Splitpay=YesShowMSR_TEST=true" + : ",MSR=MSR_With_Splitpay=NoShowMSR_CONTROL=true"; } else { console.log("Url extension without query string"); } // Convert to HTML encoding before returning - return experimentsURLExtension; + return process.env.PLAYWRIGHT_ENV == 'sys' ? '' : experimentsURLExtension; } } \ No newline at end of file diff --git a/playwright-tests/pages/HomePage.ts b/playwright-tests/pages/HomePage.ts index 52ceca875..d4703c29c 100644 --- a/playwright-tests/pages/HomePage.ts +++ b/playwright-tests/pages/HomePage.ts @@ -21,7 +21,7 @@ export class HomePage extends BasePage { readonly viewQuoteButton: Locator; //Zip Entry - readonly widgetZipEntryField: Locator; + readonly widgetZipEntryInputField: Locator; readonly getQuoteAndScheduleButton: Locator; url = process.env['BASE_URL']!; @@ -30,7 +30,7 @@ export class HomePage extends BasePage { super(page); this.page = page; - this.letsGetStartedButton = this.page.getByRole('button', { name: 'Let\'s get started' }); + this.letsGetStartedButton = this.page.locator('a.zip-submit-button'); this.cusmodalPopup = this.page.locator('#Cusmodalpopup'); this.closePopupButton = this.page.getByRole('button', { name: '×' }); @@ -46,7 +46,7 @@ export class HomePage extends BasePage { this.viewQuoteButton = this.page.locator('#ctaSubmit'); //Zip Entry - this.widgetZipEntryField = this.page.getByRole('textbox', { name: 'Enter service ZIP code' }); + this.widgetZipEntryInputField = this.page.locator('input#zipcode'); this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' }); } @@ -61,7 +61,7 @@ export class HomePage extends BasePage { async letsGetStarted(zip: string, enterFunnelWithZip: boolean) { if (enterFunnelWithZip) { - await this.widgetZipEntryField.fill(zip); + await this.widgetZipEntryInputField.fill(zip); /* await this.letsGetStartedButton.evaluate((element, zip) => { const currentHref = element.getAttribute('href') || ''; element.setAttribute('href', `${currentHref}?zipCode=${zip}`); diff --git a/playwright-tests/pages/InsuranceDetailsPage.ts b/playwright-tests/pages/InsuranceDetailsPage.ts new file mode 100644 index 000000000..4b00e98a3 --- /dev/null +++ b/playwright-tests/pages/InsuranceDetailsPage.ts @@ -0,0 +1,61 @@ +import { type Locator, type Page } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { step } from 'framework/localTypes/Step'; +import { ITestData } from 'framework/TestData'; +import { ProgressBarPercentages } from 'framework/localTypes/Enums'; + +export class InsuranceDetailsPage extends BasePage { + + readonly page: Page; + readonly insuranceCompany: Locator; + readonly policyNumber: Locator; + readonly dateOfLoss: Locator; + readonly damageCause: Locator; + readonly claimNumber: Locator; // conditional — only shown for certain insurers like Kentucky Farm Bureau + readonly streetAddress: Locator; + readonly apartment: Locator; + readonly city: Locator; + readonly policyState: Locator; + readonly policyZip: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.insuranceCompany = page.getByLabel('Insurance company'); + this.policyNumber = page.getByLabel('Policy number'); + this.dateOfLoss = page.getByLabel('Approximate date of loss'); + this.claimNumber = page.getByLabel('Claim Number'); + this.damageCause = page.getByLabel('How did the damage occur?'); + this.streetAddress = page.getByLabel('Street address'); + this.apartment = page.getByLabel('Apt. number (optional)'); + this.city = page.getByLabel('City'); + this.policyState = page.getByLabel('State'); + this.policyZip = page.getByLabel('Zip'); + } + + private async populatePage(testData: Partial): Promise { + const { claimDetails, customerDetails } = testData; + + await this.policyNumber.fill(claimDetails!.policyNumber); + await this.dateOfLoss.fill(claimDetails!.damageDate); + await this.damageCause.selectOption(claimDetails!.damageCause); + await this.streetAddress.fill(customerDetails!.address.street); + await this.apartment.fill(customerDetails!.address.aptNumber ?? ''); + await this.city.fill(customerDetails!.address.city); + await this.policyState.selectOption(customerDetails!.address.state); + await this.policyZip.fill(customerDetails!.address.postalCode); + + if (await this.claimNumber.isVisible()) { + await this.claimNumber.fill(claimDetails!.claimNumber ?? ''); + } + } + + @step("InsuranceDetailsPage >> Fill out insurance details for non integrated insurance flow") + async handleInsuranceDetailsPage(testData: Partial): Promise { + await this.validateProgressBar(ProgressBarPercentages.InsuranceDetailsPage); + await this.populatePage(testData); + await this.nextPage(); + } + +} \ No newline at end of file diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 215ccff30..d0ef408a6 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -272,9 +272,10 @@ export class PaymentMethodPage extends BasePage { if (await this.payAtServiceButton.isVisible()) { await this.payAtServiceButton.click(); } else if (isRecalVehicle) { - await this.recalibrationCheckbox.click(); + if (await this.recalibrationCheckbox.isVisible()) { + await this.recalibrationCheckbox.click(); + } } - } async verifyVAPS(): Promise { @@ -559,8 +560,10 @@ export class PaymentMethodPage extends BasePage { await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage); await this.validatePaymentDetailsPage(testData); - await this.ValidateAfterPayBreakOutSection(); - + if (testData.isPIAEnabled) { + await this.ValidateAfterPayBreakOutSection(); + } + if (isForcedOEM) { await this.validateOEMPart(isForcedOEM); } diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index 650acc27a..1c4bef5b6 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -47,10 +47,10 @@ export class PaypalPage extends BasePage { await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true }); await this.usernameTextBox.fill(paymentDetails!.username!); await this.nextButton.click(); - if (!experiments!.isAdyenPayments){ + /*if (!experiments!.isAdyenPayments){ await this.tryAnotherWayButton.click(); await this.usePasswordInsteadButton.click(); - } + }*/ await this.passwordTextBox.fill(paymentDetails!.password!); await this.paypalLoginButton.click(); await this.payWithRadioButton.click(); diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 30fc830d0..8d863e94f 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -80,6 +80,15 @@ export class SchedulePage extends BasePage { async selectLocation(testData: Partial) { const { appointmentDetails, customerDetails } = testData; + if (testData.isMSRGlassPart || testData.isDualRecal) { + if (testData.isMSRZip) { + expect(await this.mobileButton.isVisible()).toBe(true); + } else { + expect(await this.mobileButton.isVisible()).toBe(false); + expect(await this.yellowAlertMessage.locator('.alert-heading').filter({ hasText: ' We\'re not able to provide mobile service.' }).isVisible()).toBe(true); + } + } + switch(appointmentDetails?.serviceLocation) { case ServiceLocation.Mobile: await this.scheduleMobile(testData); @@ -252,6 +261,12 @@ export class SchedulePage extends BasePage { await this.waitForPageOrComponentload(); await this.validateProgressBar(ProgressBarPercentages.SchedulePage); + //Looks like an issue with the insurance flow - remove comments once fixed + /*if (testData.isHeavyTruck) { + const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + expect(vuexState.order.lineItems.supportingItems.find((item: any) => item && item.partNumber == "labor2")).toBeTruthy(); + }*/ + if (handleMobileFirstModal) { return await this.handleMobileFirstPopUp(testData); } diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 2f16cb7b4..8fbc22a87 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -5,6 +5,7 @@ import { ProgressBarPercentages } from 'framework/localTypes/Enums'; import { PaymentMethod } from "framework/localTypes/Enums"; import { step } from 'framework/localTypes/Step'; import { ITestData } from 'framework/TestData'; +import { PaymentMethodPage } from './PaymentMethodPage'; export class ServicePackagesPage extends BasePage { readonly page: Page; @@ -227,6 +228,32 @@ export class ServicePackagesPage extends BasePage { } } + async verifyGlassCashQuoteEvent() { + // Get displayed price text for Glass Only package (e.g. "$41.25in 4 interest-free payments\nor $164.99 in single payment") + const expectedGlassOnlyPrice = await this.glassOnlypackagePrice.innerText(); + + // Extract the single payment amount (e.g. 164.99) from the end of the string using regex + const singlePaymentRegex = /\$([\d,]+\.\d{2})\s*in single payment/; + const match = expectedGlassOnlyPrice.match(singlePaymentRegex); + if (!match) { + throw new Error(`Expected single payment price to be present in: ${expectedGlassOnlyPrice}`); + } + const expectedSinglePayAmount = parseFloat(match[1].replace(',', '')); + + // Retrieve the dataLayer from the browser. (Assumes dataLayer is in global scope. Adjust if needed.) + const dataLayer = await this.page.evaluate(() => (window as any).dataLayer); + + // Find the first object with a GlassCashQuote property and extract its value. + const glassCashQuote = dataLayer?.find((item: any) => item && item.GlassCashQuote)?.GlassCashQuote; + if (glassCashQuote === undefined || glassCashQuote === null) { + throw new Error("No GlassCashQuote found in dataLayer"); + } + + // Validate that the GlassCashQuote value matches the extracted expectedSinglePayAmount (with currency float precision) + Soft.expect(Number.parseFloat(glassCashQuote)).toBe(expectedSinglePayAmount); + + } + @step("ServicePackagePage >> Select Payment Method and Service Type: ") async handleServicePackagePage(testData: Partial) { const { customerDetails, paymentMethod, paymentDetails, servicePackage, isCanNotRecal, isDynamicRecal, hasOemEndorsement, vehicleDamage, appointmentDetails, totalAmount } = testData; @@ -289,6 +316,10 @@ export class ServicePackagesPage extends BasePage { await this.verifyVehicleParts(vehicleDamage!); } + if (testData.paymentMethod == PaymentMethod.SelfPay) { + await this.verifyGlassCashQuoteEvent(); + } + // Validate backend for OEM endorsement if (hasOemEndorsement) { await this.verifyOEMPart(); diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 026f5b2d9..f5295bc1c 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -100,7 +100,8 @@ export default defineConfig({ headless: process.env.CI ? true : false, screenshot: "only-on-failure", actionTimeout: 60_000, - navigationTimeout: 60_000 + navigationTimeout: 60_000, + bypassCSP: true }, /* Configure projects for major browsers */ diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 0dcb5155e..fbeccd97d 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -19,12 +19,14 @@ import { ApiResponseInterceptUtil } from 'safelite-playwright-core'; import cashReplaceGlassPromoInshopTests from "./CashReplaceGlassPromoInshop"; import cashReplaceMultiGlassPromoInshopTests from "./CashReplaceMultiGlassPromoInshop"; import cashReplaceStaticInshopTests from "./CashReplaceStaticInshop"; +import cashReplaceStaticMobileMSRTests from "./CashReplaceStaticMobileMSR"; import cashReplaceRainDefensePromoInshopTests from "./CashReplaceRainDefensePromoInshop"; import cashReplaceSafeliteCanNotRecalMobileTests from "./CashReplaceSafeliteCanNotRecalMobile"; import cashReplaceVinMobileTests from "./CashReplaceVinMobile"; import cashReplaceWiperDropoffTests from "./CashReplaceWiperDropoff"; import cashReplaceWiperPromoInShopTests from "./CashReplaceWiperPromoInshop"; import insuranceAcuityPaypalTests from "./InsuranceAcuityPaypal"; +// import insuranceUSAAMsrTests from "./InsuranceUSAAMsr"; import insuranceITAC21stCenturyTests from "./InsuranceITAC21stCentury"; import insuranceGeicoTests from "./InsuranceGeico"; import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState"; @@ -38,10 +40,14 @@ import cashReplaceMultiGlassMobileTests from "./CashReplaceMultiGlassMobile"; import { getTestObject, TestCase, prepareTest, RuleEngine, TestInfo } from 'framework/Typedefs'; import { createTestPages } from "framework/TestPages"; import cashReplaceSwitchToInsuranceProgressiveNoCompTests from "./CashReplaceSwitchToInsuranceProgressiveNoComp"; -import insuranceBigTruckVerifiedTests from "./InsuranceBigTruckVerified"; +import insuranceUSAABigTruckVerifiedTests from "./InsuranceUSAABigTruckVerified"; +import partsNotFoundBailoutTests from "./PartsNotFoundBailout"; import insuranceUnverifiedTests from "./InsuranceUnverified"; import CashReplaceSplitWindshieldTests from "./CashReplaceSplitWindshield"; import insuranceMeemicNearSchoolVerifiedTests from "./InsuranceMeemicNearSchoolVerified"; +import cashReplaceDualMobileMSRTests from "./CashReplaceDualMobileMSR"; +import cashReplaceDualNonMSRInshopTests from "./CashReplaceDualNonMSRInshop"; + const test = getTestObject(); @@ -78,21 +84,25 @@ const allStandardTests = [ { name: "CashReplaceVinMobile", tests: cashReplaceVinMobileTests }, { name: "CashReplaceWiperDropoff", tests: cashReplaceWiperDropoffTests }, { name: "CashReplaceStaticInshop", tests: cashReplaceStaticInshopTests }, + { name: "CashReplaceDualMobileMSR", tests: cashReplaceDualMobileMSRTests }, + { name: "CashReplaceDualNonMSRInshop", tests: cashReplaceDualNonMSRInshopTests }, + { name: "CashReplaceStaticMobileMSR", tests: cashReplaceStaticMobileMSRTests }, { name: "CashReplaceWiperPromoInshop", tests: cashReplaceWiperPromoInShopTests }, { name: "CashReplaceSwitchToInsuranceProgressiveNoComp", tests: cashReplaceSwitchToInsuranceProgressiveNoCompTests }, - { name: "CashReplaceMobileFirstModal", tests: cashReplaceMobileFirstModalTests }, + // { name: "CashReplaceMobileFirstModal", tests: cashReplaceMobileFirstModalTests }, // TODO: Uncomment when QA is ready to run heavy truck tests // {name: "CashReplaceSplitWindshield", tests: CashReplaceSplitWindshieldTests}, { name: "InsuranceAcuityPaypal", tests: insuranceAcuityPaypalTests }, + // { name: "InsuranceUSAAMsr", tests: insuranceUSAAMsrTests }, { name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests }, { name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests }, - // {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests}, + {name: "InsuranceUSAABigTruckVerified", tests: insuranceUSAABigTruckVerifiedTests}, { name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests }, { name: "InsuranceUnverified", tests: insuranceUnverifiedTests }, // {name: "InsuranceGeico", tests: insuranceGeicoTests}, // {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests} { name: "InsuranceMeemicNearSchoolVerified", tests: insuranceMeemicNearSchoolVerifiedTests }, - + // { name: "PartsNotFoundBailout", tests: partsNotFoundBailoutTests }, // code is not available in QA ]; // Alert validation scenarios @@ -266,6 +276,14 @@ async function runWorkflow(page: Page, testCase: TestCase) { await serviceZipPage.handleServiceZipPage(testCase.testData); } + // Handle bailout page if parts bailout scenario + if (testCase.testData.bailoutFlags?.isVehicleLookupBailout) { + let bailoutPage = testCase.pages.bailoutPage; + await bailoutPage.handleBailoutPage(testCase.testData); + let bailoutSuccessPage = testCase.pages.bailoutSuccessPage; + await bailoutSuccessPage.handleBailoutSuccessPage(); + } + // Handle part questions if applicable if (partQuestions && partQuestions.length > 0) { let partQuestionsPage = testCase.pages.partQuestionsPage; diff --git a/playwright-tests/tests/CashRepairInShopAfterPay.ts b/playwright-tests/tests/CashRepairInShopAfterPay.ts index dc34404e5..71bce3f25 100644 --- a/playwright-tests/tests/CashRepairInShopAfterPay.ts +++ b/playwright-tests/tests/CashRepairInShopAfterPay.ts @@ -38,7 +38,8 @@ const cashRepairInShopAfterPayData : Partial = { // Experiments experiments: { - ...getDefaultExperimentsData() + ...getDefaultExperimentsData(), + isAdyenPayments: true } } diff --git a/playwright-tests/tests/CashReplaceDualMobileMSR.ts b/playwright-tests/tests/CashReplaceDualMobileMSR.ts new file mode 100644 index 000000000..454f4d2a8 --- /dev/null +++ b/playwright-tests/tests/CashReplaceDualMobileMSR.ts @@ -0,0 +1,99 @@ +//Imports here +import { ITestData, getDefaultExperimentsData } from 'framework/TestData' +import { ServicePackage, PaymentType, PartQuestionType, ServiceLocation } from 'safelite-playwright-core'; +import { ITestCase } from '../framework/Typedefs' +import { VehicleLookupType } from 'safelite-playwright-core'; +import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; +import { PaymentMethod } from 'framework/localTypes/Enums'; + +// Set the seed before generating any data +setFakerSeedFromTestName("CashReplaceDualMobileMSR"); + +// Now get the test data with the seeded faker +const CashReplaceDualMobileMSRData: Partial = { + ...getDefaultTestData(), // Get default data with current seed + + // CASH Client + paymentMethod: PaymentMethod.SelfPay, + + // Key feature: Premium package with Rain Defense + servicePackage: ServicePackage.Standard, + + // Flag for recalibration vehicle + isRecalVehicle: true, + + // Override customer details + customerDetails: { + ...getDefaultTestData().customerDetails!, + address: { + ...getDefaultTestData().customerDetails!.address, + postalCode: '43085' + // postalCode: '21237' + } + }, + + // Override appointment details + appointmentDetails: { + serviceLocation: ServiceLocation.Mobile, + appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate, + serviceAddress: { + // Use street address from current faker seed + street: "649 High Street", + city: 'Worthington', + state: 'OH', + postalCode: '43085', // + country: 'United States' + }, + }, + + // override part questions + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + } + ], + + + // Override vehicle details + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '2020', + make: 'Honda', + model: 'Pilot', + style: '4 door utility', + vehicleLookupType: VehicleLookupType.Zip + }, + + mockFirstInshopCallNoSchedule: true, + + isMSRGlassPart: true, + isMSRZip: true, + + // No need to override vehicleDamage as it already defaults to WindshieldCrack + + // Payment at service + paymentDetails: { + paymentType: PaymentType.PayAtService + }, + + // Flag for PIA disabled + isPIAEnabled: false, + + // Experiments + experiments: { + ...getDefaultExperimentsData() + } +} + +const cashReplaceDualMobileMSRTests: ITestCase[] = []; + +const tc = { + name: `CashReplaceDualMobileMSR`, + tags: ['@E2E', '@CashReplaceDualMobileMSR', '@test_report', '@CASH'], + testData: CashReplaceDualMobileMSRData +}; +cashReplaceDualMobileMSRTests.push(tc); + +export default cashReplaceDualMobileMSRTests; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceDualNonMSRInshop.ts b/playwright-tests/tests/CashReplaceDualNonMSRInshop.ts new file mode 100644 index 000000000..8abea0f00 --- /dev/null +++ b/playwright-tests/tests/CashReplaceDualNonMSRInshop.ts @@ -0,0 +1,82 @@ +//Imports here +import { ITestData, getDefaultExperimentsData } from 'framework/TestData' +import { ServicePackage, PaymentType, PartQuestionType, ServiceLocation } from 'safelite-playwright-core'; +import { ITestCase } from '../framework/Typedefs' +import { VehicleLookupType } from 'safelite-playwright-core'; +import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; +import { PaymentMethod } from 'framework/localTypes/Enums'; + +// Set the seed before generating any data +setFakerSeedFromTestName("CashReplaceDualNonMSRInshop"); + +// Now get the test data with the seeded faker +const CashReplaceDualMobileMSRData: Partial = { + ...getDefaultTestData(), // Get default data with current seed + + // CASH Client + paymentMethod: PaymentMethod.SelfPay, + + // Key feature: Premium package with Rain Defense + servicePackage: ServicePackage.Standard, + + // Flag for recalibration vehicle + isRecalVehicle: true, + + // Override customer details + customerDetails: { + ...getDefaultTestData().customerDetails!, + address: { + ...getDefaultTestData().customerDetails!.address, + postalCode: '43085' + // postalCode: '21237' + } + }, + + // override part questions + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + } + ], + + // Override vehicle details + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '2024', + make: 'Subaru', + model: 'Ascent', + style: '4 door utility', + vehicleLookupType: VehicleLookupType.Zip + }, + + mockFirstInshopCallNoSchedule: true, + + isMSRGlassPart: false, + isMSRZip: true, + + // Payment at service + paymentDetails: { + paymentType: PaymentType.PayAtService + }, + + // Flag for PIA disabled + isPIAEnabled: false, + + // Experiments + experiments: { + ...getDefaultExperimentsData() + } +} + +const cashReplaceDualNonMSRInshopTests: ITestCase[] = []; + +const tc = { + name: `CashReplaceDualNonMSRInshop`, + tags: ['@E2E', '@CashReplaceDualNonMSRInshop', '@test_report', '@CASH'], + testData: CashReplaceDualMobileMSRData +}; +cashReplaceDualNonMSRInshopTests.push(tc); + +export default cashReplaceDualNonMSRInshopTests; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts index 50f885f96..6feb3c651 100644 --- a/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts +++ b/playwright-tests/tests/CashReplaceGlassAddressLookupInshopAfterPay.ts @@ -35,10 +35,10 @@ const cashReplaceGlassAddressLookupInshopAfterPayData: Partial = { // Override vehicle details vehicleDetails: { ...getDefaultTestData().vehicleDetails!, - year: '2013', - make: 'Hyundai', - model: 'Sonata', - style: '4 door sedan', + year: '2024', + make: 'Acura', + model: 'MDX', + style: '4 door utility', vehicleLookupType: VehicleLookupType.Address }, @@ -48,7 +48,7 @@ const cashReplaceGlassAddressLookupInshopAfterPayData: Partial = { // Experiments experiments: { ...getDefaultExperimentsData() - } + } } const cashReplaceGlassAddressLookupInshopAfterPayTests: ITestCase[] = []; diff --git a/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts b/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts index 89406bc29..c0ca40afa 100644 --- a/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts +++ b/playwright-tests/tests/CashReplaceMultiGlassPromoInshop.ts @@ -35,9 +35,12 @@ const cashReplaceMultiGlassPromoInshopData: Partial = { make: 'Subaru', model: 'Outback', style: '4 door station wagon', - vin: '4S4BSENC4K3221004', - vehicleLookupType: VehicleLookupType.Vin + // vin: '4S4BSENC4K3221004', + vehicleLookupType: VehicleLookupType.Zip }, + + // Special flag to skip estimate page + isSkipEstimatePage: true, // Key feature: multiple damaged glasses vehicleDamage: [ @@ -55,6 +58,15 @@ const cashReplaceMultiGlassPromoInshopData: Partial = { // Add promo code promoCode: '20CALL', }, + + // override part questions + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + } + ], // Vehicle part questions for multiple glass parts vehiclePartQuestions: [ @@ -62,7 +74,7 @@ const cashReplaceMultiGlassPromoInshopData: Partial = { partQuestionType: PartQuestionType.WindshieldColor, isOnPage: true, optionToSelect: 'Green Tint, Blue Shade', - secondaryQuestionOptionToSelect: 'solar, lane departure warning system, heated glass wiper park, high beam assist, soundproofing' + secondaryQuestionOptionToSelect: 'solar, lane departure warning system, hwp, high beam assist, soundproofing' }, { partQuestionType: PartQuestionType.DriverFrontColor, diff --git a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts index c687861a8..67b8ab76e 100644 --- a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts +++ b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts @@ -85,7 +85,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial = { partQuestionType: PartQuestionType.WindshieldColor, isOnPage: true, optionToSelect: 'Green Tint', - secondaryQuestionOptionToSelect: 'rain sensor, solar, soundproofing, third visor frit, lane departure warning system, heated glass wiper park, w/combination bracket' + secondaryQuestionOptionToSelect: 'rain sensor, solar, soundproofing, third visor frit, lane departure warning system, hwp, w/combination bracket' }, { partQuestionType: PartQuestionType.PassengerFrontColor, diff --git a/playwright-tests/tests/CashReplaceStaticInshop.ts b/playwright-tests/tests/CashReplaceStaticInshop.ts index 107b625a1..f7304ca46 100644 --- a/playwright-tests/tests/CashReplaceStaticInshop.ts +++ b/playwright-tests/tests/CashReplaceStaticInshop.ts @@ -15,19 +15,26 @@ const CashReplaceStaticInshopData: Partial = { // CASH Client paymentMethod: PaymentMethod.SelfPay, - + // Key feature: Premium package with Rain Defense servicePackage: ServicePackage.Standard, - + // Flag for recalibration vehicle isRecalVehicle: true, - + + // Flag for MSR Glass Part + isMSRGlassPart: true, + + // Flag for MSR Zip + isMSRZip: false, + // Override customer details customerDetails: { ...getDefaultTestData().customerDetails!, address: { ...getDefaultTestData().customerDetails!.address, - postalCode: '43085' + postalCode: '55113' // Non MSR zip + // postalCode: '21237' } }, @@ -37,34 +44,32 @@ const CashReplaceStaticInshopData: Partial = { partQuestionType: PartQuestionType.GeneralQuestion1, isOnPage: true, optionToSelect: 'Yes' - }, - { - partQuestionType: PartQuestionType.GeneralQuestion2, - isOnPage: true, - optionToSelect: 'Yes' } ], - + + // Override vehicle details vehicleDetails: { ...getDefaultTestData().vehicleDetails!, - year: '2022', - make: 'Mazda', - model: 'CX-9', - style: '4 door utility', - vin: '3FA6P0HD8LR234510', + year: '2023', + make: 'Hyundai', + model: 'Elantra', + style: '4 door sedan', vehicleLookupType: VehicleLookupType.Zip }, mockFirstInshopCallNoSchedule: true, - + // No need to override vehicleDamage as it already defaults to WindshieldCrack - + // Payment at service paymentDetails: { paymentType: PaymentType.PayAtService }, + // Flag for PIA disabled + isPIAEnabled: false, + // Experiments experiments: { ...getDefaultExperimentsData() @@ -75,7 +80,7 @@ const cashReplaceStaticInshopTests: ITestCase[] = []; const tc = { name: `CashReplaceStaticInshop`, - tags: ['@E2E','@CashReplaceStaticInshop', '@test_report', '@CASH'], + tags: ['@E2E', '@CashReplaceStaticInshop', '@test_report', '@CASH'], testData: CashReplaceStaticInshopData }; cashReplaceStaticInshopTests.push(tc); diff --git a/playwright-tests/tests/CashReplaceStaticMobileMSR.ts b/playwright-tests/tests/CashReplaceStaticMobileMSR.ts new file mode 100644 index 000000000..f2839e101 --- /dev/null +++ b/playwright-tests/tests/CashReplaceStaticMobileMSR.ts @@ -0,0 +1,92 @@ +//Imports here +import { ITestData, getDefaultExperimentsData } from 'framework/TestData' +import { ServicePackage, PaymentType, PartQuestionType, ServiceLocation } from 'safelite-playwright-core'; +import { ITestCase } from '../framework/Typedefs' +import { VehicleLookupType } from 'safelite-playwright-core'; +import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; +import { PaymentMethod } from 'framework/localTypes/Enums'; + +// Set the seed before generating any data +setFakerSeedFromTestName("CashReplaceStaticMobileMSR"); + +// Now get the test data with the seeded faker +const CashReplaceStaticMobileMSRData: Partial = { + ...getDefaultTestData(), // Get default data with current seed + + // CASH Client + paymentMethod: PaymentMethod.SelfPay, + + // Key feature: Premium package with Rain Defense + servicePackage: ServicePackage.Standard, + + // Flag for recalibration vehicle + isRecalVehicle: true, + + // Override customer details + customerDetails: { + ...getDefaultTestData().customerDetails!, + address: { + ...getDefaultTestData().customerDetails!.address, + postalCode: '43085' + // postalCode: '21237' + } + }, + + // override part questions + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + } + ], + + // Override vehicle details + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '2023', + make: 'Hyundai', + model: 'Elantra', + style: '4 door sedan', + vin: '3FA6P0HD8LR234510', + vehicleLookupType: VehicleLookupType.Zip + }, + + mockFirstInshopCallNoSchedule: true, + + // No need to override vehicleDamage as it already defaults to WindshieldCrack + // Override appointment details + appointmentDetails: { + serviceLocation: ServiceLocation.Mobile, + appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate, + serviceAddress: { + // Use street address from current faker seed + street: "649 High Street", + city: 'Worthington', + state: 'OH', + postalCode: '43085', // + country: 'United States' + }, + }, + + // Payment at service + paymentDetails: { + paymentType: PaymentType.PayAtService + }, + + // Experiments + experiments: { + ...getDefaultExperimentsData() + } +} + +const cashReplaceStaticMobileMSRTests: ITestCase[] = []; + +const tc = { + name: `cashReplaceStaticMobileMSR`, + tags: ['@E2E','@cashReplaceStaticMobileMSR', '@test_report', '@CASH'], + testData: CashReplaceStaticMobileMSRData +}; +cashReplaceStaticMobileMSRTests.push(tc); + +export default cashReplaceStaticMobileMSRTests; \ No newline at end of file diff --git a/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts b/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts index 98c965b0c..7261cc238 100644 --- a/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts +++ b/playwright-tests/tests/CashReplaceSwitchToInsuranceProgressiveNoComp.ts @@ -37,6 +37,8 @@ const cashReplaceSwitchToInsuranceProgressiveNoCompData: Partial = { style: "4 door sedan" }, + isSkipEstimatePage: true, + partQuestions: [ { partQuestionType: PartQuestionType.GeneralQuestion1, diff --git a/playwright-tests/tests/InsuranceBigTruckVerified.ts b/playwright-tests/tests/InsuranceUSAABigTruckVerified.ts similarity index 81% rename from playwright-tests/tests/InsuranceBigTruckVerified.ts rename to playwright-tests/tests/InsuranceUSAABigTruckVerified.ts index 3e616230e..74566a535 100644 --- a/playwright-tests/tests/InsuranceBigTruckVerified.ts +++ b/playwright-tests/tests/InsuranceUSAABigTruckVerified.ts @@ -1,16 +1,16 @@ //Imports here import { ITestData, getDefaultExperimentsData } from 'framework/TestData' -import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core'; +import { ServiceLocation, DamageType, Flow} from 'safelite-playwright-core'; import { ITestCase } from '../framework/Typedefs' import { PaymentMethod } from "framework/localTypes/Enums"; import { VehicleLookupType } from 'safelite-playwright-core'; import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; // Set the seed based on test name for consistent but unique data -setFakerSeedFromTestName("InsuranceBigTruckVerified"); +setFakerSeedFromTestName("InsuranceUSAABigTruckVerified"); // Now get the test data with the seeded faker -const insuranceBigTruckVerifiedData: Partial = { +const insuranceUSAABigTruckVerifiedData: Partial = { ...getDefaultTestData(), // Get default data with current seed // Key feature: Insurance flow with GEICO @@ -19,6 +19,8 @@ const insuranceBigTruckVerifiedData: Partial = { // Insurance claim flags isDuplicateClaim: true, isPolicyFound: true, + flow: Flow.Managed, + isCanNotRecal: true, isUseVehicleOnPolicy: true, isHeavyTruck: true, @@ -62,7 +64,7 @@ const insuranceBigTruckVerifiedData: Partial = { // Override for in-shop appointment appointmentDetails: { serviceLocation: ServiceLocation.InShop, - shopAddress: '5719 Brandt Pike, Dayton, OH 45424', + shopAddress: '3455 Centerpoint Dr, Urbancrest, OH 43123', appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate }, @@ -75,13 +77,13 @@ const insuranceBigTruckVerifiedData: Partial = { } } -const insuranceBigTruckVerifiedTests: ITestCase[] = []; +const insuranceUSAABigTruckVerifiedTests: ITestCase[] = []; const tc = { - name: `InsuranceBigTruckVerified`, + name: `InsuranceUSAABigTruckVerified`, tags: ['@E2E','@InsuranceBigTruckVerified', '@test_report', '@Insurance'], - testData: insuranceBigTruckVerifiedData + testData: insuranceUSAABigTruckVerifiedData }; -insuranceBigTruckVerifiedTests.push(tc); +insuranceUSAABigTruckVerifiedTests.push(tc); -export default insuranceBigTruckVerifiedTests; \ No newline at end of file +export default insuranceUSAABigTruckVerifiedTests; \ No newline at end of file diff --git a/playwright-tests/tests/InsuranceUSAAMsr.ts b/playwright-tests/tests/InsuranceUSAAMsr.ts new file mode 100644 index 000000000..052fdc387 --- /dev/null +++ b/playwright-tests/tests/InsuranceUSAAMsr.ts @@ -0,0 +1,101 @@ +//Imports here +import { ITestData, getDefaultExperimentsData } from 'framework/TestData' +import { ServiceLocation, DamageType, PartQuestionType, VehicleDamage, PaymentType, Flow } from 'safelite-playwright-core'; +import { PaymentMethod } from "framework/localTypes/Enums"; +import { ITestCase } from '../framework/Typedefs' +import { VehicleLookupType } from 'safelite-playwright-core'; +import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; + +// Set the seed based on test name for consistent but unique data +setFakerSeedFromTestName("InsuranceUSAAMsr"); + +// Now get the test data with the seeded faker +const insuranceUSAAMsrData: Partial = { + ...getDefaultTestData(), // Get default data with current seed + + // Key feature: Insurance flow with Acuity + paymentMethod: PaymentMethod.Insurance, + + // Insurance claim flags + isDuplicateClaim: true, + flow: Flow.Managed, + isUseVehicleOnPolicy: true, + + // Override customer details for Kentucky location + customerDetails: { + ...getDefaultTestData().customerDetails!, + address: { + street: getDefaultTestData().customerDetails!.address.street, + city: 'Cleaveland', + state: 'Ohio', + postalCode: '44125', + country: 'United States' + } + }, + + // Insurance claim details + claimDetails: { + client: 'USAA', + policyNumber: 'MOCK900040MSR', + policyDeductible: 100.00, + damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}), + damageCause: DamageType.Rock + }, + + // Heavy duty truck details with VIN lookup + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '2023', + make: 'Hyundai', + model: 'Elantra', + style: '4 door sedan' + }, + + isRecalVehicle: true, + + // No need to override vehicleDamage as it already defaults to WindshieldCrack + + // Override for mobile service + appointmentDetails: { + serviceLocation: ServiceLocation.Mobile, + appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate, + serviceAddress: { + // Use street address from current faker seed + street: "13300 Carpenter Rd", + city: 'Cleveland', + state: 'OH', + postalCode: '44125', + country: 'United States' + } + }, + + // Part questions for windshield + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + }, + ], + + // Override payment details (empty because we skip payment method page in insurance flow) + paymentDetails: { + paymentType: PaymentType.PayAtService + }, + + // Experiments + experiments: { + ...getDefaultExperimentsData() + } +} + +const insuranceUSAAMsrTests: ITestCase[] = []; + +const tc = { + name: `InsuranceUSAAMsr`, + tags: ['@E2E','@InsuranceAcuityPaypal', '@test_report', '@Insurance', '@CASH-1187', '@CASH-848'], + testData: insuranceUSAAMsrData +}; +insuranceUSAAMsrTests.push(tc); + +export default insuranceUSAAMsrTests; \ No newline at end of file diff --git a/playwright-tests/tests/PartsNotFoundBailout.ts b/playwright-tests/tests/PartsNotFoundBailout.ts new file mode 100644 index 000000000..1ec10c36c --- /dev/null +++ b/playwright-tests/tests/PartsNotFoundBailout.ts @@ -0,0 +1,52 @@ +import { ITestData, getDefaultExperimentsData } from 'framework/TestData'; +import { VehicleDamage, VehicleLookupType } from 'safelite-playwright-core'; +import { ITestCase } from '../framework/Typedefs'; +import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; + +setFakerSeedFromTestName("CashDiamondReoBailout"); + +const partsNotFoundBailoutTestData: Partial = { + ...getDefaultTestData(), + + isHeavyTruck: true, + + customerDetails: { + ...getDefaultTestData().customerDetails!, + address: { + ...getDefaultTestData().customerDetails!.address, + postalCode: '43085' + } + }, + + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '1974', + make: 'Diamond Reo', + model: 'CF65', + style: 'cabover', + vehicleLookupType: VehicleLookupType.Zip, + }, + + vehicleDamage: [ + VehicleDamage.WindshieldCrack + ], + + bailoutFlags: { + isVehicleLookupBailout: true + }, + + experiments: { + ...getDefaultExperimentsData() + } +}; + +const partsNotFoundBailoutTests: ITestCase[] = []; + +const tc = { + name: `PartsNotFoundBailout`, + tags: ['@E2E', '@PartsNotFoundBailout', '@test_report', '@Bailout'], + testData: partsNotFoundBailoutTestData +}; +partsNotFoundBailoutTests.push(tc); + +export default partsNotFoundBailoutTests; diff --git a/src/constants/bailout-codes.js b/src/constants/bailout-codes.js new file mode 100644 index 000000000..af6290363 --- /dev/null +++ b/src/constants/bailout-codes.js @@ -0,0 +1,5 @@ +const bailoutCodes = { + PARTS_NOT_FOUND: 10, +}; + +export { bailoutCodes }; diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 9adfc0613..efb32ffc6 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -25,7 +25,7 @@ const endpoints = { method: "GET", }, GetDamageOptions: { - url: "/parts/api/v1/parts/damage-options", + url: "/parts/api/v2/parts/damage-options", method: "GET", }, LookupVehicleByYmms: { diff --git a/src/constants/insurance.js b/src/constants/insurance.js index 5987eb11f..8f8cd80fd 100644 --- a/src/constants/insurance.js +++ b/src/constants/insurance.js @@ -82,3 +82,6 @@ export function coverageTypeEnum(strCoverageType) { export const parentAccountNumbers = { CONNECT: "560636", }; +export const NON_MANAGED_SHOW_CLAIM_NUMBER_PARENTS = [ + { name: "KentuckyFarmBureau", value: "223499" }, +]; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 702f7c737..a50ff4bb9 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -123,6 +123,7 @@ const storeActions = { CREATE_SUBMITTED_STATE: "createSubmittedState", RESET_SUBMITTED_STATE: "resetSubmittedState", + RESET_INSURANCE_DETAILS: "resetInsuranceDetails", ADD_DONATION_TO_SUBMITTED_STATE: "addDonationToSubmittedState", RESET_EXTERNAL_PARAMETER_STATE: "resetExternalParameterState", UPDATE_EXTERNAL_PARAMETER_MMS: "updateExternalParameterMMS", @@ -131,6 +132,8 @@ const storeActions = { UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey", CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry", + + SAVE_BAILOUT_CODE: "saveBailoutCode", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index c43e737d3..d2fc2b0fc 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -100,6 +100,7 @@ const storeMutations = { RESET_SERVICE_LOCATION_MOBILE_ADDRESS: "resetServiceLocationMobileAddress", RESET_SCHEDULE: "resetSchedule", RESET_PAYMENT_METHOD_CHOICE: "resetPaymentMethodChoice", + RESET_INSURANCE_DETAILS: "resetInsuranceDetails", // OTHER MUTATIONS UPDATE_PAGE_DATA: "updatePageData", @@ -114,6 +115,9 @@ const storeMutations = { UPDATE_EXPERIMENTS: "updateExperiments", UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry", + // BAILOUT MUTATIONS + UPDATE_BAILOUT_CODE: "updateBailoutCode", + // EXTERNAL_PARAMETER MUTATIONS UPDATE_IS_EXTERNAL_PARAMETER: "updateIsExternalParameter", UPDATE_EXTERNAL_PARAMETER_YEAR: "updateExternalParameterYear", diff --git a/src/digital-components/date-picker-popup/date-picker-popup.vue b/src/digital-components/date-picker-popup/date-picker-popup.vue index 3c666e775..7f3e109ac 100644 --- a/src/digital-components/date-picker-popup/date-picker-popup.vue +++ b/src/digital-components/date-picker-popup/date-picker-popup.vue @@ -27,6 +27,7 @@ :max-date="maxDate" :auto-apply="autoApply" :text-input="textInput" + :prevent-min-max-navigation="preventMinMaxNavigation" :teleport="true" :aria-labels="{ input: questionText }" @update:model-value="onDateChange" @@ -106,6 +107,10 @@ export default { type: Boolean, default: false, }, + preventMinMaxNavigation: { + type: Boolean, + default: false, + }, questionAlignment: String, labelBold: { type: Boolean, diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 9a6f55576..faa28376d 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -962,7 +962,8 @@ export default { return this.supportingItems.find( (lineItem) => lineItem.partType == partTypeStrings.MOBILE_FEE && - lineItem.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE + (lineItem.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE || + lineItem.partNumber == partNumberStrings.MOBILE_DUAL_RECAL_FEE) ); }, msrFeeCartItem() { diff --git a/src/helpers/damage-helper.js b/src/helpers/damage-helper.js index 1763ffbea..5c7a2de7c 100644 --- a/src/helpers/damage-helper.js +++ b/src/helpers/damage-helper.js @@ -103,7 +103,7 @@ export function getSideDoorGlassString(glassLocation, glassName) { export function getDamageInfoWordingText(damageInfo) { const glassTextArray = []; - damageInfo.glassToReplace.forEach((item) => { + damageInfo.glassToReplace?.forEach((item) => { let string = ""; if (item.glassLocation === glassLocations.WINDSHIELD) { string = item.glassLocation; diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index a3dc363f5..613f239f2 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -106,6 +106,14 @@ export async function saveQuote({ pageNameToLog }) { return; } +export async function submitBailout({ pageNameToLog }) { + await saveSession({ + pageNameToLog: pageNameToLog, + shouldAwaitSaveSessionQueue: true, + submitAfterSave: false, + }); +} + // PRIVATE FUNCTIONS // /* diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js index 9564b5c1f..fff87d82f 100644 --- a/src/helpers/pricing-helper.js +++ b/src/helpers/pricing-helper.js @@ -108,7 +108,9 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) { export function getMSRFeePartPrice(supportingItems, includeTax) { let msrFeePrice = 0; const msrFeeLineItem = supportingItems?.find( - (lineItem) => lineItem.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE + (lineItem) => + lineItem.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE || + lineItem.partNumber === partNumberStrings.MOBILE_DUAL_RECAL_FEE ); if (msrFeeLineItem) { msrFeePrice = baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts( diff --git a/src/layouts/bailout-success/bailout-success.spec.js b/src/layouts/bailout-success/bailout-success.spec.js new file mode 100644 index 000000000..5d5f73f14 --- /dev/null +++ b/src/layouts/bailout-success/bailout-success.spec.js @@ -0,0 +1,56 @@ +// Components +import bailoutSuccess from "@/layouts/bailout-success/bailout-success.vue"; + +// Supporting Files +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +// Mock fetchCmsContentForPage +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: jest.fn(), +})); + +describe("bailout-success.vue", () => { + test("renders funnelHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true); + }); + + test("renders funnelSubHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true); + }); + + test("renders Form component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true); + }); + + test("renders buttonMain component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "buttonMain" }).exists()).toBe(true); + }); +}); + +function setupMocks() { + const mountOptions = getMountOptions({}); + + //Mock props + const mockMixin = { + methods: { + getCmsContent: jest.fn(), + }, + }; + + mountOptions.mixins = [mockMixin]; + const wrapper = shallowMount(bailoutSuccess, mountOptions); + + wrapper.vm.setCmsContent = jest.fn(); + + return { wrapper }; +} diff --git a/src/layouts/bailout-success/bailout-success.vue b/src/layouts/bailout-success/bailout-success.vue new file mode 100644 index 000000000..97f186ca6 --- /dev/null +++ b/src/layouts/bailout-success/bailout-success.vue @@ -0,0 +1,91 @@ + + + diff --git a/src/layouts/bailout/bailout.spec.js b/src/layouts/bailout/bailout.spec.js index ebed2697a..905f9e5a0 100644 --- a/src/layouts/bailout/bailout.spec.js +++ b/src/layouts/bailout/bailout.spec.js @@ -16,6 +16,26 @@ jest.mock("@/helpers/cms-content-helper", () => ({ })); describe("bailout.vue", () => { + test("renders funnelHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true); + }); + + test("renders funnelSubHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true); + }); + + test("renders Form component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true); + }); + + test("renders navbar component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "navbar" }).exists()).toBe(true); + }); + test("arePagePrerequisitesValid should be true ", async () => { //Arrange const { wrapper } = setupMocks(); diff --git a/src/layouts/bailout/bailout.vue b/src/layouts/bailout/bailout.vue index a64f7a416..ae5600e08 100644 --- a/src/layouts/bailout/bailout.vue +++ b/src/layouts/bailout/bailout.vue @@ -1,21 +1,80 @@ diff --git a/src/layouts/insurance-details/insurance-details.vue b/src/layouts/insurance-details/insurance-details.vue index 67670b9cc..a675e0871 100644 --- a/src/layouts/insurance-details/insurance-details.vue +++ b/src/layouts/insurance-details/insurance-details.vue @@ -39,7 +39,18 @@ format="MM/dd/yyyy" modelType="MM/dd/yyyy" :enableTimePicker="false" - :clearable="false" /> + :clearable="false" + :textInput="true" + :maxDate="today" + :preventMinMaxNavigation="true" /> + + item.value === order.payment?.parentAccountNumber?.toString() + ); + return !!parent; + }, stateOptions() { return stateOptions; }, @@ -231,6 +256,9 @@ export default { getPolicyZipFromStore() { return store.getters.order.policy.zipCode ?? ""; }, + getClaimNumberFromStore() { + return store.getters.order.policy.claimNumber ?? ""; + }, backButtonAction() { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK, @@ -247,6 +275,7 @@ export default { city: this.city, state: this.policyState, zipCode: this.policyZip, + claimNumber: this.claimNumber, }, false ); diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 5b3a663ea..fc3ad668d 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -35,6 +35,7 @@ :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" :isMSRFeeApplicable="isMSRFeeApplicable" + :IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance" :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> @@ -77,6 +78,7 @@ import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { Dropin } from "@adyen/adyen-web/auto"; import { applicationConfig } from "@/constants/application-config"; import { paymentMethods } from "@/constants/payment-method-constants"; +import analyticsMixin from "@/mixins/analytics-mixin"; import "@adyen/adyen-web/styles/adyen.css"; import { mapAdyenToFmgPaymentMethod, mapFmgToAdyenPaymentMethod } from "../../helpers/adyen-helper"; @@ -133,6 +135,25 @@ export default { }, methods: { + async initializeAdyenWithErrorHandling() { + try { + await this.initializeAdyen(); + } catch (error) { + console.error("Failed to initialize Adyen payment:", error); + + const errorJson = JSON.stringify(error, Object.getOwnPropertyNames(Object(error))); + analyticsMixin.methods.pushFmgSessionData(errorJson); + await global.$logger.logError(errorJson); + + this.$router.navigateWithoutSaving( + this.navigationScenarios.PIA_ERROR, + this.pageName + ); + + return; + } + }, + async backButtonAction() { // Stub, for navigating back via nav-bar. this.$router.navigateWithoutSaving( @@ -203,6 +224,12 @@ export default { }, onError: (error, component) => { console.log(`Error from Adyen`); + const errorJson = JSON.stringify( + error, + Object.getOwnPropertyNames(Object(error)) + ); + analyticsMixin.methods.pushFmgSessionData(errorJson); + this.handleError(error); }, }, @@ -371,7 +398,12 @@ export default { await global.$logger.logError(stringToLog); - this.hasPaymentFailureError = true; + this.$router.navigateWithoutSaving( + this.navigationScenarios.PIA_ERROR, + this.pageName + ); + + return; } this.dropinComponent?.update(); @@ -560,13 +592,16 @@ export default { isMSRFeeApplicable() { return this.$store.getters.order.isMSRFeeApplicable; }, + isMSRFeeCoveredByInsurance() { + return this.$store.getters.order.isMSRFeeCoveredByInsurance; + }, lineItems() { return deepClone(this.$store.getters.lineItems); }, }, mounted() { - this.initializeAdyen(); + this.initializeAdyenWithErrorHandling(); }, components: { diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index da49aa039..b0347f581 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -834,11 +834,12 @@ export default { return getBoolFromString(this.$route?.query?.piaError); }, shouldDisplayUnverifiedInsuranceAlert() { - const shouldDisplayUnverifiedDeduct = experimentMixin.methods.hasSettingEqualTo( + const hasExperimentSetting = experimentMixin.methods.hasSettingEqualTo( experimentSettings.SHOW_UNVERIFIED_DEDUCT_ENTRY, "true" ); - return shouldDisplayUnverifiedDeduct; + + return hasExperimentSetting && this.isInsurance; }, // Necessary to make the watcher of lineItems work // JavaScript does not keep a record of the old value, only a reference to it's location in the memory. diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index d9f238733..206ff2050 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -799,6 +799,7 @@ export default { false, false ); + this.dispatchStoreAction(this.storeActions.RESET_INSURANCE_DETAILS, false, false); this.dispatchStoreAction(this.storeActions.SAVE_CAN_SCHEDULE_ONLINE, false, false); this.dispatchStoreAction(this.storeActions.SAVE_ACCOUNT_NAME, null, false); } else { diff --git a/src/layouts/return-user/return-user.vue b/src/layouts/return-user/return-user.vue index ad307080d..6ce0b2528 100644 --- a/src/layouts/return-user/return-user.vue +++ b/src/layouts/return-user/return-user.vue @@ -47,6 +47,7 @@ import { Form } from "vee-validate"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { storeActions } from "@/constants/store-actions"; +import baseMixin from "@/mixins/base-mixin"; export default { name: "return-user", @@ -74,6 +75,13 @@ export default { startOverImage: null, }; }, + mounted() { + // The global baseMixin.mounted() skips hiding the loading modal when + // external-parameter state is active, but those parameters have no + // consumer on this interstitial. Clear that state and ensure the loader + // is hidden so the user can never get stuck behind it on this page. + baseMixin.methods.ResetExternalParamsAndHideModal(); + }, methods: { arePagePrerequisitesValid() { return getFunnelCookie() !== null; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c483c90df..788b7c388 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -706,10 +706,16 @@ export default { isMobileStaticRecalibrationApplicable() { return ( this.displayMSR && - this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE && + this.isMSRFeePartNumber && (this.enableMSRSplitPay || this.isCashItacNoComp || this.mobileFeePart?.isInsurable) ); }, + isMSRFeePartNumber() { + return ( + this.mobileFeePart?.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE || + this.mobileFeePart?.partNumber === partNumberStrings.MOBILE_DUAL_RECAL_FEE + ); + }, displayMSR() { return ( experimentMixin.methods @@ -1228,6 +1234,7 @@ export default { supportingItems[mobileFeeIndex].laborAmount = this.mobileFeePart.laborAmount; supportingItems[mobileFeeIndex].sellingPrice = this.mobileFeePart.sellingPrice; supportingItems[mobileFeeIndex].kitPrice = this.mobileFeePart.kitPrice; + supportingItems[mobileFeeIndex].isInsurable = this.mobileFeePart.isInsurable; } else { if (this.mobileFeePart !== null) supportingItems.push(this.mobileFeePart); } diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 3ab575f58..1ea812c49 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -160,12 +160,16 @@ export default { // and aws FireHose stream, DigitalConsumer-Session-Firehose, to push data to an // S3 bucket, safelite-dev-digitalconsumer-session-data-us-east-2/1. // This bucket data is then picked up by snowflake for analytics use. - async pushFmgSessionData() { + async pushFmgSessionData(errorJson = null) { var currentPageName = getPageNameFromRouter(true); if (!currentPageName) { return; } + if (errorJson) { + currentPageName += ` ERROR: ${errorJson}`; + } + const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.getters.order; @@ -209,6 +213,7 @@ export default { var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`; var sessionData = {}; + sessionData.currentPage = currentPageName; sessionData.sid = getSessionIdValue(); sessionData.deviceId = getDeviceIdValue(); @@ -931,6 +936,11 @@ function getPageNameFromRouter(useDefaultUrl = false) { router.currentRoute.value && router.currentRoute.value.name ) { + const query = router.currentRoute.value.query; + if (query && Object.keys(query).length > 0 && useDefaultUrl === true) { + const queryString = new URLSearchParams(query).toString(); + return `${router.currentRoute.value.name}?${queryString}`; + } return router.currentRoute.value.name; } diff --git a/src/mixins/bailout-mixin.js b/src/mixins/bailout-mixin.js new file mode 100644 index 000000000..f8c724da2 --- /dev/null +++ b/src/mixins/bailout-mixin.js @@ -0,0 +1,13 @@ +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; + +export default { + methods: { + navigateToBailoutPage(vm, bailoutCode) { + const self = vm ?? this; + + self.dispatchStoreAction(self.storeActions.SAVE_BAILOUT_CODE, bailoutCode).then(() => { + self.$router.navigateWithoutSaving(navigationScenarios.BAILOUT, self.pageName); + }); + }, + }, +}; diff --git a/src/mixins/bailout-mixin.spec.js b/src/mixins/bailout-mixin.spec.js new file mode 100644 index 000000000..c213de767 --- /dev/null +++ b/src/mixins/bailout-mixin.spec.js @@ -0,0 +1,103 @@ +import bailoutMixin from "@/mixins/bailout-mixin"; +import { storeActions } from "@/constants/store-actions.js"; +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; +import { bailoutCodes } from "@/constants/bailout-codes.js"; + +describe("bailout-mixin.js", () => { + test("navigateToBailoutPage: dispatches SAVE_BAILOUT_CODE action with bailout code", async () => { + // Arrange + const mockVm = createMockVm(); + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + bailoutCode + ); + }); + + test("navigateToBailoutPage: navigates to bailout page after saving bailout code", async () => { + // Arrange + const mockVm = createMockVm(); + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith( + navigationScenarios.BAILOUT, + mockVm.pageName + ); + }); + + test("navigateToBailoutPage: uses current context (this) when vm is not provided", async () => { + // Arrange + const mockRouter = { + navigateWithoutSaving: jest.fn().mockResolvedValue(undefined), + }; + const mockThis = { + dispatchStoreAction: jest.fn().mockResolvedValue(undefined), + $router: mockRouter, + storeActions: storeActions, + pageName: "test-page", + }; + + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage.call(mockThis, undefined, bailoutCode); + + // Assert + expect(mockThis.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + bailoutCode + ); + }); + + test("navigateToBailoutPage: passes correct bailout code to store", async () => { + // Arrange + const mockVm = createMockVm(); + const customBailoutCode = 999; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, customBailoutCode); + + // Assert + expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + customBailoutCode + ); + }); + + test("navigateToBailoutPage: calls navigateWithoutSaving with correct parameters", async () => { + // Arrange + const mockVm = createMockVm(); + const mockPageName = "vehicle-damage"; + mockVm.pageName = mockPageName; + const bailoutCode = bailoutCodes.PARTS_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith( + navigationScenarios.BAILOUT, + mockPageName + ); + }); +}); + +function createMockVm() { + return { + dispatchStoreAction: jest.fn().mockResolvedValue(undefined), + $router: { + navigateWithoutSaving: jest.fn().mockResolvedValue(undefined), + }, + storeActions, + pageName: "test-page", + }; +} diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index fc195d910..2309cbde5 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -1,9 +1,11 @@ import { storeActions } from "@/constants/store-actions.js"; import store from "@/store"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; +import bailoutMixin from "@/mixins/bailout-mixin"; import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import { experimentSettings } from "@/constants/experiments"; import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js"; +import { bailoutCodes } from "@/constants/bailout-codes"; export default { computed: { @@ -34,6 +36,11 @@ export default { const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS, { pageNameToLog: pageName, }); + + if (result.PartsNotFound) { + bailoutMixin.methods.navigateToBailoutPage(this, bailoutCodes.PARTS_NOT_FOUND); + } + const partsOrQuestions = result.data.partsOrQuestions; vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this); diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index 984204e3c..8112cb98e 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -14,6 +14,11 @@ const navigationScenarios = { CLICKED_FORWARD_WITH_INSURANCE: "CLICKED_FORWARD_WITH_INSURANCE", CLICKED_CANCEL_VERIFICATION: "CLICKED_CANCEL_VERIFICATION", + // Bailout + BAILOUT: "BAILOUT", + BAILOUT_SUCCESS: "BAILOUT_SUCCESS", + CLICKED_BACK_TO_HOMEPAGE: "CLICKED_BACK_TO_HOMEPAGE", + // TODO: use virtual page? // Vin selection CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN", diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index fc54025ef..479d0899d 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -171,6 +171,14 @@ export const routeData = { path: "/virtual/restart", virtual: true, }, + BAILOUT: { + name: "bailout", + path: "/bailout", + }, + BAILOUT_SUCCESS: { + name: "bailout-success", + path: "/bailout-success", + }, }; export const FUNNEL_START_PAGE = routeData.VEHICLE; diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 7decd143e..f66d67d0c 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -274,6 +274,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, destinationPageData: routeData.QUOTE, }, + { + scenario: navigationScenarios.BAILOUT, + destinationPageData: routeData.BAILOUT, + }, ], }, { @@ -806,6 +810,24 @@ const routingTable = function () { }, ], }, + { + pageName: routeData.BAILOUT.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.BAILOUT_SUCCESS, + }, + ], + }, + { + pageName: routeData.BAILOUT_SUCCESS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_TO_HOMEPAGE, + destinationPageData: routeData.RESTART, + }, + ], + }, ]; }; diff --git a/src/router/methods/navigate.js b/src/router/methods/navigate.js index b80d31769..1cdd87aac 100644 --- a/src/router/methods/navigate.js +++ b/src/router/methods/navigate.js @@ -70,7 +70,13 @@ export async function navigateWithSaving(scenario, currentPageName) { export async function navigateWithPageData(scenario, currentPageName, pageData = {}) { const nextPage = getDestination(currentPageName, scenario); - await savePageData(nextPage.name, pageData); + + if (pageData && pageData.bailoutCode) { + pageData.AppName = "FixMyGlass"; + await savePageData(currentPageName, pageData); + } else { + await savePageData(nextPage.name, pageData); + } return await navigate(scenario, currentPageName, true); } diff --git a/src/router/methods/routes.js b/src/router/methods/routes.js index 88cc17f94..34056ba76 100644 --- a/src/router/methods/routes.js +++ b/src/router/methods/routes.js @@ -51,6 +51,8 @@ export const routes = [ createRoute(routeData.RECALIBRATION_INFO), createRoute(routeData.COVERAGE_STATEMENT), createRoute(routeData.VERIFY_DETAILS), + createRoute(routeData.BAILOUT), + createRoute(routeData.BAILOUT_SUCCESS), // Virtual pages (resolve to a non-virtual page.) createVirtualRoute(routeData.LANDING, landingBeforeEnter), createVirtualRoute(routeData.HERITAGE, heritageBeforeEnter), diff --git a/src/store/index.js b/src/store/index.js index a58200a96..5b7449c4b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -170,6 +170,7 @@ const getDefaultState = () => { currentDeductible: 0, originalDeductible: 0, policyNumber: null, + claimNumber: null, isItac: false, additionalAuthFlag: null, isNoComp: false, @@ -216,6 +217,7 @@ const getDefaultState = () => { affiliateCookies: [], loggingOption: false, hasAlreadyTriggeredError: false, + bailoutCode: null, }, idempotencyKeyFields: { referralCorrelationId: null, @@ -355,6 +357,9 @@ export const mutations = { updateZipCode(state, zipCode) { state.order.policy.zipCode = zipCode; }, + updateClaimNumber(state, claimNumber) { + state.order.policy.claimNumber = claimNumber; + }, updateIsClaimAndCoverage(state, isClaimAndCoverage) { state.order.payment.isClaimAndCoverage = isClaimAndCoverage; }, @@ -370,6 +375,26 @@ export const mutations = { updateIsInsurance(state, isInsurance) { state.order.payment.isInsurance = isInsurance; }, + resetInsuranceDetails(state) { + state.order.payment.insuranceCoverage = { + isVerified: null, + coverageStatus: null, + coverageSubStatus: null, + coverageType: null, + coverageVerificationType: null, + }; + + state.order.policy.policyNumber = null; + state.order.policy.claimNumber = null; + state.order.policy.insuranceCompanyName = null; + state.order.policy.streetAddress = null; + state.order.policy.apartment = null; + state.order.policy.city = null; + state.order.policy.state = null; + state.order.policy.zipCode = null; + state.order.damage.dateOfLoss = null; + state.order.damage.damageCause = null; + }, updateIsPia(state, isPia) { state.order.payment.isPia = isPia; }, @@ -462,9 +487,11 @@ export const mutations = { state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate; }, updateServiceZip(state, serviceZipInfo) { - state.order.serviceLocation.state = serviceZipInfo.state; - state.order.serviceLocation.zipCode = serviceZipInfo.zipCode; - state.order.serviceLocation.zipCodeCtu = serviceZipInfo.zipCodeCtu; + state.order.serviceLocation.state = serviceZipInfo.state || serviceZipInfo.payload?.state; + state.order.serviceLocation.zipCode = + serviceZipInfo.zipCode || serviceZipInfo.payload?.zipCode; + state.order.serviceLocation.zipCodeCtu = + serviceZipInfo.zipCodeCtu || serviceZipInfo.payload?.zipCodeCtu; }, updateServiceLocation(state, serviceLocationInfo) { state.order.serviceLocation.address = serviceLocationInfo.address; @@ -523,6 +550,7 @@ export const mutations = { state.order.policy.city = insuranceDetails.city; state.order.policy.state = insuranceDetails.state; state.order.policy.zipCode = insuranceDetails.zipCode; + state.order.policy.claimNumber = insuranceDetails.claimNumber; } }, updateDamageDetails(state, damageDetails) { @@ -948,6 +976,9 @@ export const mutations = { state.idempotencyKeyFields.totalInCents = totalInCents; state.idempotencyKeyFields.expiryTime = expiryTime; }, + updateBailoutCode(state, bailoutCode) { + state.applicationUser.bailoutCode = bailoutCode; + }, }; // Export Getters @@ -1901,21 +1932,38 @@ export const actions = { // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); - const response = await globalMethods.callHttpClient({ - method: endpoints.GetPartsOrQuestions.method, - endpoint: endpoints.GetPartsOrQuestions.url, - payload: { - carId: carId, - glassPieces: glassArrayForPayload, - zip: zipCode, - vin: vin, - serviceType: serviceType, - referralSeqNumber: referralSeqNumber, - parentAccountNumber: parentAccountNumber, - }, - logApiCall: true, - pageNameToLog: pageNameToLog, - }); + const response = await globalMethods + .callHttpClient({ + method: endpoints.GetPartsOrQuestions.method, + endpoint: endpoints.GetPartsOrQuestions.url, + payload: { + carId: carId, + glassPieces: glassArrayForPayload, + zip: zipCode, + vin: vin, + serviceType: serviceType, + referralSeqNumber: referralSeqNumber, + parentAccountNumber: parentAccountNumber, + }, + logApiCall: true, + pageNameToLog: pageNameToLog, + }) + .catch((error) => { + if (error.status == 500) { + return { PartsNotFound: true }; + } + }); + + // Triggers bailout + if (response.PartsNotFound) { + return response; + } + + // Check if we only have MISC parts to trigger bailout + const miscPartsResponse = checkIfMiscParts(response.data.partsOrQuestions); + if (miscPartsResponse.PartsNotFound) { + return miscPartsResponse; + } // Flatten location and name properties response.data.partsOrQuestions = convertGlassPieceNamingFromApi( @@ -3111,6 +3159,9 @@ export const actions = { saveBillToAccountNumber(context, billToAccountNumber) { context.commit(storeMutations.UPDATE_BILL_TO_ACCT_NUMBER, billToAccountNumber); }, + resetInsuranceDetails(context) { + context.commit(storeMutations.RESET_INSURANCE_DETAILS); + }, saveSupportingItems(context, supportingItems) { syncLineItemIds(supportingItems, context.state.order.lineItems.supportingItems); @@ -3818,6 +3869,10 @@ export const actions = { context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey); } }, + + saveBailoutCode(context, bailoutCode) { + context.commit(storeMutations.UPDATE_BAILOUT_CODE, bailoutCode); + }, }; export default createStore({ @@ -4359,3 +4414,17 @@ const timeSlotCallFlags = { shop: false, mobile: false, }; + +function checkIfMiscParts(partsOrQuestions) { + if (!partsOrQuestions || partsOrQuestions.length === 0) { + return { PartsNotFound: true }; + } else if ( + partsOrQuestions.length === 1 && + partsOrQuestions[0].parts && + partsOrQuestions[0].parts.length === 1 && + partsOrQuestions[0].parts[0].partNumber.startsWith("MISC") + ) { + return { PartsNotFound: true }; + } + return { PartsNotFound: false }; +}