diff --git a/playwright-tests/business-logic/data/ClientData.ts b/playwright-tests/business-logic/data/ClientData.ts index 1f1c2da6..a6948d69 100644 --- a/playwright-tests/business-logic/data/ClientData.ts +++ b/playwright-tests/business-logic/data/ClientData.ts @@ -1,5 +1,5 @@ import { IClient } from "@business-logic/types/Client"; -import { IPaymentDetails } from "@business-logic/types/CustomerDetails"; +import { IPaymentDetails, IPaymentDetailsAdyen } from "@business-logic/types/CustomerDetails"; import { PaymentType } from "@business-logic/types/Enums"; const essentialClients: IClient[] = [ @@ -223,6 +223,19 @@ const defaultCreditCardDetails: IPaymentDetails = { } } +const creditCardDetailsAdyen: IPaymentDetailsAdyen = { + paymentType: PaymentType.Credit, + cardNumber: '4151500000000008', + expirationDate: '03/30', + cvv: '737', + billingAddress: { + street: '123 Test Road', + city: 'Columbus', + state: 'Ohio', + postalCode: '43028', + } +} + const defaultAfterpayDetails: IPaymentDetails = { paymentType: PaymentType.AfterPay, username: 'itqatest@safelite.com', @@ -233,12 +246,24 @@ const defaultAfterpayDetails: IPaymentDetails = { cvv: '000' } +const afterpayDetailsAdyen: IPaymentDetailsAdyen = { + paymentType: PaymentType.AfterPay, + username: 'itqatest@safelite.com', + password: 'Safelite1', +} + const defaultPaypalDetails: IPaymentDetails = { paymentType: PaymentType.Paypal, username:'Itqatest@safelite.com', password: 'Safelite1' } +const paypalDetailsAdyen: IPaymentDetailsAdyen = { + paymentType: PaymentType.Paypal, + username: 'Itqatest@safelite.com', + password: 'Safelite1' +} + export default class ClientData { static getEssentialClients() { return essentialClients; @@ -264,10 +289,22 @@ export default class ClientData { return defaultCreditCardDetails; } + static getCreditCardDetailsAdyen() { + return creditCardDetailsAdyen; + } + static getDefaultAfterpayDetails() { return defaultAfterpayDetails; } + static getAfterpayDetailsAdyen() { + return afterpayDetailsAdyen; + } + + static getPaypalDetailsAdyen() { + return paypalDetailsAdyen; + } + static getDefaultPaypalDetails() { return defaultPaypalDetails; } diff --git a/playwright-tests/business-logic/types/CustomerDetails.ts b/playwright-tests/business-logic/types/CustomerDetails.ts index 1020694f..f722565a 100644 --- a/playwright-tests/business-logic/types/CustomerDetails.ts +++ b/playwright-tests/business-logic/types/CustomerDetails.ts @@ -72,6 +72,16 @@ export interface IPaymentDetails { billingAddress?: IAddress, } +export interface IPaymentDetailsAdyen { + paymentType: PaymentType, + username?: string, + password?: string, + cardNumber?: string, + expirationDate?: string, + cvv?: string, + billingAddress?: IAddress, +} + // export interface IVehicleDamage { // isRearWindowDamage?: boolean, // windshieldDamage?: WindshieldDamage, diff --git a/playwright-tests/business-logic/types/Enums.ts b/playwright-tests/business-logic/types/Enums.ts index 9280ab00..da4f2f1e 100644 --- a/playwright-tests/business-logic/types/Enums.ts +++ b/playwright-tests/business-logic/types/Enums.ts @@ -68,7 +68,8 @@ export enum VehicleDamage { export enum VehicleLookupType { Vin, Address, - LicensePlateNumber + LicensePlateNumber, + RatherNotShareVin } export enum ServiceLocation { diff --git a/playwright-tests/business-logic/types/ITestData.ts b/playwright-tests/business-logic/types/ITestData.ts index d3a19d34..a457644c 100644 --- a/playwright-tests/business-logic/types/ITestData.ts +++ b/playwright-tests/business-logic/types/ITestData.ts @@ -1,5 +1,5 @@ import { ServicePackage, VehicleDamage } from "./Enums" -import { IAppointmentDetails, IClaimDetails, ICustomerDetails, IEndorsementDetails, IPartQuestion, IPaymentDetails, IVehicleDetails, IAddressVehicleDetails } from "./CustomerDetails" +import { IAppointmentDetails, IClaimDetails, ICustomerDetails, IEndorsementDetails, IPartQuestion, IPaymentDetails, IPaymentDetailsAdyen, IVehicleDetails, IAddressVehicleDetails } from "./CustomerDetails" import IBailoutFlags from "./IBailoutFlags" export interface ITestData { @@ -35,13 +35,16 @@ export interface ITestData { otherVehiclesOnPolicy: IVehicleDetails[], // IF defined, we validate that the vehicles are present. vehicleDamage: VehicleDamage[], // Array of vehicle damage appointmentDetails: IAppointmentDetails, - paymentDetails: IPaymentDetails // Payment information + paymentDetails: IPaymentDetails, // Payment information + paymentDetailsAdyen: IPaymentDetailsAdyen, // Adyen Payment information isRecalNotification: boolean, isRecalWarning: boolean, isRecalVehicle: boolean, + isCanSafeliteRecalibrate: boolean, // Can Safelite recalibrate the vehicle? If no, modal displays after clicking time slot and continue isSeparateApptsWarning: boolean, // IF true, check for the separate appts warning on VehicleDamagePage isAuthenticationRequired: boolean, isMoldingQuestion: boolean, isNonServiceable: boolean, // For the flow where a a non-serviceable vehicle is selected on lookup isNonServiceableVin: boolean, // For the flow where a serviceable vehicle is selected on lookup, but then the VIN of a non-serviceable vehicle is entered + isRepairTPA: boolean, } diff --git a/playwright-tests/pages/AfterpayPage.ts b/playwright-tests/pages/AfterpayPage.ts index 2f13ed04..e70ef387 100644 --- a/playwright-tests/pages/AfterpayPage.ts +++ b/playwright-tests/pages/AfterpayPage.ts @@ -1,6 +1,6 @@ import { Locator, Page } from "@playwright/test"; import { BasePage } from "./BasePage"; -import { IClaimDetails, IPaymentDetails } from "@business-logic/types/CustomerDetails"; +import { IClaimDetails, IPaymentDetails, IPaymentDetailsAdyen } from "@business-logic/types/CustomerDetails"; import { ServicePackage } from "@business-logic/types/Enums"; export class AfterpayPage extends BasePage { @@ -33,11 +33,11 @@ export class AfterpayPage extends BasePage { await this.submitButton.click(); } - async executeAfterpayPayment(paymentDetails: IPaymentDetails, claimDetails: IClaimDetails, servicePackage: ServicePackage) { + async executeAfterpayPayment(paymentDetailsAdyen: IPaymentDetailsAdyen, claimDetails: IClaimDetails, servicePackage: ServicePackage) { const confirmButtonOrPaymentOptions = this.confirmButton.or(this.selectAfterpayWithoutInterestButton); - await this.login(paymentDetails.password!); + await this.login(paymentDetailsAdyen.password!); await this.page.locator('div[data-testid=\'loading-icon-svg\']').filter({ visible: true}).first().waitFor({ state: 'hidden' }); await confirmButtonOrPaymentOptions.waitFor({ state: 'visible' }); diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index bf950f07..d8452aec 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -30,7 +30,7 @@ export class BasePage { await expect(async () => { const currentUrl = this.page.url(); if (currentUrl === startingUrl) { - await this.continueButton.click({ timeout: 1000 }); + await this.continueButton.click({ timeout: 5_000 }); } //this causes the schedule page to fail //await expect(this.buttonLoadSpin).toHaveCount(0, {timeout: 180000}); @@ -96,24 +96,23 @@ export class BasePage { } async logReferralNumber() { - let mainSessionStorage = JSON.parse(await this.page.evaluate('sessionStorage.getItem(\'main\')')); - let referralNumber = mainSessionStorage.order.referralNumber as number; - let referralSequenceNumber = mainSessionStorage.order.referralSequenceNumber as number; - if (referralNumber == null) { - for (let i = 1; i <= 20; i++) { - if (!referralNumber == null) break; - await this.page.waitForTimeout(500); - mainSessionStorage = JSON.parse(await this.page.evaluate('localStorage.getItem(\'main\')')); - referralNumber = mainSessionStorage.order.referralNumber as number; - referralSequenceNumber = mainSessionStorage.order.referralSequenceNumber as number; - } + let referralNumber: number | null = null; + let referralSequenceNumber: number | null = null; + for (let i = 0; i < 3; i++) { + const main = JSON.parse(await this.page.evaluate(() => sessionStorage.getItem('main')) ?? 'null'); + referralNumber = main?.order?.referralNumber ?? null; + referralSequenceNumber = main?.order?.referralSequenceNumber ?? null; + if (referralNumber !== null) break; + await this.page.waitForTimeout(500); + } + if (referralNumber === null) { + console.warn('Referral Number could not be retrieved after retries.'); + return; } - await test.step(`Referral Number:${referralNumber} Referral Sequence Number:${referralSequenceNumber}`, async () => { console.log(`Referral Number:${referralNumber}`); console.log(`Referral Sequence Number:${referralSequenceNumber}`); }); - } async validateURL(issPageValue: string) { @@ -132,6 +131,6 @@ export class BasePage { await expect(async () => { const currentUrl = this.page.url(); expect(currentUrl).not.toEqual(startingUrl); - }).toPass({ timeout: 70_000 }); + }).toPass({ timeout: 90_000 }); } } \ No newline at end of file diff --git a/playwright-tests/pages/PartQuestionsPage.ts b/playwright-tests/pages/PartQuestionsPage.ts index 1591b132..ed18eabb 100644 --- a/playwright-tests/pages/PartQuestionsPage.ts +++ b/playwright-tests/pages/PartQuestionsPage.ts @@ -11,17 +11,6 @@ export class PartQuestionsPage extends BasePage { this.page = page; } - async validatePartQuestions(partQuestions: IPartQuestion[]) { - for (const pq of partQuestions) { - const partQuestionOptions = this.page.locator(`fieldset[aria-labelledby="${pq.partQuestionType}"]`); - if (pq.isOnPage) { - await expect(partQuestionOptions).toBeAttached(); - } else { - await expect(partQuestionOptions).not.toBeAttached(); - } - } - } - async selectPartQuestionResponses(partQuestions: IPartQuestion[]) { for (const pq of partQuestions) { const parentobject=this.page.locator(`fieldset[aria-labelledby="${pq.partQuestionType}"]`); diff --git a/playwright-tests/pages/PaymentAdyenPage.ts b/playwright-tests/pages/PaymentAdyenPage.ts new file mode 100644 index 00000000..94c00e6f --- /dev/null +++ b/playwright-tests/pages/PaymentAdyenPage.ts @@ -0,0 +1,46 @@ +import { type Locator, type Page } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { IPaymentDetailsAdyen } from '@business-logic/types/CustomerDetails'; + +export class PaymentAdyenPage extends BasePage { + readonly page: Page; + + readonly cardNumberTextField: Locator; + readonly expiryDateTextField: Locator; + readonly securityCodeTextField: Locator; + readonly billingAddressTextField: Locator; + readonly cityTextField: Locator; + readonly stateDropDown: Locator; + readonly payButton: Locator; + + readonly afterPayButton: Locator; + readonly afterPayLaunchButton: Locator; + readonly applePayButton: Locator; + + issPageValue = 'payment-page-adyen'; + + constructor(page: Page) { + super(page); + this.page = page; + + //Credit Card Fields + this.cardNumberTextField = this.page.locator('iframe[title="Iframe for card number"]').contentFrame().getByRole('textbox', { name: 'Card number' }); + this.expiryDateTextField = this.page.locator('iframe[title="Iframe for expiry date"]').contentFrame().getByRole('textbox', { name: 'Expiry date' }); + this.securityCodeTextField = this.page.locator('iframe[title="Iframe for security code"]').contentFrame().getByRole('textbox', { name: 'CVV/CVC' }); + this.billingAddressTextField = this.page.getByRole('textbox', { name: 'Address' }); + this.cityTextField = this.page.getByRole('textbox', { name: 'City' }); + this.stateDropDown = this.page.getByRole('combobox', { name: 'State' }); + this.payButton = this.page.getByRole('button', { name: 'Pay $' }); + } + + async populateCreditCardDetails(paymentDetailsAdyen: IPaymentDetailsAdyen){ + await this.cardNumberTextField.fill(paymentDetailsAdyen.cardNumber!); + await this.expiryDateTextField.fill(paymentDetailsAdyen.expirationDate!); + await this.securityCodeTextField.fill(paymentDetailsAdyen.cvv!); + await this.billingAddressTextField.fill(paymentDetailsAdyen.billingAddress!.street); + await this.cityTextField.fill(paymentDetailsAdyen.billingAddress!.city); + await this.stateDropDown.fill(paymentDetailsAdyen.billingAddress!.state); + await this.stateDropDown.press('Enter'); + } + +} \ No newline at end of file diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index d7c7ef9c..b74dd22c 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -1,18 +1,24 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { IClaimDetails, IPaymentDetails } from '@business-logic/types/CustomerDetails'; +import { IClaimDetails, IPaymentDetails, IPaymentDetailsAdyen } from '@business-logic/types/CustomerDetails'; import { PaymentType, ServicePackage } from '@business-logic/types/Enums'; import { PaymentPage } from './PaymentPage'; import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; +import { PaymentAdyenPage } from './PaymentAdyenPage'; export class PaymentMethodPage extends BasePage { readonly page: Page; readonly paypalPage: PaypalPage; readonly paymentPage: PaymentPage; + readonly paymentAdyenPage: PaymentAdyenPage; readonly payNowButton: Locator; readonly paypalButton: Locator; + readonly payPalAdyenButton: Locator; + readonly payPalLaunchAdyenButton: Locator; + readonly afterpayAdyenButton: Locator; + readonly afterpayLaunchAdyenButton: Locator; readonly payInFourButton: Locator; readonly payAtAppointmentButton: Locator; @@ -28,9 +34,20 @@ export class PaymentMethodPage extends BasePage { this.page = page; this.paypalPage = new PaypalPage(page); this.paymentPage = new PaymentPage(page); + this.paymentAdyenPage = new PaymentAdyenPage(page); this.payNowButton = this.page.locator('[buttonlabel="Pay now"]'); // credit and paypal options behind this button this.paypalButton = this.page.frameLocator('iframe[name="card-frame"]').locator('div[id="paypalParentDiv"]'); + + // Ayden Paypal button + this.payPalAdyenButton = this.page.getByRole('radio', { name: 'PayPal' }); + this.payPalLaunchAdyenButton = this.page.frameLocator('iframe[title="PayPal-paypal"]:first-of-type').locator('div[role="link"][class*="paypal-button"]'); + + // Ayden Afterpay buttons + this.afterpayAdyenButton = this.page.getByRole('radio', { name: 'Afterpay' }); + this.afterpayLaunchAdyenButton = this.page.getByRole('button', { name: 'Continue to Afterpay' }); + + this.payInFourButton = this.page.locator('[buttonlabel="Pay in 4 installments"]'); // Afterpay this.payAtAppointmentButton = this.page.locator('[buttonlabel="Pay at my appointment"]'); @@ -40,22 +57,24 @@ export class PaymentMethodPage extends BasePage { this.submitButton = this.page.getByRole('button', { name: 'Submit' }); } - async executePayment(paymentDetails: IPaymentDetails, claimDetails: IClaimDetails, servicePackage: ServicePackage) { + async executePayment(paymentDetails: IPaymentDetails, paymentDetailsAdyen: IPaymentDetailsAdyen, claimDetails: IClaimDetails, servicePackage: ServicePackage) { const browserContext = this.page.context(); - switch (paymentDetails.paymentType) { - case PaymentType.Credit: + switch (paymentDetailsAdyen?.paymentType ?? paymentDetails?.paymentType) { case PaymentType.Credit: await this.payNowButton.click(); await this.textReminderNoButton.click(); await this.continueToCheckoutButton.click(); - await this.selectCreditCard(paymentDetails); + await this.selectCreditCard(paymentDetailsAdyen); break; case PaymentType.Paypal: await this.payNowButton.click(); await this.textReminderNoButton.click(); await this.continueToCheckoutButton.click(); - await this.selectPaypal(); - await this.paypalPage.completePaypalPurchase(paymentDetails); + + const paypalPopup = await this.selectPaypal(); + const paypalPage = new PaypalPage(paypalPopup); + + await paypalPage.completePaypalPurchase(paymentDetailsAdyen); break; case PaymentType.AfterPay: await this.payInFourButton.click(); @@ -63,11 +82,11 @@ export class PaymentMethodPage extends BasePage { await this.continueToCheckoutButton.click(); // Capture popup - const afterpayPopup = await browserContext.waitForEvent('page'); - const afterpayPage = new AfterpayPage(afterpayPopup); + const afterpayPage = await this.selectAfterpayAdyen(); + const afterpayAdyenPage = new AfterpayPage(afterpayPage); // Execute payment - await afterpayPage.executeAfterpayPayment(paymentDetails, claimDetails, servicePackage); + await afterpayAdyenPage.executeAfterpayPayment(paymentDetailsAdyen, claimDetails, servicePackage); break; case PaymentType.PayAtService: @@ -80,13 +99,25 @@ export class PaymentMethodPage extends BasePage { break; } } - - async selectPaypal() { - await this.paypalButton.click(); + async selectAfterpayAdyen(): Promise { + await this.afterpayAdyenButton.click(); + await this.afterpayLaunchAdyenButton.click(); + //await this.page.waitForLoadState('networkidle'); + return this.page; } - async selectCreditCard(paymentDetails: IPaymentDetails) { - await this.paymentPage.populateCreditCardDetails(paymentDetails); + async selectPaypal(): Promise { + await this.payPalAdyenButton.click(); + const paypalPage = this.page.waitForEvent('popup'); + await this.payPalLaunchAdyenButton.click(); + const paypalPopup = await paypalPage; + await paypalPopup.waitForLoadState(); + return paypalPopup; + } + + async selectCreditCard(paymentDetailsAdyen: IPaymentDetailsAdyen) { + await this.paymentAdyenPage.populateCreditCardDetails(paymentDetailsAdyen); + await this.paymentAdyenPage.payButton.click(); } async selectPayAtAppointment() { diff --git a/playwright-tests/pages/PaymentPage.ts b/playwright-tests/pages/PaymentPage.ts index 4ba47ee6..fff2edd3 100644 --- a/playwright-tests/pages/PaymentPage.ts +++ b/playwright-tests/pages/PaymentPage.ts @@ -1,6 +1,5 @@ import { type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { AfterpayPage } from './AfterpayPage'; import { IPaymentDetails } from '@business-logic/types/CustomerDetails'; export class PaymentPage extends BasePage { diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index 05178dd9..8f97a019 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -1,6 +1,6 @@ import { expect, type Locator, type Page } from "@playwright/test"; import { BasePage } from "./BasePage"; -import { IPaymentDetails } from "@business-logic/types/CustomerDetails"; +import { IPaymentDetailsAdyen } from "@business-logic/types/CustomerDetails"; export class PaypalPage extends BasePage { readonly page: Page; @@ -13,7 +13,7 @@ export class PaypalPage extends BasePage { constructor(page: Page) { super(page); this.page = page; - this.usernameTextBox = page.locator("#email"); + this.usernameTextBox = page.getByRole("textbox", { name: "Email or mobile number" }); this.nextButton = page.getByRole("button", { name: "Next" }); this.passwordTextBox = page.getByRole("textbox", { name: "Password" }); this.paypalLoginButton = page.getByRole("button", { @@ -23,16 +23,16 @@ export class PaypalPage extends BasePage { this.payButton = page.getByRole("button", { name: "Pay $" }); } - async completePaypalPurchase(paymentDetails: IPaymentDetails) { + async completePaypalPurchase(paymentDetailsAdyen: IPaymentDetailsAdyen) { await expect(async () => { - await this.usernameTextBox.fill(paymentDetails.username!); + await this.usernameTextBox.fill(paymentDetailsAdyen.username!); await this.nextButton.waitFor({ state: 'visible', timeout: 5000 }); await this.nextButton.click(); await expect(this.passwordTextBox).toBeVisible({ timeout: 5000 }); }).toPass({ timeout: 30000 }); - await this.passwordTextBox.fill(paymentDetails.password!); + await this.passwordTextBox.fill(paymentDetailsAdyen.password!); await this.paypalLoginButton.click(); await this.payButton.click(); } diff --git a/playwright-tests/pages/PolicyHolderDetailsPage.ts b/playwright-tests/pages/PolicyHolderDetailsPage.ts index 2cca536a..dd708f64 100644 --- a/playwright-tests/pages/PolicyHolderDetailsPage.ts +++ b/playwright-tests/pages/PolicyHolderDetailsPage.ts @@ -45,11 +45,11 @@ export class PolicyHolderDetailsPage extends BasePage { // For essential flows, when address fields are not pre-filled if (!addressValue || addressValue.trim() === '') { await this.addressInputBox.click(); - await this.addressInputBox.fill(customerDetails.address.street); + await this.addressInputBox.pressSequentially(customerDetails.address.street); // Wait for suggestions to load (Google Places has a slight delay) - await this.page.locator('.pac-item').first().waitFor({ state: 'visible', timeout: 6000 }); - + await this.page.locator('.pac-item').first().waitFor({ state: 'attached', timeout: 6000 }); + await this.page.locator('.pac-item').first().isVisible(); // Click the first result await this.page.locator('.pac-item').first().click(); diff --git a/playwright-tests/pages/ProviderPreferencePage.ts b/playwright-tests/pages/ProviderPreferencePage.ts index 50b1a617..d35c54ad 100644 --- a/playwright-tests/pages/ProviderPreferencePage.ts +++ b/playwright-tests/pages/ProviderPreferencePage.ts @@ -42,6 +42,8 @@ export class ProviderPreferencePage extends BasePage { async selectProvider(isSafelite: boolean) { if (isSafelite) { + await expect(this.scheduleNowButton).toBeVisible(); + await expect(this.scheduleNowButton).toBeEnabled(); await this.scheduleNowButton.click(); await this.nextPage(); } diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 8c8f3f8c..921cddb9 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -25,14 +25,19 @@ export class SchedulePage extends BasePage { readonly dateText: Locator; readonly viewMoreDatesLink: Locator; + readonly recalAcknowledgementModal: Locator; + readonly learnMoreLinkRecalAcknowledgementModal: Locator; + readonly recalAcknowledgementCheckBox: Locator; + readonly continueButtonRecalAcknowledgementModal: Locator; + readonly continueButton: Locator; constructor(page: Page) { super(page); this.page = page; - this.inShopButton = this.page.locator('[buttonlabel="At a Safelite shop"]'); - this.mobileButton = this.page.locator('[buttonlabel="Have Safelite come to me"]'); + this.inShopButton = this.page.getByText('At a Safelite shop'); + this.mobileButton = this.page.getByText('Have Safelite come to me'); // For in shop this.moreLocationsButton = this.page.getByRole('button', { name: 'More Locations' }); // For mobile @@ -50,6 +55,12 @@ export class SchedulePage extends BasePage { this.dateText = this.page.locator('[class="modal-header mb-2 mt-2"]'); this.viewMoreDatesLink = this.page.getByRole('button', { name: 'More Right arrow icon' }); + // Recal acknowledgement modal when Safelite cannot recalibrate the vehicle + this.recalAcknowledgementModal = this.page.getByLabel('ModalComponentLabel'); + this.learnMoreLinkRecalAcknowledgementModal = this.page.getByRole('link', { name: 'Learn more' }); + this.recalAcknowledgementCheckBox = this.page.getByRole('checkbox', { name: /I acknowledge/i }); + this.continueButtonRecalAcknowledgementModal = this.page.locator('#RecalAckModalWidget').getByRole('button', { name: 'Continue' }); + this.continueButton = this.page.locator('#stacked').locator('button:has-text("Continue")'); } @@ -77,6 +88,7 @@ export class SchedulePage extends BasePage { await (await this.getFirstNonDropOffTimeSlot()).click(); } } else { + await this.inShopButton.click(); await (await this.getFirstNonDropOffTimeSlot()).click(); } } else { @@ -110,4 +122,10 @@ export class SchedulePage extends BasePage { const nonDropOff = timeSlots.filter({ hasNotText: /Drop & Go/i }); return nonDropOff.first(); } + + async validateRecalAcknowledgementModal() { + await this.learnMoreLinkRecalAcknowledgementModal.click(); + await this.recalAcknowledgementCheckBox.click(); + await this.continueButtonRecalAcknowledgementModal.click(); + } } \ No newline at end of file diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 83c0b133..98070447 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -32,7 +32,7 @@ export class ServicePackagesPage extends BasePage { try {await this.wiperModal.waitFor({ state: 'visible', timeout: 5000 }); await this.wiperModalCloseButton.click(); } catch { - await this.continueButton.click(); + console.log('Wiper modal not displayed'); } } else if (servicePackage === ServicePackage.GlassOnly) { await this.glassOnlyPackageButton.check(); diff --git a/playwright-tests/pages/TpaSubmitPage.ts b/playwright-tests/pages/TpaSubmitPage.ts index 49891145..fe7743f8 100644 --- a/playwright-tests/pages/TpaSubmitPage.ts +++ b/playwright-tests/pages/TpaSubmitPage.ts @@ -24,6 +24,8 @@ export class TpaSubmitPage extends BasePage { async validateDeductible(claimDetails: IClaimDetails, isUnverifiedPolicyAfterVehicleLookup: boolean, isPolicyFound: boolean) { const deductibleTextValue = await this.deductible.textContent(); + await expect(this.deductible).toBeVisible(); + if (claimDetails.policyDeductible !== -1 && !isUnverifiedPolicyAfterVehicleLookup) { expect.soft(deductibleTextValue).toContain(claimDetails.policyDeductible.toLocaleString()); } else diff --git a/playwright-tests/pages/VehicleLookupPage.ts b/playwright-tests/pages/VehicleLookupPage.ts index 8e8433d3..b847918e 100644 --- a/playwright-tests/pages/VehicleLookupPage.ts +++ b/playwright-tests/pages/VehicleLookupPage.ts @@ -12,6 +12,7 @@ export class VehicleLookupPage extends BasePage { readonly addressLookupButton: Locator; readonly licenseLookupButton: Locator; readonly vinLookupPage: VinLookupPage; + readonly ratherNotShareVinButton: Locator; readonly vehicleLookupAddressPage: VehicleLookupAddressPage; readonly vehicleLookupLicensePage: VehicleLookupLicensePage; issPageValue = 'vehicle-lookup'; @@ -22,6 +23,7 @@ export class VehicleLookupPage extends BasePage { this.vinLookupButton = page.getByLabel('Provide my VIN', { exact: true }); this.addressLookupButton = page.getByLabel('Provide my home address', { exact: true }); this.licenseLookupButton = page.getByLabel('Provide my license plate #', { exact: true }); + this.ratherNotShareVinButton = page.getByText(/rather not share my vin/i); this.vinLookupPage = new VinLookupPage(page); // this.validateURL(this.url); @@ -41,6 +43,10 @@ export class VehicleLookupPage extends BasePage { await this.selectVinLookup(); await this.nextPage(); break; + case VehicleLookupType.RatherNotShareVin: + await this.selectRatherNotShareVin(); + await this.nextPage(); + break; default: console.error('VehicleLookupPage >> DATA ISSUE: VehicleLookupType not provided'); break; @@ -58,4 +64,8 @@ export class VehicleLookupPage extends BasePage { async selectLicenseLookup(){ await this.licenseLookupButton.click(); } + + async selectRatherNotShareVin(){ + await this.ratherNotShareVinButton.click(); + } } diff --git a/playwright-tests/pages/VehicleSelectionPage.ts b/playwright-tests/pages/VehicleSelectionPage.ts index 9c61f3de..1dc752e2 100644 --- a/playwright-tests/pages/VehicleSelectionPage.ts +++ b/playwright-tests/pages/VehicleSelectionPage.ts @@ -27,7 +27,7 @@ export class VehicleSelectionPage extends BasePage { await this.yearDropdown.selectOption(vehicleDetails.year); await this.yearDropdown.press('Tab'); await this.makeDropdown.selectOption(vehicleDetails.make); - await expect(this.modelDropdown).toBeEditable({ timeout: 5000 }); + await expect(this.modelDropdown).toBeEditable({ timeout: 6000 }); await this.makeDropdown.press('Tab'); await this.modelDropdown.selectOption(vehicleDetails.model); await expect(this.styleDropdown).toBeEditable({ timeout: 2000 }); diff --git a/playwright-tests/pages/VinLookupPage.ts b/playwright-tests/pages/VinLookupPage.ts index 825b37bd..39257997 100644 --- a/playwright-tests/pages/VinLookupPage.ts +++ b/playwright-tests/pages/VinLookupPage.ts @@ -35,7 +35,7 @@ export class VinLookupPage extends BasePage { } } - async triggerBailout() { + async returnToVehicleLookupPage() { await this.continueButton.click(); await this.lookupVinForMe.click(); } diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 94842ffb..682ee0fc 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -68,9 +68,9 @@ export default defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 1 : 0, + retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 4 : 5, + workers: process.env.CI ? 1 : 5, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI? [ ['junit'], @@ -91,9 +91,11 @@ export default defineConfig({ baseURL: process.env.BASE_URL || 'https://selfservice.test.glassclaim.com', trace: 'on-first-retry', headless: process.env.CI ? true : false, + video: 'off', + viewport: { width: 1920, height: 1080 }, screenshot: "only-on-failure", - actionTimeout: 60_000, - navigationTimeout: 60_000 + actionTimeout: process.env.CI ? 90_000 : 60_000, + navigationTimeout: process.env.CI ? 90_000 : 60_000 }, /* Configure projects for major browsers */ diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index b7c69e7d..787b5704 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -24,7 +24,7 @@ import advancedScenario0001TestCases from "./advanced/0001a_ReplaceInShopCredit" import advancedScenario0003TestCases from "./advanced/0003a_MobileAfterpay"; import essentialReplaceDynamicAdasTests from "./0005_EssentialReplaceDynamicAdas"; import essentialReplaceStaticAdasTests from "./0001_EssentialReplaceStatisAdas"; -import essentialVehicleLookupBailoutTests from "./0020_EssentialVehicleLookupBailout"; +import essentialPartQuestionsAndNotShareVinTests from "./0020_EssentialPartQuestionsAndNotShareVin"; import essentialServiceableBigTruckTestCases from "./0022_EssentialServiceableBigTruck"; import essentialNonServiceableBigTruckTestCases from "./0023_EssentialNonServiceableBigTruck"; import essentialNonServiceableBigTruckVinTestCases from "./0024_EssentialNonServiceableBigTruckVin"; @@ -85,7 +85,7 @@ test.describe.parallel('ISS QA Automation Regression', () => { addSmokeTagToRandomTest(essentialTpaNotEnabledReplace_0017); addSmokeTagToRandomTest(essentialTpaEnabledReplace_0018); addSmokeTagToRandomTest(essentialTpaEnabledReplaceRecal_0019); - addSmokeTagToRandomTest(essentialVehicleLookupBailoutTests); + addSmokeTagToRandomTest(essentialPartQuestionsAndNotShareVinTests); addSmokeTagToRandomTest(essentialServiceableBigTruckTestCases); addSmokeTagToRandomTest(essentialNonServiceableBigTruckTestCases); addSmokeTagToRandomTest(essentialNonServiceableBigTruckVinTestCases); @@ -131,7 +131,7 @@ test.describe.parallel('ISS QA Automation Regression', () => { for (const testCase of essentialUniqueGlassTests) { test(...prepareTest(testCase, run, options, ruleEngine)); } - //Scenario 13 //defect# SSR-2009 + //Scenario 13 for (const testCase of essentialReplaceTestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -160,8 +160,8 @@ test.describe.parallel('ISS QA Automation Regression', () => { test(...prepareTest(testCase, run, options, ruleEngine)); } //Scenario 20 - for (const testCase of essentialVehicleLookupBailoutTests) { - test(...prepareTest(testCase, run, options, ruleEngine)); //skipping this until SSR-2004 is fixed + for (const testCase of essentialPartQuestionsAndNotShareVinTests) { + test(...prepareTest(testCase, run, options, ruleEngine)); } // Scenario 22 for (const testCase of essentialServiceableBigTruckTestCases) { @@ -191,7 +191,6 @@ test.describe.parallel('ISS QA Automation Regression', () => { } // Scenario 0003a - // Note: payment will fail in dev. Payment (PIA) works fine in SYS for (const testCase of advancedScenario0003TestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -256,7 +255,6 @@ test.describe.parallel('ISS QA Automation Regression', () => { } // Scenario 0016a - // FIXME: Defect was created INSR-2138 for (const testCase of advancedScenario0016TestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -276,7 +274,7 @@ test.describe.parallel('ISS QA Automation Regression', () => { test(...prepareTest(testCase, run, options, ruleEngine)); } - // Scenario 0020a : There's a defect open INSR-2149 + // Scenario 0020a for (const testCase of advancedScenario0020TestCases) { test(...prepareTest(testCase, run, options, ruleEngine)); } @@ -377,11 +375,11 @@ async function runWorkflow(page: Page, testCase: TestCase) { // Destructure data for easy access const { customerDetails, claimDetails, vehicleDetails, addressVehicleDetails, vehicleDamage, appointmentDetails, isSafelite, endorsements, - partQuestions, paymentDetails, isNoComp, isItac, isRecalNotification, isUnverifiedPolicyAfterVehicleLookup, + partQuestions, paymentDetails, paymentDetailsAdyen, isNoComp, isItac, isRecalNotification, isUnverifiedPolicyAfterVehicleLookup, isRecalWarning, servicePackage, hasOemEndorsement, hasStateLawPopup, otherVehiclesOnPolicy, isUseVehicleFromAddressLookup, isSeparateApptsWarning, vehiclePartQuestions, editVehicleDetails, isAddressLookupValidations, hasMilitaryWarning, capabilityQuestions, isUseVehicleOnPolicy, - isVehicleLookupValidations, isMoldingQuestion, isNonServiceable, isNonServiceableVin, isRecalVehicle } = testCase.testData; + isVehicleLookupValidations, isMoldingQuestion, isNonServiceable, isNonServiceableVin, isRecalVehicle, isCanSafeliteRecalibrate, isRepairTPA } = testCase.testData; let { isPolicyFound } = testCase.testData; // Allow isPolicyFound to be re-assigned @@ -573,11 +571,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { }); await test.step('Click Invalid vin Link', async () => { - await vinLookupPage.triggerBailout(); - }); - - await test.step('BailoutPage >> Validate Bailout-' + BailoutCode.VehicleNotFound, async () => { - await bailoutPage.validateBailoutDetails(customerDetails!, BailoutCode.VehicleNotFound); + await vinLookupPage.returnToVehicleLookupPage(); }); return; @@ -649,14 +643,12 @@ async function runWorkflow(page: Page, testCase: TestCase) { if (capabilityQuestions && capabilityQuestions.length > 0) { await capabilityQuestionsPage.validateURL(capabilityQuestionsPage.issPageValue); - await capabilityQuestionsPage.validatePartQuestions(capabilityQuestions); await capabilityQuestionsPage.selectPartQuestionResponses(capabilityQuestions); await capabilityQuestionsPage.nextPage(); } if (partQuestions && partQuestions.length > 0) { await partQuestionsPage.validateURL(partQuestionsPage.issPageValue); - await partQuestionsPage.validatePartQuestions(partQuestions); await partQuestionsPage.selectPartQuestionResponses(partQuestions); await partQuestionsPage.nextPage(); } @@ -740,7 +732,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { await expect(async () => { await providerPreferencePage.stateLawModalOkayButton.waitFor({ state: 'visible' }); await providerPreferencePage.stateLawModalOkayButton.click(); - }).toPass({ timeout: 30000 }); + }).toPass({ timeout: 60_000 }); }); } @@ -796,6 +788,13 @@ async function runWorkflow(page: Page, testCase: TestCase) { await providerPreferencePage.scheduleTPAWithoutAdas(); }); } + + if (!isSafelite && isRepairTPA) { + await test.step('ProviderPreferencePage >> Schedule repair with TPA without Adas', async () => { + await providerPreferencePage.validateURL(providerPreferencePage.issPageValue); + await providerPreferencePage.scheduleTPAWithoutAdas(); + }); + } } // TPA Flow if (!isPolicyFound || !(isItac || isNoComp) || isUnverifiedPolicyAfterVehicleLookup) { @@ -837,13 +836,15 @@ async function runWorkflow(page: Page, testCase: TestCase) { await test.step('SchedulePage >> Select day and time', async () => { await schedulePage.validateURL(schedulePage.issPageValue); - if (appointmentDetails?.serviceLocation === ServiceLocation.Mobile) { - await schedulePage.scheduleMobile(appointmentDetails); + if (isCanSafeliteRecalibrate === false) { + await schedulePage.validateRecalAcknowledgementModal(); + await schedulePage.scheduleInShop(appointmentDetails!); + } else if (appointmentDetails?.serviceLocation === ServiceLocation.InShop || appointmentDetails?.serviceLocation === ServiceLocation.DropOff) { + await schedulePage.scheduleInShop(appointmentDetails!); + } else { + await schedulePage.scheduleMobile(appointmentDetails!); } - if (appointmentDetails?.serviceLocation === ServiceLocation.InShop || appointmentDetails?.serviceLocation === ServiceLocation.DropOff) { - await schedulePage.scheduleInShop(appointmentDetails); - } }); @@ -878,7 +879,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { if (isPolicyFound && (isUseVehicleOnPolicy ?? true) && claimDetails!.policyDeductible > 0) { await test.step('PaymentMethodPage >> Execute Payment', async () => { await paymentMethodPage.validateURL(paymentMethodPage.issPageValue); - await paymentMethodPage.executePayment(paymentDetails!, claimDetails!, servicePackage!); + await paymentMethodPage.executePayment(paymentDetails!, paymentDetailsAdyen!, claimDetails!, servicePackage!); await paymentMethodPage.nextPage(); }); diff --git a/playwright-tests/tests/0020_EssentialVehicleLookupBailout.ts b/playwright-tests/tests/0020_EssentialPartQuestionsAndNotShareVin.ts similarity index 65% rename from playwright-tests/tests/0020_EssentialVehicleLookupBailout.ts rename to playwright-tests/tests/0020_EssentialPartQuestionsAndNotShareVin.ts index a70e8da0..1d97684b 100644 --- a/playwright-tests/tests/0020_EssentialVehicleLookupBailout.ts +++ b/playwright-tests/tests/0020_EssentialPartQuestionsAndNotShareVin.ts @@ -1,23 +1,31 @@ import ClientData from "@business-logic/data/ClientData"; import TestCase from "@business-logic/types/TestCase"; -import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType } from "@business-logic/types/Enums"; +import { DamageType, ServiceLocation, ServicePackage, VehicleDamage, VehicleLookupType, PartQuestionType } from "@business-logic/types/Enums"; import { ITestData } from "@business-logic/types/ITestData" import { faker } from "@faker-js/faker"; import { getNextWeekday } from "@impl/utils/DateUtils"; const nextWeekday = getNextWeekday(); -const essentialVehicleLookupBailoutData: Partial = { +const essentialPartQuestionsAndNotShareVinData: Partial = { clientTag: 'ALL_ESSENTIAL', isDuplicateClaim: false, isPolicyFound: false, endorsements: [], isReplace: false, - partQuestions: undefined, + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + }, + { + partQuestionType: PartQuestionType.GeneralQuestion2, + isOnPage: true, + optionToSelect: 'Yes' + } + ], isSafelite: true, - bailoutFlags: { - isVehicleLookupBailout: true - }, servicePackage: faker.helpers.enumValue(ServicePackage), customerDetails: { firstName: faker.person.firstName(), @@ -43,19 +51,10 @@ const essentialVehicleLookupBailoutData: Partial = { year: '2020', make: 'BMW', model: '740', - style: '4 door sedan' // TODO: Check correctness of vehicle style + style: '4 door sedan', + vehicleLookupType: VehicleLookupType.RatherNotShareVin, }, - vehicleDamage: [ - // VehicleDamage.WindshieldThreeChips, - VehicleDamage.WindshieldCrack, - // VehicleDamage.DriverFrontDoor, - // VehicleDamage.DriverQuarterPanel, - // VehicleDamage.DriverRearDoor, - // VehicleDamage.PassengerFrontDoor, - // VehicleDamage.PassengerQuarterPanel, - // VehicleDamage.PassengerRearDoor, - // VehicleDamage.RearWindow - ], + vehicleDamage: [VehicleDamage.WindshieldCrack], appointmentDetails: { serviceLocation: ServiceLocation.InShop, shopAddress: '6826 Sawmill Rd, Columbus, OH 43235', @@ -65,17 +64,17 @@ const essentialVehicleLookupBailoutData: Partial = { } const essentialClients = ClientData.getEssentialClients(); -const essentialVehicleLookupBailoutTests: TestCase[] = []; +const essentialPartQuestionsAndNotShareVinTests: TestCase[] = []; for (const client of essentialClients) { - const data = {...essentialVehicleLookupBailoutData}; + const data = {...essentialPartQuestionsAndNotShareVinData}; data.clientTag = client.clientTag; data.isAuthenticationRequired = client.clientFlags.isAuthenticationEnabled ?? false const tc = new TestCase({ - name: `0020 Essential Vehicle Lookup Bailout Client: "${client.accountName}"`, - tags: [`@${client.clientTag}`, `@${client.accountName}`, '@Bailout', '@VehicleLookup', '@Essentials'], + name: `0020 Essential Part Questions and Not Share Vin Client: "${client.accountName}"`, + tags: [`@${client.clientTag}`, `@${client.accountName}`, '@VehicleLookup', '@Essentials'], testData: data }, undefined, '0020'); - essentialVehicleLookupBailoutTests.push(tc); + essentialPartQuestionsAndNotShareVinTests.push(tc); } -export default essentialVehicleLookupBailoutTests; \ No newline at end of file +export default essentialPartQuestionsAndNotShareVinTests; \ No newline at end of file diff --git a/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts b/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts index f3014dee..3d4da669 100644 --- a/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts +++ b/playwright-tests/tests/0022_EssentialServiceableBigTruck.ts @@ -13,9 +13,6 @@ const essentialServiceableBigTruckData: Partial = { isPolicyFound: false, endorsements: [], isReplace: true, - bailoutFlags: { - isHeavyTruckVehicleBailout: true, - }, vehiclePartQuestions: [ { partQuestionType: PartQuestionType.WindshieldColor, @@ -24,6 +21,7 @@ const essentialServiceableBigTruckData: Partial = { }, ], isSafelite: true, + isCanSafeliteRecalibrate: false, servicePackage: faker.helpers.enumValue(ServicePackage), customerDetails: { firstName: faker.person.firstName(), diff --git a/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts b/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts index 0a7c5d4d..baa6a433 100644 --- a/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts +++ b/playwright-tests/tests/advanced/0001a_ReplaceInShopCredit.ts @@ -59,7 +59,7 @@ const advancedScenario0001Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getCreditCardDetailsAdyen() } // TODO: Add validation for deductible/covered amount diff --git a/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts b/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts index 07adbdf3..c124598f 100644 --- a/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts +++ b/playwright-tests/tests/advanced/0002a_ReplaceOemEndorsement.ts @@ -106,7 +106,7 @@ const advancedScenario0002Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts b/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts index 6243fa2e..9ea2e07b 100644 --- a/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts +++ b/playwright-tests/tests/advanced/0003a_MobileAfterpay.ts @@ -74,7 +74,7 @@ const advancedScenario0003Data: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0008a_RepairTpa.ts b/playwright-tests/tests/advanced/0008a_RepairTpa.ts index 85b353be..621ea38d 100644 --- a/playwright-tests/tests/advanced/0008a_RepairTpa.ts +++ b/playwright-tests/tests/advanced/0008a_RepairTpa.ts @@ -33,20 +33,8 @@ const advancedScenario0008Data: Partial = { isDuplicateClaim: false, isPolicyFound: true, isNoComp: false, - hasStateLawPopup: true, - endorsements: undefined, - vehiclePartQuestions: [ - // { - // partQuestionType: PartQuestionType.WindshieldColor, - // isOnPage: true, - // optionToSelect: 'Green Tint' - // }, - // { - // partQuestionType: PartQuestionType.PassengerRearColor, - // isOnPage: true, - // optionToSelect: 'Green Tint' - // }, - ], + hasStateLawPopup: false, + isRepairTPA: true, isSafelite: false, servicePackage: faker.helpers.enumValue(ServicePackage), customerDetails: customerDetails, diff --git a/playwright-tests/tests/advanced/0010a_RearGlass.ts b/playwright-tests/tests/advanced/0010a_RearGlass.ts index c793e224..bc4e0c8f 100644 --- a/playwright-tests/tests/advanced/0010a_RearGlass.ts +++ b/playwright-tests/tests/advanced/0010a_RearGlass.ts @@ -54,7 +54,7 @@ const advancedScenario0010Data: Partial = { // }, ], isSafelite: true, - servicePackage: faker.helpers.enumValue(ServicePackage), + servicePackage: ServicePackage.Premium, customerDetails: customerDetails, claimDetails: { policyNumber: policyNumber, diff --git a/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts b/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts index 8bffcbf0..7d3a1b04 100644 --- a/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts +++ b/playwright-tests/tests/advanced/0011a_ItacNoAdas.ts @@ -94,7 +94,7 @@ const advancedScenario0011Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0012a_ItacDropOff.ts b/playwright-tests/tests/advanced/0012a_ItacDropOff.ts index 213ec490..9f393b66 100644 --- a/playwright-tests/tests/advanced/0012a_ItacDropOff.ts +++ b/playwright-tests/tests/advanced/0012a_ItacDropOff.ts @@ -44,7 +44,7 @@ const advancedScenario0012Data: Partial = { ], partQuestions: undefined, isSafelite: true, - servicePackage: faker.helpers.enumValue(ServicePackage), + servicePackage: ServicePackage.GlassOnly, customerDetails: customerDetails, claimDetails: { policyNumber: policyNumber, @@ -68,7 +68,7 @@ const advancedScenario0012Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0014a_NoCompAdas.ts b/playwright-tests/tests/advanced/0014a_NoCompAdas.ts index 919833c0..a88d2ba9 100644 --- a/playwright-tests/tests/advanced/0014a_NoCompAdas.ts +++ b/playwright-tests/tests/advanced/0014a_NoCompAdas.ts @@ -68,7 +68,7 @@ const advancedScenario0014Data: Partial = { alternateServiceZip: '43016', appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts b/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts index e336e3ea..23796265 100644 --- a/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts +++ b/playwright-tests/tests/advanced/0016a_NoCompAllGlass.ts @@ -103,7 +103,7 @@ const advancedScenario0016Data: Partial = { serviceAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts b/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts index 94619a0b..9cb32f2a 100644 --- a/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts +++ b/playwright-tests/tests/advanced/0018a_NoCompGlassOnly.ts @@ -71,7 +71,7 @@ const advancedScenario0018Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultAfterpayDetails() + paymentDetailsAdyen: ClientData.getAfterpayDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts b/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts index d9b4c818..48ce03b8 100644 --- a/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts +++ b/playwright-tests/tests/advanced/0019a_NoCompEditVehicle.ts @@ -73,7 +73,7 @@ const advancedScenario0019Data: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts b/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts index 358dfbd5..56e5888b 100644 --- a/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts +++ b/playwright-tests/tests/advanced/0028a_ItacCancelMyClaim.ts @@ -72,7 +72,7 @@ const advancedScenario0028aData: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getCreditCardDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts b/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts index e392adc2..295c3aed 100644 --- a/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts +++ b/playwright-tests/tests/advanced/0030a_APIErrorBailout.ts @@ -67,7 +67,7 @@ const advancedScenario0030aData: Partial = { serviceAddress: customerAddress, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getCreditCardDetailsAdyen() } diff --git a/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts b/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts index b4fb227e..bf4218e5 100644 --- a/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts +++ b/playwright-tests/tests/advanced/0031a_ReplaceServiceableBigTruck.ts @@ -31,6 +31,7 @@ const advancedScenario0031Data: Partial = { isDuplicateClaim: false, isPolicyFound: true, isNoComp: false, + isCanSafeliteRecalibrate: false, hasStateLawPopup: false, endorsements: undefined, vehiclePartQuestions: [ @@ -65,7 +66,7 @@ const advancedScenario0031Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails()//ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen() } // TODO: Add validation for deductible/covered amount diff --git a/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts b/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts index b2433ced..20185f72 100644 --- a/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts +++ b/playwright-tests/tests/advanced/0032a_DeclineNonServiceableBigTruck.ts @@ -63,7 +63,7 @@ const advancedScenario0032Data: Partial = { shopAddress: undefined, appointmentDate: nextWeekday }, - paymentDetails: ClientData.getDefaultPaypalDetails(),//ClientData.getDefaultCreditCardDetails() + paymentDetailsAdyen: ClientData.getPaypalDetailsAdyen(), isNonServiceable: true, } diff --git a/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json b/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json index f3d25831..fffb8f9b 100644 --- a/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json +++ b/playwright-tests/tests/mockResponses/0002a_Advanced_Replace_Deductible_Client/experiments/api/v1/experiments/run.json @@ -14,6 +14,21 @@ "settings": { "DisplayPIAInsurance": "true" } + }, + { + "universeName": "NextGenAdyenPaymentTest", + "universeId": 848, + "testName": "NextGenAdyenPaymentIntegration_V1", + "testId": 727, + "variationName": "YesShowAdyenPaymentIntergration_V1_TEST", + "variationId": 1855, + "isActive": true, + "isExposed": true, + "userPartitionNumber": 26, + "assignmentId": 16698635, + "settings": { + "ISS_Enable_Adyen_V1": "true" + } } ] } \ No newline at end of file diff --git a/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json b/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json index 38a3b7f8..0472dc19 100644 --- a/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json +++ b/playwright-tests/tests/mockResponses/common/experiments/api/v1/experiments/run.json @@ -14,6 +14,21 @@ "settings": { "DisplayPIAInsurance": "true" } + }, + { + "universeName": "NextGenAdyenPaymentTest", + "universeId": 848, + "testName": "NextGenAdyenPaymentIntegration_V1", + "testId": 727, + "variationName": "YesShowAdyenPaymentIntergration_V1_TEST", + "variationId": 1855, + "isActive": true, + "isExposed": true, + "userPartitionNumber": 26, + "assignmentId": 16698635, + "settings": { + "ISS_Enable_Adyen_V1": "true" + } } ] } \ No newline at end of file diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 8cf147e8..dc023b15 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,3 +1,6 @@ +import packageNames from '@/constants/package-names'; +import { paymentMethods } from '@/constants/payment-method-constants'; + const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', EVENT: 'EVENT' @@ -40,4 +43,18 @@ const ValueToLogTypes = Object.freeze({ LAST_5: 'last_5' }); -export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes }; +const analyticsPaymentTypeMap = new Map([ + [paymentMethods.AFTERPAY, 'after_pay'], + [paymentMethods.CREDIT_CARD, 'credit_card'], + [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'], + [paymentMethods.PAYPAL, 'pay_pal'], + [paymentMethods.APPLEPAY, 'apple_pay'], +]); + +const analyticsServicePackageMap = new Map([ + [packageNames.TIER_ONE, 'basic'], + [packageNames.TIER_TWO, 'essentials'], + [packageNames.TIER_THREE, 'essentialsplus'] +]); + +export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap }; diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index d18d073e..59515bd3 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -1,5 +1,3 @@ -import applicationConfig from './application-config'; - const ACCOUNT_BASE_URL = '/account/api/v1/account'; const ANALYTICS_BASE_URL = '/analytics/api/v1/analytics'; const CLIENT_AUTH_BASE_URL = '/clientauth/api/v1/clientauth'; @@ -8,19 +6,13 @@ const COVERAGE_BASE_URL = '/coverage/api/v1/coverage'; const EXPERIMENTS_BASE_URL = '/experiments/api/v1/experiments'; const LOCATION_BASE_URL = '/location/api/v1/location'; const ORDER_BASE_URL = '/order/api/v1/order'; -const PARTS_BASE_URL = '/parts/api/v2/parts'; const PARTS_V1_BASE_URL = '/parts/api/v1/parts'; +const PARTS_V2_BASE_URL = '/parts/api/v2/parts'; const PRICE_BASE_URL = '/price/api/v1/price'; const SCHEDULE_BASE_URL = '/schedule/api/v1/schedule'; const VEHICLE_BASE_URL = '/vehicle/api/v1/vehicle'; const PAYMENT_BASE_URL = '/payment/api/v1/payment'; -const isLocalOrDev = (() => { - const currentEnv = (applicationConfig?.CURRENT_ENVIRONMENT || '').toLowerCase(); - return currentEnv === 'localhost' || currentEnv === 'dev'; -})(); -const PARTS_EFFECTIVE_BASE_URL = isLocalOrDev ? PARTS_BASE_URL : PARTS_V1_BASE_URL; - const endpoints = Object.freeze({ GetRouteInfo: { url: (applicationAbbreviation) => `${CONTENT_BASE_URL}/${applicationAbbreviation}/RouteInfo`, @@ -51,7 +43,7 @@ const endpoints = Object.freeze({ method: 'POST' }, GetMobilePremiumFee: { - url: `${PARTS_EFFECTIVE_BASE_URL}/mobile-premium-fee`, + url: `${PARTS_V1_BASE_URL}/mobile-premium-fee`, method: 'GET' }, GetVehicleYears: { @@ -71,15 +63,15 @@ const endpoints = Object.freeze({ method: 'GET' }, GetDamageOptions: { - url: `${PARTS_EFFECTIVE_BASE_URL}/damage-options`, + url: `${PARTS_V1_BASE_URL}/damage-options`, method: 'GET' }, GetPartsOrQuestions: { - url: `${PARTS_EFFECTIVE_BASE_URL}/parts-or-questions`, + url: `${PARTS_V1_BASE_URL}/parts-or-questions`, method: 'POST' }, GetParts: { - url: `${PARTS_EFFECTIVE_BASE_URL}/parts`, + url: `${PARTS_V1_BASE_URL}/parts`, method: 'POST' }, GetITACPriceOrderItems: { @@ -108,19 +100,19 @@ const endpoints = Object.freeze({ method: 'GET' }, GetCapabilityQuestions: { - url: `${PARTS_EFFECTIVE_BASE_URL}/capability-questions`, + url: `${PARTS_V1_BASE_URL}/capability-questions`, method: 'GET' }, GetPartFromCapabilityAnswer: { - url: `${PARTS_EFFECTIVE_BASE_URL}/part-from-capability-answer`, + url: `${PARTS_V1_BASE_URL}/part-from-capability-answer`, method: 'POST' }, GetWipers: { - url: `${PARTS_EFFECTIVE_BASE_URL}/wipers`, + url: `${PARTS_V1_BASE_URL}/wipers`, method: 'GET' }, GetRainDefense: { - url: `${PARTS_EFFECTIVE_BASE_URL}/rain-repel`, + url: `${PARTS_V1_BASE_URL}/rain-repel`, method: 'GET' }, GetRecalParts: { @@ -132,19 +124,19 @@ const endpoints = Object.freeze({ zipCode, applicationName, referralSequenceNumber - ) => `${PARTS_EFFECTIVE_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, + ) => `${PARTS_V1_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`, method: 'GET' }, GetGlassFees: { - url: `${PARTS_EFFECTIVE_BASE_URL}/glass-fees`, + url: `${PARTS_V1_BASE_URL}/glass-fees`, method: 'GET' }, GetSupportingItems: { - url: `${PARTS_EFFECTIVE_BASE_URL}/supporting-items`, + url: `${PARTS_V1_BASE_URL}/supporting-items`, method: 'POST' }, GetMobileFeePart: { - url: `${PARTS_EFFECTIVE_BASE_URL}/mobile-fee`, + url: `${PARTS_V2_BASE_URL}/mobile-fee`, method: 'GET' }, GetServiceabilityDetails: { diff --git a/src/helpers/object-helper.js b/src/helpers/object-helper.js index 37ee46be..e142c236 100644 --- a/src/helpers/object-helper.js +++ b/src/helpers/object-helper.js @@ -86,3 +86,17 @@ export function getPropertyCaseInsensitive(obj, property) { while (prop = props.pop()) if (prop.toLowerCase() === property.toLowerCase()) return prop; return null; } + +export function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) { + return (array ?? []).map((x) => x[propertyName]).filter((x) => x); +} + +export function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) { + if (!arrayOfObjects) return null; + + return arrayOfObjects.sort((a, b) => { + if (a[propertyName] < b[propertyName]) return -1; + if (a[propertyName] > b[propertyName]) return 1; + return 0; + }); +} diff --git a/src/helpers/recal-helper.js b/src/helpers/recal-helper.js index f364bf68..029aee25 100644 --- a/src/helpers/recal-helper.js +++ b/src/helpers/recal-helper.js @@ -1,5 +1,5 @@ import partTypeStrings from '@/constants/part-type-strings'; -import { deepClone } from '@/helpers/object-helper'; +import { deepClone, getNonFalseValuesOfPropertyInArrayOfObjects } from '@/helpers/object-helper'; const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION]; @@ -73,6 +73,31 @@ export function containsRecalParts(lineItems) { } } +export function isRecalOrder(lineItems) { + return (containsRecalParts(lineItems) && getHasRecalibrationPart(lineItems)); +} + +export function getHasRecalibrationPart(lineItems) { + const hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(lineItems.glassParts, 'requiresRecalibration')?.length > 0; + const hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(lineItems.glassParts, 'recalibrationType')?.length > 0; + + if (hasRequiresRecalibration) { + if (hasRecalibrationType) { + // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType' + return ( + getNonFalseValuesOfPropertyInArrayOfObjects( + lineItems.glassParts, + 'recalibrationType' + )[0].toLowerCase() !== 'unknown' + ); + } + // Has 'requiresRecalibration' but no 'recalibrationType' at all + return true; + } + // Does not have 'requiresRecalibration' + return false; +} + export function anyPartWithRequiresRecalFlag(lineItems) { if (!lineItems) { return false; diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index be64fdc8..9438b038 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -14,6 +14,8 @@ import coverageType from '@/constants/coverage-type'; import { deepClone } from '@/helpers/object-helper'; import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import { paymentMethods } from '@/constants/payment-method-constants'; +import partTypeStrings from '@/constants/part-type-strings'; +import packageNames from '@/constants/package-names'; jest.mock('@/helpers/layout-helper.js', () => jest.fn()); @@ -27,7 +29,9 @@ jest.mock('@/helpers/cms-content-helper', () => ({ const mockMixin = { methods: { getCmsContent: jest.fn(), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() } }; @@ -152,7 +156,8 @@ const sessionStorage = { currentDeductible: { replace: 100, repair: 0 - } + }, + isUnverified: false }; function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}, mixin = mockMixin) { @@ -778,7 +783,8 @@ describe('OrderConfirmation.vue', () => { } return ''; }), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; }); @@ -827,7 +833,8 @@ describe('OrderConfirmation.vue', () => { } return ''; }), - setCmsContent: jest.fn() + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn() } }; }); @@ -955,6 +962,251 @@ describe('OrderConfirmation.vue', () => { }); }); describe('Methods', () => { + describe('handleAnalyticsEvents', () => { + let mixin; + beforeEach(() => { + mixin = { + methods: { + getCmsContent: jest.fn(), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + }); + test('should call pushEventToGA with confirmation event for mobile repair', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; + order.damage.isRepair = true; + order.isVerified = false; + order.isMobileAppointment = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('mobile'), true); + }); + test('should call pushEventToGA with confirmation event for in-shop replace', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; + order.damage.isRepair = false; + order.isVerified = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('replace'), true); + }); + test('should call pushEventToGA for payment PIA successful when payment.isPayInAdvance is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = true; + order.payment.paymentMethod = paymentMethods.CREDIT_CARD; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('payment_page', 'pia_successful', expect.any(String), true); + }); + test('should not call pushEventToGA for PIA when payment.isPayInAdvance is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = false; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const paymentPageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'payment_page'); + expect(paymentPageCalls.length).toBe(0); + }); + test('should call pushEventToGA for service package when submittedOrder.servicePackage exists', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = packageNames.TIER_ONE; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('service_package', 'package_purchased', expect.any(String), true); + }); + test('should not call pushEventToGA for service package when submittedOrder.servicePackage is null', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = null; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const servicePackageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'service_package'); + expect(servicePackageCalls.length).toBe(0); + }); + test('should call pushEventToGA for total price when isFirstLoad is true and cart total greater than 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // Act + wrapper.vm.handleAnalyticsEvents(true); + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('total_price', expectedTotal.toString(), expect.any(String), true); + }); + test('should not call pushEventToGA for total price when isFirstLoad is false', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + useMainStore().pageData = jest.fn().mockReturnValue({ isFirstLoad: false }); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should not call pushEventToGA for total price when cart total is 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 0; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }]; + order.insuranceCoverage.coverageType = coverageType.ITAC; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should call pushEventToGA for rain defense purchased when hasRainRepel is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.RAIN_DEFENSE }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for rain defense no_purchase when hasRainRepel is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'no_purchase', expect.any(String), true); + }); + test('should call pushEventToGA for visitor_info_confirmation with ClientSite referring_site', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + }); + test('should call pushEventToGA for visitor_info_confirmation with client_name', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'client_name', expect.any(String), true); + }); + test('should call pushEventToGA for wipers purchased when hasWipers is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.FRONT_WIPER }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const mixin = { + methods: { + getCmsContent: jest.fn().mockReturnValue([{ Text: 'Front Beam' }]), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for wipers no_purchase when hasWipers is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true); + }); + }); describe('formatDate', () => { test.each([ ['Wednesday, April 22, 2020', '2020-04-22'], diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index 21ce4fc7..063346f2 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -55,7 +55,7 @@ :isRepair="isRepair" />
{{ wipersTitle }} {{ description }}
{{ rainRepelTitle }} {{ rainRepelBody }} @@ -135,6 +135,10 @@ import widgetFields from '@/constants/cms-widget-fields.js'; import { getGlassList } from '@/helpers/damage-helper'; import partTypeStrings from '@/constants/part-type-strings'; import coverageType from '@/constants/coverage-type'; +import { analyticsPaymentTypeMap, analyticsServicePackageMap } from '@/constants/analytics'; +import { containsRecalParts } from '@/helpers/recal-helper'; +import issPageValues from '@/router/router-constants/issPage-values'; +import { getCartTotal } from '@/helpers/cart-helper'; export default { name: 'order-confirmation', @@ -199,6 +203,7 @@ export default { hasRecalibrationPart, referralNumber: referralNumber?.toString(), isNoComp: this.submittedOrder.insuranceCoverage.coverageType === coverageType.NO_COMP, + isITAC: this.submittedOrder.insuranceCoverage.coverageType === coverageType.ITAC, widgets: { siteHeader: 'SiteHeaderWidget', emailConfirmation: 'EmailConfirmationWordingWidget', @@ -418,10 +423,16 @@ export default { // Temporarily set return true to see cart in localhost or dev environment return false; }, - displayWipers() { + hasWipers() { const hasWiperPart = this.submittedOrder.lineItems.vaps.some((part) => part.partType.toLowerCase().includes('wiper')); return hasWiperPart; }, + hasFrontWiper() { + return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER); + }, + hasRearWiper() { + return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER); + }, wipersTitle() { return this.getCmsContent( this.widgets.wipersText, @@ -431,13 +442,10 @@ export default { wipersBody() { const wiperTypesOnOrder = []; - const hasFrontWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER); - const hasRearWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER); - - if (hasFrontWiper) { + if (this.hasFrontWiper) { wiperTypesOnOrder.push(partTypeStrings.FRONT_WIPER); } - if (hasRearWiper) { + if (this.hasRearWiper) { wiperTypesOnOrder.push(partTypeStrings.REAR_WIPER); } @@ -447,7 +455,7 @@ export default { return wiperDescriptions; }, - displayRainRepel() { + hasRainRepel() { return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.RAIN_DEFENSE); }, rainRepelTitle() { @@ -508,6 +516,10 @@ export default { if (this.carrierUrl) { this.$refs.siteFooter.updateButtonText(`Go back to ${this.carrierName}`); } + + const isFirstLoad = this.mainStore.pageData(issPageValues.ORDER_CONFIRMATION)?.isFirstLoad ?? true; + this.savePageDataToStore(issPageValues.ORDER_CONFIRMATION, { isFirstLoad: false }); + this.handleAnalyticsEvents(isFirstLoad); }, methods: { arePagePrerequisitesValid() { @@ -611,6 +623,72 @@ export default { const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsPartType); return vapsTypeDescription?.Text ?? ''; + }, + handleAnalyticsEvents(isFirstLoad) { + const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop'; + const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified'; + const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; + this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true); + + if (this.payment.isPayInAdvance) { + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.payment.paymentMethod); + this.pushEventToGA('payment_page', 'pia_successful', paymentMethodForAnalytics, true); + } + + if (this.submittedOrder.servicePackage) { + const servicePackageForAnalytics = analyticsServicePackageMap.get(this.submittedOrder.servicePackage); + this.pushEventToGA('service_package', 'package_purchased', servicePackageForAnalytics, true); + } + + if (containsRecalParts(this.submittedOrder.lineItems)) { + let coverageType = ''; + if (this.isITAC) { + coverageType = 'ITAC'; + } else if (this.isNoComp) { + coverageType = 'no_comp'; + } else if (this.submittedOrder.isVerified) { + coverageType = 'verified'; + } else { + coverageType = 'unverified'; + } + const recalibrationType = this.submittedOrder.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType; + this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true); + } + + const ymms = `${this.vehicle.year}_${this.vehicle.make}_${this.vehicle.model}_${this.vehicle.style}`; + if (isFirstLoad && getCartTotal(this.submittedOrder) > 0) { + this.pushEventToGA('total_price', getCartTotal(this.submittedOrder).toString(), ymms, true); + } + + this.pushEventToGA('rain_defense', this.hasRainRepel ? 'purchased' : 'no_purchase', ymms, true); + + // TODO: Check if we're coming from SFA and add relevant events + // eslint-disable-next-line + if (false) { + this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'SFA', null); + } + else { + this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + } + + this.pushEventToGA('visitor_info_confirmation', 'client_name', this.mainStore.accountNameForEvents, true); + + const wiperAction = this.hasWipers ? 'purchased' : 'no_purchase'; + + let wiperLabel = this.wipersBody.join('_').toLowerCase().replace('
', '').replace(/beam/g, '').replace(/blades/g, '').trim().replace(/ /g, '_'); + if (wiperLabel.indexOf('front') < 0 && wiperLabel.indexOf('rear') < 0) { + wiperLabel = 'none_none'; + } else { + if (wiperLabel.indexOf('front') < 0) { + wiperLabel = 'front_none_' + wiperLabel; + } + if (wiperLabel.indexOf('rear') < 0) { + wiperLabel = wiperLabel + '_rear_none'; + } + } + wiperLabel = wiperLabel.replace(/__/g, '_'); + + this.pushEventToGA('wipers', wiperAction, wiperLabel, true); } } }; diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue index 91462f51..8608bc29 100644 --- a/src/layouts/payment-page-adyen/payment-page-adyen.vue +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -75,6 +75,7 @@ import { mapAdyenToIssPaymentMethod, mapIssToAdyenPaymentMethod } from '@/helper import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { Dropin } from "@adyen/adyen-web/auto"; import applicationConfig from '@/constants/application-config'; +import { analyticsPaymentTypeMap } from '@/constants/analytics'; export default { name: 'payment-page-adyen', @@ -118,6 +119,7 @@ export default { }, mounted() { showIssLoadingModal(true); + this.pushEventToGA('payment_page', 'Mode', 'Adyen', true); this.initializeAdyen().finally(() => { showIssLoadingModal(false); }); @@ -353,6 +355,8 @@ export default { }, async paymentFailedPayLater() { this.showIssLoadingModal(true); + const paymentMethod = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', 'save_your_appointment_clicked', `${paymentMethod}_declined`, true); this.mainStore.savePaymentMethodChoice(paymentMethods.PAY_AT_TIME_OF_SERVICE); await submitWorkOrder({ submitType: submitType.SAFELITE }); this.$router.navigate( @@ -523,6 +527,8 @@ export default { const paymentMethodFromSession = adyenResponse?.paymentMethod; const paymentMethod = mapAdyenToIssPaymentMethod(paymentMethodFromSession); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); this.mainStore.savePaymentMethodChoice(paymentMethod); @@ -544,6 +550,10 @@ export default { await global.$logger.logError(stringToLog); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); + this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); + this.hasPaymentFailureError = true; } @@ -556,6 +566,9 @@ export default { const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`; await global.$logger.logError(stringToLog); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); + this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); this.hasPaymentFailureError = true; } diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index d306857d..ce1ae7e5 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -18,6 +18,7 @@ import navigationScenarios from '@/router/router-constants/navigation-scenarios' import submitType from '@/constants/submit-type'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; import { paymentMethods } from '@/constants/payment-method-constants'; +import { analyticsPaymentTypeMap } from '@/constants/analytics'; export default { name: 'payment-return-adyen', @@ -58,6 +59,7 @@ export default { const sessionInfo = await getSessionInfo(sessionId, result.sessionResult); const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod); + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod); const amountDue = getCartTotal(store.order); const ccToken = generateCcToken(sessionInfo); if (paymentMethod === paymentMethods.AFTERPAY) { @@ -67,6 +69,7 @@ export default { ccToken.expMonth = "03"; ccToken.expYear = "2030"; } + this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0); store.savePaymentMethodChoice(paymentMethod); store.updateCreditCardToken(ccToken); @@ -95,6 +98,11 @@ export default { } ); }, + computed: { + piaType() { + return this.mainStore.payment.paymentMethod; + } + }, methods: { finalizeAdyenPayment(sessionId, redirectResult) { return new Promise((resolve, reject) => { @@ -111,6 +119,10 @@ export default { const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`; global.$logger.logError(stringToLog); + + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); + this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); } reject({ @@ -123,6 +135,10 @@ export default { const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`; global.$logger.logError(stringToLog); + + const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); + this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); } reject({ diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index 9833cdcd..42ae3603 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -487,6 +487,7 @@ export default { logEvents() { const vehicleString = this.mainStore.order.vehicle.make + '_' + this.mainStore.order.vehicle.model + '_' + this.mainStore.order.vehicle.style; this.pushEventToGA("CAR SUBMISSION", this.mainStore.order.vehicle.year?.toString(), vehicleString, true, null, 0); + this.pushEventToGA('forward_progress', 'continue_clicked', 'vehicle_damage_2', true); if (this.isWindshieldRepair) { this.pushEventToGA("damage", "selected", "repair", true, null, null); diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index b619b6b4..81d63e41 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -21,9 +21,10 @@ import { ValueToLogTypes } from '@/constants/analytics'; import { getCartTotal, getSubtotal } from '@/helpers/cart-helper'; -import { getRecalPartNumbers } from "@/helpers/recal-helper"; +import { getRecalPartNumbers, isRecalOrder } from "@/helpers/recal-helper"; import coverageStatuses from '@/constants/coverage-statuses'; import coverageType from '@/constants/coverage-type'; +import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import issPageValues from '@/router/router-constants/issPage-values'; import { useMainStore } from '@/store'; @@ -128,11 +129,6 @@ export default { }, pushValueToGA() { - const gaSiteType = { - ['siteType']: useMainStore().issConfig.siteType - }; - this.pushGenericObjectToGA(gaSiteType); - const gaServiceType = { ['service_type']: useMainStore().order?.serviceLocation?.appointmentType?.toLowerCase() }; @@ -140,6 +136,195 @@ export default { this.pushGenericObjectToGA(gaServiceType); } }, + + pushOrderToDataLayer() { + // helper check for if an object is defined (but maybe falsey) + const isDefined = (x) => x !== null && x !== undefined; + const store = useMainStore(); + + // Get correct order object + const hasSubmittedOrder = store.hasSubmittedOrder(); + const submittedOrder = store.getSubmittedOrder(); + const order = hasSubmittedOrder ? submittedOrder : store.order; + const deviceId = getDeviceIdValue(); + const sid = getSessionIdValue(); + + // Begin assembling payload for data layer + const payload = {}; + + payload.appName = "ISS"; + payload.siteType = store.issConfig.siteType; + payload.pageName = this.getPageNameByQueryString(); + payload.deviceId = deviceId; + payload.sessionId = sid; + payload.clientName = store.issConfig.clientName; + payload.lossCause = order.policy?.damageCause ?? ""; + + // Service Zip + if ( + order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE && + isDefined(order.serviceLocation.zipCode) + ) { + payload.serviceZipCode = order.serviceLocation.zipCode; + } else if ( + isDefined(order.serviceLocation.appointmentType) && + order.serviceLocation.appointmentType !== AppointmentTypeStrings.MOBILE && + isDefined(order.serviceLocation.provider.address.zipCode) + ) { + payload.serviceZipCode = order.serviceLocation.provider.address.zipCode; + } else { + payload.serviceZipCode = ""; + } + + // Damage Type + if (isDefined(order.damage.isRepair)) { + payload.damageType = order.damage.isRepair ? "repair" : "replace"; + } else { + payload.damageType = ""; + } + + // Account Type - always insurance for ISS + payload.accountType = "insurance"; + + // Promo Codes + const promos = order.lineItems.promos ?? []; + if (promos.length === 0) { + payload.promoCodes = ""; + } else { + const promoCodes = promos.map((promo) => promo.promoCode); + const promoString = promoCodes.reduce((prev, next) => `${prev},${next}`); + payload.promoCodes = promoString; + } + + // Vehicle info + if (isDefined(order.vehicle.year)) { + // Ensure cast to string. + payload.vehicleYear = `${order.vehicle.year}`; + } else { + payload.vehicleYear = ""; + } + + if (isDefined(order.vehicle.make)) { + payload.vehicleMake = order.vehicle.make; + } else { + payload.vehicleMake = ""; + } + + if (isDefined(order.vehicle.model)) { + payload.vehicleModel = order.vehicle.model; + } else { + payload.vehicleModel = ""; + } + + if (isDefined(order.vehicle.style)) { + payload.vehicleStyle = order.vehicle.style; + } else { + payload.vehicleStyle = ""; + } + + // Glass pieces + const glass = order.damage.glassToReplace ?? []; + if (glass.length === 0) { + payload.glassToReplace = ""; + } else { + const glassNames = glass.map((g) => `${g.glassLocation}/${g.glassName}`); + const glassString = glassNames.reduce((prev, next) => `${prev},${next}`); + + payload.glassToReplace = glassString; + } + + //EON + if (order.eon) { + payload.eon = order.eon; + } else { + payload.eon = ""; + } + + // Work Order Id + if (order.workOrderId) { + const parsedId = parseInt(order.workOrderId); + if (!isNaN(parsedId)) { + payload.workOrderId = parsedId; + } else { + payload.workOrderId = ""; + } + } else { + payload.workOrderId = ""; + } + + // Provider Ctu + if (isDefined(order.serviceLocation.zipCodeCtu)) { + payload.providerCtu = order.serviceLocation.zipCodeCtu; + } else { + payload.providerCtu = ""; + } + + // Work Order Number + if (order.workOrderNumber) { + payload.orderNumber = order.workOrderNumber; + } else { + payload.orderNumber = ""; + } + + // Unverified (no price or deductible displayed) + if (!store.isVerified) { + payload.priceSubTotal = ""; + payload.priceTotal = ""; + } + // Deductible case (no price is displayed, only deductible) + else if ( + store.isVerified && + (typeof store.currentDeductible === 'number' && store.currentDeductible >= 0) && + !store.isITAC && + !store.isNoComp + ) { + payload.priceSubTotal = ""; + payload.priceTotal = ""; + // ITAC or NoComp (where cash price is shown) + } else if (store.isVerified && (store.isITAC || store.isNoComp)) { + const subtotal = getSubtotal(order).toFixed(2); + payload.priceSubTotal = parseFloat(subtotal); + const total = getCartTotal(order).toFixed(2); + payload.priceTotal = parseFloat(total); + } else { + payload.priceSubTotal = ""; + payload.priceTotal = ""; + } + + // Cash Quote or Cash Price Sub Total + payload.cashPriceSubTotal = getSubtotal(order).toString(); + + // Recalibration + payload.isRecalibrationOnOrder = isRecalOrder(order.lineItems); + + // Appointment Type + if (isDefined(order.serviceLocation.appointmentType)) { + payload.appointmentType = order.serviceLocation.appointmentType; + } else { + payload.appointmentType = ""; + } + + payload.isInsuranceVerified = store.isVerified; + payload.insuranceCompanyName = store.issConfig.clientName ?? ""; + if (!store.isVerified) { + payload.isInsuranceItac = ""; + payload.isInsuranceNoComp = ""; + } else { + payload.isInsuranceItac = store.isITAC ?? ""; + payload.isInsuranceNoComp = store.isNoComp ?? ""; + } + if ( + store.isITAC || + store.isNoComp || + !store.isVerified + ) { + payload.insuranceDeductible = ""; + } else { + payload.insuranceDeductible = store.currentDeductible ?? ""; + } + + pushToDataLayerIfDefined(payload); + }, pushExperimentsToDataLayer() { const { experiments } = useMainStore().applicationUser; @@ -345,7 +530,7 @@ export default { sessionData.coverageSubStatus = coverageType.mapToApi(order?.insuranceCoverage.coverageType); sessionData.isNoComp = store.isNoComp; - sessionData.isItac = store.isITAC; + sessionData.isItac = store.isITAC; sessionData.subTotalPrice = getSubtotal(order).toString(); sessionData.totalPrice = getCartTotal(order).toString(); sessionData.cashPriceSubTotal = getSubtotal(order).toString(); diff --git a/src/router/index.js b/src/router/index.js index eeca615e..4fd5306e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -192,6 +192,9 @@ router.afterEach(async (to, from) => { // Push values to GA analyticsMixin.methods.pushValueToGA(); + + // Push current order status to Data Layer + analyticsMixin.methods.pushOrderToDataLayer(); } }); diff --git a/src/store/index.js b/src/store/index.js index db5894c6..680561af 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -22,11 +22,12 @@ import { repairWaivedForSelectedVehicle } from '@/helpers/policy-vehicle-helper'; import { buildURLSearchParams, getPartNumbersListForQueryString } from '@/helpers/querystring-helper'; -import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal } from '@/helpers/recal-helper'; +import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal, getHasRecalibrationPart } from '@/helpers/recal-helper'; import { getDateForSavedSessionTimeout } from '@/helpers/session-helper'; import { isMobileDevice } from '@/helpers/useragent-helper'; import issPageValues from '@/router/router-constants/issPage-values'; import CoverageStatuses from '@/constants/coverage-statuses'; +import { getNonFalseValuesOfPropertyInArrayOfObjects, sortArrayOfObjectsByPropertyValue } from '@/helpers/object-helper'; const storeId = 'main'; @@ -276,7 +277,7 @@ export const useMainStore = defineStore({ state: () => state, getters: { billToAccountNumber: (storeState) => storeState.issConfig.billToAccountNumber, - hasRecalibrationPart: (storeState) => getHasRecalibrationPart(storeState), + hasRecalibrationPart: (storeState) => getHasRecalibrationPartOnOrder(storeState), vehicle: (storeState) => storeState.order.vehicle, damage: (storeState) => storeState.order.damage, lineItems: (state) => state.order.lineItems, @@ -1348,45 +1349,6 @@ export const useMainStore = defineStore({ }); }, getServiceabilityDetails(serviceZipCode) { - console.log('BEEP BOOP'); - console.log('--ENVIRONMENT DETECTOR ROBOT ENGAGED--'); - const currentEnvironment = applicationConfig.CURRENT_ENVIRONMENT; - console.log('I HAVE DETERMINED THAT THE CURRENT ENVIRONMENT IS:', currentEnvironment); - console.log('---'); - console.log('---'); - if (currentEnvironment === 'Localhost' || currentEnvironment === 'Dev') { - console.log('NEW SERVICEABILITY DETAILS METHOD ACTIVATED'); - return this.getServiceabilityDetailsNewMethod(serviceZipCode); - } else { - console.log('OLD SERVICEABILITY DETAILS METHOD ACTIVATED'); - return this.getServiceabilityDetailsOldMethod(serviceZipCode); - } - }, - getServiceabilityDetailsOldMethod(serviceZipCode) { - const { vehicle, damage, parentAccountNumber, referralSequenceNumber, lineItems } = this.order; - const { carId } = vehicle; - const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace); - const lineItemParts = [...(lineItems.glassParts || []), ...(lineItems.supportingItems || [])].map((part) => ({ - partNumber: part.partNumber, - recalibrationType: part.recalibrationType - })); - - const params = buildURLSearchParams({ - applicationName: applicationConfig.APPLICATION_NAME, - parentAccountNumber, - referralSequenceNumber, - zip: serviceZipCode, - carId, - glassPieces: glassArray, - lineItems: lineItemParts - }); - - return globalMethods.callHttpClient({ - method: endpoints.GetServiceabilityDetails.method, - endpoint: `${endpoints.GetServiceabilityDetails.url}?${params.toString()}` - }); - }, - getServiceabilityDetailsNewMethod(serviceZipCode) { const { damage, lineItems, parentAccountNumber, vehicle } = this.order; const { carId } = vehicle; const flattenedGlassParts = getLineItemsFlattened(lineItems.glassParts); @@ -3179,39 +3141,8 @@ export const useMainStore = defineStore({ // Private Functions -function getHasRecalibrationPart(state) { - const hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, 'requiresRecalibration')?.length > 0; - const hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, 'recalibrationType')?.length > 0; - - if (hasRequiresRecalibration) { - if (hasRecalibrationType) { - // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType' - return ( - getNonFalseValuesOfPropertyInArrayOfObjects( - state.order.lineItems.glassParts, - 'recalibrationType' - )[0].toLowerCase() !== 'unknown' - ); - } - // Has 'requiresRecalibration' but no 'recalibrationType' at all - return true; - } - // Does not have 'requiresRecalibration' - return false; -} - -function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) { - return (array ?? []).map((x) => x[propertyName]).filter((x) => x); -} - -function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) { - if (!arrayOfObjects) return null; - - return arrayOfObjects.sort((a, b) => { - if (a[propertyName] < b[propertyName]) return -1; - if (a[propertyName] > b[propertyName]) return 1; - return 0; - }); +function getHasRecalibrationPartOnOrder(state) { + return getHasRecalibrationPart(state.order.lineItems); } function convertGlassPieceNamingForApi(glassArray) {