diff --git a/playwright-tests/framework/TestPages.ts b/playwright-tests/framework/TestPages.ts index bc98e44b5..39c2832f9 100644 --- a/playwright-tests/framework/TestPages.ts +++ b/playwright-tests/framework/TestPages.ts @@ -33,8 +33,12 @@ import { EndorsementsPage } from "../pages/EndorsementsPage" import { PolicyDriverPage } from "../pages/PolicyDriverPage" import { ServiceZipPage } from "../pages/ServiceZipPage" import { MobileDetailsPage } from "pages/MobileDetailsPage"; +import { BailoutPage } from '../pages/BailoutPage'; +import { BailoutSuccessPage } from '../pages/BailoutSuccessPage'; export interface ITestPages { + bailoutPage: BailoutPage, + bailoutSuccessPage: BailoutSuccessPage, capabilityQuestionsPage: CapabilityQuestionsPage, ccPolicyInfoPage: CCPolicyInfoPage, contactDetailsPage: ContactDetailsPage, @@ -72,6 +76,8 @@ export interface ITestPages { export const createTestPages: TestPagesFactory = (page: Page) => { const pages: ITestPages = { + bailoutPage: new BailoutPage(page), + bailoutSuccessPage: new BailoutSuccessPage(page), capabilityQuestionsPage: new CapabilityQuestionsPage(page), ccPolicyInfoPage: new CCPolicyInfoPage(page), contactDetailsPage: new ContactDetailsPage(page), diff --git a/playwright-tests/pages/BailoutPage.ts b/playwright-tests/pages/BailoutPage.ts new file mode 100644 index 000000000..5d8c54428 --- /dev/null +++ b/playwright-tests/pages/BailoutPage.ts @@ -0,0 +1,51 @@ +import { type Locator, type Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { step } from 'framework/localTypes/Step'; +import { ICustomerDetails } from 'safelite-playwright-core'; +import { ITestData } from 'framework/TestData'; + +export class BailoutPage extends BasePage { + readonly page: Page; + readonly firstNameTextBox: Locator; + readonly lastNameTextBox: Locator; + readonly emailAddressTextBox: Locator; + readonly phoneNumberTextBox: Locator; + readonly optInToSMSBox: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + + this.firstNameTextBox = this.page.getByRole('textbox', { name: 'First name' }); + this.lastNameTextBox = this.page.getByRole('textbox', { name: 'Last name' }); + this.emailAddressTextBox = this.page.getByRole('textbox', { name: 'Email address' }); + this.phoneNumberTextBox = this.page.getByRole('textbox', { name: 'Phone number' }); + this.optInToSMSBox = this.page.getByRole('checkbox', { name: 'Opt in to SMS' }); + } + + async fillOutBailoutForm(customerDetails: ICustomerDetails) { + await this.firstNameTextBox.fill(customerDetails!.firstName!); + await this.lastNameTextBox.fill(customerDetails!.lastName!); + await this.emailAddressTextBox.fill(customerDetails!.email!); + await this.phoneNumberTextBox.fill(customerDetails!.phoneNumber!); + } + + async checkOptInToSMSBox() { + await this.optInToSMSBox.check(); + } + + @step("BailoutPage >> Fill out bailout form: ") + async handleBailoutPage(testData: Partial) { + + const { customerDetails } = testData; + + await expect(this.page).toHaveURL(/bailout/); + await this.fillOutBailoutForm(customerDetails!); + + if (testData.isOptedInForTextMessages) { + await this.checkOptInToSMSBox(); + } + + await this.nextPage(); + } +} diff --git a/playwright-tests/pages/BailoutSuccessPage.ts b/playwright-tests/pages/BailoutSuccessPage.ts new file mode 100644 index 000000000..603172bd3 --- /dev/null +++ b/playwright-tests/pages/BailoutSuccessPage.ts @@ -0,0 +1,35 @@ +import { type Locator, type Page, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { TestSuccessAlert } from 'safelite-playwright-core'; +import { step } from 'framework/localTypes/Step'; + +export class BailoutSuccessPage extends BasePage { + readonly thankYouHeading: Locator; + readonly bodyText: Locator; + readonly returnToHomepageButton: Locator; + + constructor(page: Page) { + super(page); + this.thankYouHeading = page.getByText("Thanks! We'll get back to you soon"); + this.bodyText = page.getByText(/We've got it from here!/); + this.returnToHomepageButton = page.locator('#btn-vehicle-not-listed'); + } + + async validateBailoutSuccessPage() { + await expect(this.page).toHaveURL(/bailout-success/); + await expect(this.thankYouHeading).toBeVisible(); + await expect(this.bodyText).toBeVisible(); + await expect(this.bodyText).toHaveText( + /We've got it from here! One of our experts will be in touch to schedule your appointment\. If you have any questions, please contact 1-888-238-4527/ + ); + await expect(this.returnToHomepageButton).toBeVisible(); + } + + @step("BailoutSuccessPage >> Validate bailout success page and return to vehicle page") + async handleBailoutSuccessPage() { + await this.validateBailoutSuccessPage(); + await this.returnToHomepageButton.click(); + await expect(this.page).toHaveURL(/vehicle/); + throw new TestSuccessAlert('Parts not found bailout validated successfully.'); + } +} diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 088049693..8d863e94f 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -261,6 +261,12 @@ export class SchedulePage extends BasePage { await this.waitForPageOrComponentload(); await this.validateProgressBar(ProgressBarPercentages.SchedulePage); + //Looks like an issue with the insurance flow - remove comments once fixed + /*if (testData.isHeavyTruck) { + const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + expect(vuexState.order.lineItems.supportingItems.find((item: any) => item && item.partNumber == "labor2")).toBeTruthy(); + }*/ + if (handleMobileFirstModal) { return await this.handleMobileFirstPopUp(testData); } diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 79cfc89f8..fbeccd97d 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -40,7 +40,8 @@ import cashReplaceMultiGlassMobileTests from "./CashReplaceMultiGlassMobile"; import { getTestObject, TestCase, prepareTest, RuleEngine, TestInfo } from 'framework/Typedefs'; import { createTestPages } from "framework/TestPages"; import cashReplaceSwitchToInsuranceProgressiveNoCompTests from "./CashReplaceSwitchToInsuranceProgressiveNoComp"; -import insuranceBigTruckVerifiedTests from "./InsuranceBigTruckVerified"; +import insuranceUSAABigTruckVerifiedTests from "./InsuranceUSAABigTruckVerified"; +import partsNotFoundBailoutTests from "./PartsNotFoundBailout"; import insuranceUnverifiedTests from "./InsuranceUnverified"; import CashReplaceSplitWindshieldTests from "./CashReplaceSplitWindshield"; import insuranceMeemicNearSchoolVerifiedTests from "./InsuranceMeemicNearSchoolVerified"; @@ -95,13 +96,13 @@ const allStandardTests = [ // { name: "InsuranceUSAAMsr", tests: insuranceUSAAMsrTests }, { name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests }, { name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests }, - // {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests}, + {name: "InsuranceUSAABigTruckVerified", tests: insuranceUSAABigTruckVerifiedTests}, { name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests }, { name: "InsuranceUnverified", tests: insuranceUnverifiedTests }, // {name: "InsuranceGeico", tests: insuranceGeicoTests}, // {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests} { name: "InsuranceMeemicNearSchoolVerified", tests: insuranceMeemicNearSchoolVerifiedTests }, - + // { name: "PartsNotFoundBailout", tests: partsNotFoundBailoutTests }, // code is not available in QA ]; // Alert validation scenarios @@ -275,6 +276,14 @@ async function runWorkflow(page: Page, testCase: TestCase) { await serviceZipPage.handleServiceZipPage(testCase.testData); } + // Handle bailout page if parts bailout scenario + if (testCase.testData.bailoutFlags?.isVehicleLookupBailout) { + let bailoutPage = testCase.pages.bailoutPage; + await bailoutPage.handleBailoutPage(testCase.testData); + let bailoutSuccessPage = testCase.pages.bailoutSuccessPage; + await bailoutSuccessPage.handleBailoutSuccessPage(); + } + // Handle part questions if applicable if (partQuestions && partQuestions.length > 0) { let partQuestionsPage = testCase.pages.partQuestionsPage; diff --git a/playwright-tests/tests/InsuranceBigTruckVerified.ts b/playwright-tests/tests/InsuranceUSAABigTruckVerified.ts similarity index 81% rename from playwright-tests/tests/InsuranceBigTruckVerified.ts rename to playwright-tests/tests/InsuranceUSAABigTruckVerified.ts index 3e616230e..74566a535 100644 --- a/playwright-tests/tests/InsuranceBigTruckVerified.ts +++ b/playwright-tests/tests/InsuranceUSAABigTruckVerified.ts @@ -1,16 +1,16 @@ //Imports here import { ITestData, getDefaultExperimentsData } from 'framework/TestData' -import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core'; +import { ServiceLocation, DamageType, Flow} from 'safelite-playwright-core'; import { ITestCase } from '../framework/Typedefs' import { PaymentMethod } from "framework/localTypes/Enums"; import { VehicleLookupType } from 'safelite-playwright-core'; import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; // Set the seed based on test name for consistent but unique data -setFakerSeedFromTestName("InsuranceBigTruckVerified"); +setFakerSeedFromTestName("InsuranceUSAABigTruckVerified"); // Now get the test data with the seeded faker -const insuranceBigTruckVerifiedData: Partial = { +const insuranceUSAABigTruckVerifiedData: Partial = { ...getDefaultTestData(), // Get default data with current seed // Key feature: Insurance flow with GEICO @@ -19,6 +19,8 @@ const insuranceBigTruckVerifiedData: Partial = { // Insurance claim flags isDuplicateClaim: true, isPolicyFound: true, + flow: Flow.Managed, + isCanNotRecal: true, isUseVehicleOnPolicy: true, isHeavyTruck: true, @@ -62,7 +64,7 @@ const insuranceBigTruckVerifiedData: Partial = { // Override for in-shop appointment appointmentDetails: { serviceLocation: ServiceLocation.InShop, - shopAddress: '5719 Brandt Pike, Dayton, OH 45424', + shopAddress: '3455 Centerpoint Dr, Urbancrest, OH 43123', appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate }, @@ -75,13 +77,13 @@ const insuranceBigTruckVerifiedData: Partial = { } } -const insuranceBigTruckVerifiedTests: ITestCase[] = []; +const insuranceUSAABigTruckVerifiedTests: ITestCase[] = []; const tc = { - name: `InsuranceBigTruckVerified`, + name: `InsuranceUSAABigTruckVerified`, tags: ['@E2E','@InsuranceBigTruckVerified', '@test_report', '@Insurance'], - testData: insuranceBigTruckVerifiedData + testData: insuranceUSAABigTruckVerifiedData }; -insuranceBigTruckVerifiedTests.push(tc); +insuranceUSAABigTruckVerifiedTests.push(tc); -export default insuranceBigTruckVerifiedTests; \ No newline at end of file +export default insuranceUSAABigTruckVerifiedTests; \ No newline at end of file diff --git a/playwright-tests/tests/PartsNotFoundBailout.ts b/playwright-tests/tests/PartsNotFoundBailout.ts new file mode 100644 index 000000000..1ec10c36c --- /dev/null +++ b/playwright-tests/tests/PartsNotFoundBailout.ts @@ -0,0 +1,52 @@ +import { ITestData, getDefaultExperimentsData } from 'framework/TestData'; +import { VehicleDamage, VehicleLookupType } from 'safelite-playwright-core'; +import { ITestCase } from '../framework/Typedefs'; +import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core'; + +setFakerSeedFromTestName("CashDiamondReoBailout"); + +const partsNotFoundBailoutTestData: Partial = { + ...getDefaultTestData(), + + isHeavyTruck: true, + + customerDetails: { + ...getDefaultTestData().customerDetails!, + address: { + ...getDefaultTestData().customerDetails!.address, + postalCode: '43085' + } + }, + + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '1974', + make: 'Diamond Reo', + model: 'CF65', + style: 'cabover', + vehicleLookupType: VehicleLookupType.Zip, + }, + + vehicleDamage: [ + VehicleDamage.WindshieldCrack + ], + + bailoutFlags: { + isVehicleLookupBailout: true + }, + + experiments: { + ...getDefaultExperimentsData() + } +}; + +const partsNotFoundBailoutTests: ITestCase[] = []; + +const tc = { + name: `PartsNotFoundBailout`, + tags: ['@E2E', '@PartsNotFoundBailout', '@test_report', '@Bailout'], + testData: partsNotFoundBailoutTestData +}; +partsNotFoundBailoutTests.push(tc); + +export default partsNotFoundBailoutTests; diff --git a/src/constants/bailout-codes.js b/src/constants/bailout-codes.js new file mode 100644 index 000000000..e74791a5f --- /dev/null +++ b/src/constants/bailout-codes.js @@ -0,0 +1,5 @@ +const bailoutCodes = { + PART_NOT_FOUND: 18, +}; + +export { bailoutCodes }; diff --git a/src/constants/insurance.js b/src/constants/insurance.js index fe4d2b8f9..13a0fa932 100644 --- a/src/constants/insurance.js +++ b/src/constants/insurance.js @@ -79,6 +79,9 @@ export function coverageTypeEnum(strCoverageType) { } } +export const parentAccountNumbers = { + CONNECT: "560636", +}; export const NON_MANAGED_SHOW_CLAIM_NUMBER_PARENTS = [ { name: "KentuckyFarmBureau", value: "223499" }, ]; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index ad31474c3..a50ff4bb9 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -132,6 +132,8 @@ const storeActions = { UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey", CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry", + + SAVE_BAILOUT_CODE: "saveBailoutCode", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index e8bde83a5..d2fc2b0fc 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -115,6 +115,9 @@ const storeMutations = { UPDATE_EXPERIMENTS: "updateExperiments", UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry", + // BAILOUT MUTATIONS + UPDATE_BAILOUT_CODE: "updateBailoutCode", + // EXTERNAL_PARAMETER MUTATIONS UPDATE_IS_EXTERNAL_PARAMETER: "updateIsExternalParameter", UPDATE_EXTERNAL_PARAMETER_YEAR: "updateExternalParameterYear", diff --git a/src/fmg-components/funnel-footer/funnel-footer.vue b/src/fmg-components/funnel-footer/funnel-footer.vue index 5d8d0cd80..45cac1bbd 100644 --- a/src/fmg-components/funnel-footer/funnel-footer.vue +++ b/src/fmg-components/funnel-footer/funnel-footer.vue @@ -28,7 +28,7 @@ href="https://www.safelite.com/ccpa-privacy-policy" target="_blank" /> -

© 2025 Safelite Group

+

© {{ currentYear }} Safelite Group

@@ -40,6 +40,11 @@ export default { components: { textLink, }, + computed: { + currentYear() { + return new Date().getFullYear(); + }, + }, }; diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index a3dc363f5..613f239f2 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -106,6 +106,14 @@ export async function saveQuote({ pageNameToLog }) { return; } +export async function submitBailout({ pageNameToLog }) { + await saveSession({ + pageNameToLog: pageNameToLog, + shouldAwaitSaveSessionQueue: true, + submitAfterSave: false, + }); +} + // PRIVATE FUNCTIONS // /* diff --git a/src/layouts/bailout-success/bailout-success.spec.js b/src/layouts/bailout-success/bailout-success.spec.js new file mode 100644 index 000000000..5d5f73f14 --- /dev/null +++ b/src/layouts/bailout-success/bailout-success.spec.js @@ -0,0 +1,56 @@ +// Components +import bailoutSuccess from "@/layouts/bailout-success/bailout-success.vue"; + +// Supporting Files +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +// Mock fetchCmsContentForPage +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: jest.fn(), +})); + +describe("bailout-success.vue", () => { + test("renders funnelHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true); + }); + + test("renders funnelSubHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true); + }); + + test("renders Form component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true); + }); + + test("renders buttonMain component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "buttonMain" }).exists()).toBe(true); + }); +}); + +function setupMocks() { + const mountOptions = getMountOptions({}); + + //Mock props + const mockMixin = { + methods: { + getCmsContent: jest.fn(), + }, + }; + + mountOptions.mixins = [mockMixin]; + const wrapper = shallowMount(bailoutSuccess, mountOptions); + + wrapper.vm.setCmsContent = jest.fn(); + + return { wrapper }; +} diff --git a/src/layouts/bailout-success/bailout-success.vue b/src/layouts/bailout-success/bailout-success.vue new file mode 100644 index 000000000..5def1ae22 --- /dev/null +++ b/src/layouts/bailout-success/bailout-success.vue @@ -0,0 +1,98 @@ + + + diff --git a/src/layouts/bailout/bailout.spec.js b/src/layouts/bailout/bailout.spec.js index ebed2697a..f71899e49 100644 --- a/src/layouts/bailout/bailout.spec.js +++ b/src/layouts/bailout/bailout.spec.js @@ -16,9 +16,41 @@ jest.mock("@/helpers/cms-content-helper", () => ({ })); describe("bailout.vue", () => { + test("renders funnelHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelHeader" }).exists()).toBe(true); + }); + + test("renders funnelSubHeader component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "funnelSubHeader" }).exists()).toBe(true); + }); + + test("renders Form component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "Form" }).exists()).toBe(true); + }); + + test("renders navbar component", () => { + const { wrapper } = setupMocks(); + expect(wrapper.findComponent({ name: "navbar" }).exists()).toBe(true); + }); + + test("arePagePrerequisitesValid should be false ", async () => { + //Arrange + const { wrapper } = setupMocks(); + + //Act + let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + //Assert + expect(arePagePrerequisitesValid).toBe(false); + }); + test("arePagePrerequisitesValid should be true ", async () => { //Arrange const { wrapper } = setupMocks(); + wrapper.vm.getBailoutCodeFromStore = jest.fn().mockReturnValue("BAILOUT_CODE"); //Act let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); diff --git a/src/layouts/bailout/bailout.vue b/src/layouts/bailout/bailout.vue index a64f7a416..12cca1b92 100644 --- a/src/layouts/bailout/bailout.vue +++ b/src/layouts/bailout/bailout.vue @@ -1,21 +1,82 @@ diff --git a/src/layouts/coverage-statement/coverage-statement.spec.js b/src/layouts/coverage-statement/coverage-statement.spec.js index 2d4fd3f8d..a54591f34 100644 --- a/src/layouts/coverage-statement/coverage-statement.spec.js +++ b/src/layouts/coverage-statement/coverage-statement.spec.js @@ -62,6 +62,28 @@ function setupMocks() { navigateWithSaving: jest.fn(), navigateWithoutSaving: jest.fn(), }, + store: { + getters: { + order: { + policy: { + currentDeductible: 1000, + }, + lineItems: { + glassParts: [], + promos: [], + supportingItems: [], + vaps: [], + }, + }, + }, + }, + mixins: [ + { + methods: { + getTotalPriceOfAllLineItemsAndChildParts: jest.fn().mockReturnValue(250), + }, + }, + ], }); const wrapper = shallowMount(coverageStatement, mountOptions); diff --git a/src/layouts/coverage-statement/coverage-statement.vue b/src/layouts/coverage-statement/coverage-statement.vue index 4e4dd4cc0..81e2657bc 100644 --- a/src/layouts/coverage-statement/coverage-statement.vue +++ b/src/layouts/coverage-statement/coverage-statement.vue @@ -4,10 +4,56 @@
- - +
+ + +
+
+ + + + + +
+
+ +
+ + +
+
+
+ + + + + +
+ + + diff --git a/src/layouts/coverage-statement/customer-instructions/customer-instructions.vue b/src/layouts/coverage-statement/customer-instructions/customer-instructions.vue new file mode 100644 index 000000000..c584c0c19 --- /dev/null +++ b/src/layouts/coverage-statement/customer-instructions/customer-instructions.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/layouts/coverage-statement/price-display/price-display.vue b/src/layouts/coverage-statement/price-display/price-display.vue new file mode 100644 index 000000000..23d0ca3f2 --- /dev/null +++ b/src/layouts/coverage-statement/price-display/price-display.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/layouts/policy-vehicle/policy-vehicle.spec.js b/src/layouts/policy-vehicle/policy-vehicle.spec.js index 1fe1a5559..d11777667 100644 --- a/src/layouts/policy-vehicle/policy-vehicle.spec.js +++ b/src/layouts/policy-vehicle/policy-vehicle.spec.js @@ -50,11 +50,18 @@ function setupMocks() { FunnelHeaderWidget: { Text: "Header" }, FunnelSubHeaderWidget: { Text: "Subheader" }, FunnelFooterWidget: { Text: "Footer" }, + VehicleNotListedWidget: { Text: "Vehicle Not Listed" }, + CancelVerificationWidget: { Text: "Cancel Verification" }, + AmFamDisclaimerWidget: { Text: "Disclaimer" }, }; fetchCmsContentForPage.mockResolvedValue(mockCmsContent); settleAllPromises.mockResolvedValue({ cmsContent: mockCmsContent }); + const getCmsContentMock = jest.fn().mockImplementation((widgetName, cmsFieldName) => { + return mockCmsContent[widgetName][cmsFieldName]; + }); + const mountOptions = getMountOptions({ route: { name: "policy-vehicle", query: {}, params: {} }, router: { @@ -64,7 +71,13 @@ function setupMocks() { }, }); + mountOptions.global = mountOptions.global || {}; + mountOptions.global.mocks = { + ...(mountOptions.global.mocks || {}), + getCmsContent: getCmsContentMock, + }; + const wrapper = shallowMount(policyVehicle, mountOptions); - return { wrapper }; + return { wrapper, getCmsContentMock }; } diff --git a/src/layouts/policy-vehicle/policy-vehicle.vue b/src/layouts/policy-vehicle/policy-vehicle.vue index 954b04bc3..2b87e6692 100644 --- a/src/layouts/policy-vehicle/policy-vehicle.vue +++ b/src/layouts/policy-vehicle/policy-vehicle.vue @@ -4,16 +4,31 @@
- - + + + + + + + +
@@ -24,6 +39,11 @@ import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import navbar from "@/fmg-components/nav-bar/nav-bar"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; +import store from "@/store"; +import buttonMain from "@/ux-components/button-main/button-main"; +import textLink from "@/ux-components/text-link/text-link.vue"; +import { parentAccountNumbers } from "@/constants/insurance"; + import { Form } from "vee-validate"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; @@ -40,6 +60,8 @@ export default { funnelHeader, navbar, funnelSubHeader, + textLink, + buttonMain, }, // Ensure CMS content is fetched during route navigation without depending on `to`/`from` async beforeRouteEnter(to, from, next) { @@ -57,7 +79,34 @@ export default { }); }, + computed: { + vehicleNotListedQuestionText() { + return this.getCmsContent("VehicleNotListedWidget", "Text"); + }, + cancelVerificationCopy() { + return this.getCmsContent("CancelVerificationWidget", "Text"); + }, + amFamDisclaimerWidget() { + return this.getCmsContent("AmFamDisclaimerWidget", "Text"); + }, + showAmFamDisclaimer() { + return this.parentAccountNumberFromStore() === this.parentAccountNumbers.CONNECT; + }, + parentAccountNumbers() { + return parentAccountNumbers; + }, + }, + methods: { + parentAccountNumberFromStore() { + return store.getters.order?.payment?.parentAccountNumber; + }, + cancelVerificationAction() { + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_CANCEL_VERIFICATION, + this.pageName + ); + }, backButtonAction() { this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_BACK, @@ -76,3 +125,10 @@ export default { }, }; + + diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 6f2db110b..788b7c388 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1234,6 +1234,7 @@ export default { supportingItems[mobileFeeIndex].laborAmount = this.mobileFeePart.laborAmount; supportingItems[mobileFeeIndex].sellingPrice = this.mobileFeePart.sellingPrice; supportingItems[mobileFeeIndex].kitPrice = this.mobileFeePart.kitPrice; + supportingItems[mobileFeeIndex].isInsurable = this.mobileFeePart.isInsurable; } else { if (this.mobileFeePart !== null) supportingItems.push(this.mobileFeePart); } diff --git a/src/mixins/bailout-mixin.js b/src/mixins/bailout-mixin.js new file mode 100644 index 000000000..f8c724da2 --- /dev/null +++ b/src/mixins/bailout-mixin.js @@ -0,0 +1,13 @@ +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; + +export default { + methods: { + navigateToBailoutPage(vm, bailoutCode) { + const self = vm ?? this; + + self.dispatchStoreAction(self.storeActions.SAVE_BAILOUT_CODE, bailoutCode).then(() => { + self.$router.navigateWithoutSaving(navigationScenarios.BAILOUT, self.pageName); + }); + }, + }, +}; diff --git a/src/mixins/bailout-mixin.spec.js b/src/mixins/bailout-mixin.spec.js new file mode 100644 index 000000000..9571c6f26 --- /dev/null +++ b/src/mixins/bailout-mixin.spec.js @@ -0,0 +1,103 @@ +import bailoutMixin from "@/mixins/bailout-mixin"; +import { storeActions } from "@/constants/store-actions.js"; +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; +import { bailoutCodes } from "@/constants/bailout-codes.js"; + +describe("bailout-mixin.js", () => { + test("navigateToBailoutPage: dispatches SAVE_BAILOUT_CODE action with bailout code", async () => { + // Arrange + const mockVm = createMockVm(); + const bailoutCode = bailoutCodes.PART_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + bailoutCode + ); + }); + + test("navigateToBailoutPage: navigates to bailout page after saving bailout code", async () => { + // Arrange + const mockVm = createMockVm(); + const bailoutCode = bailoutCodes.PART_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith( + navigationScenarios.BAILOUT, + mockVm.pageName + ); + }); + + test("navigateToBailoutPage: uses current context (this) when vm is not provided", async () => { + // Arrange + const mockRouter = { + navigateWithoutSaving: jest.fn().mockResolvedValue(undefined), + }; + const mockThis = { + dispatchStoreAction: jest.fn().mockResolvedValue(undefined), + $router: mockRouter, + storeActions: storeActions, + pageName: "test-page", + }; + + const bailoutCode = bailoutCodes.PART_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage.call(mockThis, undefined, bailoutCode); + + // Assert + expect(mockThis.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + bailoutCode + ); + }); + + test("navigateToBailoutPage: passes correct bailout code to store", async () => { + // Arrange + const mockVm = createMockVm(); + const customBailoutCode = 999; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, customBailoutCode); + + // Assert + expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_BAILOUT_CODE, + customBailoutCode + ); + }); + + test("navigateToBailoutPage: calls navigateWithoutSaving with correct parameters", async () => { + // Arrange + const mockVm = createMockVm(); + const mockPageName = "vehicle-damage"; + mockVm.pageName = mockPageName; + const bailoutCode = bailoutCodes.PART_NOT_FOUND; + + // Act + await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode); + + // Assert + expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith( + navigationScenarios.BAILOUT, + mockPageName + ); + }); +}); + +function createMockVm() { + return { + dispatchStoreAction: jest.fn().mockResolvedValue(undefined), + $router: { + navigateWithoutSaving: jest.fn().mockResolvedValue(undefined), + }, + storeActions, + pageName: "test-page", + }; +} diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index fc195d910..c593d88b4 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -1,9 +1,11 @@ import { storeActions } from "@/constants/store-actions.js"; import store from "@/store"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; +import bailoutMixin from "@/mixins/bailout-mixin"; import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import { experimentSettings } from "@/constants/experiments"; import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js"; +import { bailoutCodes } from "@/constants/bailout-codes"; export default { computed: { @@ -34,6 +36,11 @@ export default { const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS, { pageNameToLog: pageName, }); + + if (result.PartNotFound) { + bailoutMixin.methods.navigateToBailoutPage(this, bailoutCodes.PART_NOT_FOUND); + } + const partsOrQuestions = result.data.partsOrQuestions; vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this); diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index fef66ddbb..8112cb98e 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -12,6 +12,12 @@ const navigationScenarios = { CLICKED_FORWARD: "CLICKED_FORWARD", CLICKED_FORWARD_WITH_CASH: "CLICKED_FORWARD_WITH_CASH", CLICKED_FORWARD_WITH_INSURANCE: "CLICKED_FORWARD_WITH_INSURANCE", + CLICKED_CANCEL_VERIFICATION: "CLICKED_CANCEL_VERIFICATION", + + // Bailout + BAILOUT: "BAILOUT", + BAILOUT_SUCCESS: "BAILOUT_SUCCESS", + CLICKED_BACK_TO_HOMEPAGE: "CLICKED_BACK_TO_HOMEPAGE", // TODO: use virtual page? // Vin selection diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index fc54025ef..479d0899d 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -171,6 +171,14 @@ export const routeData = { path: "/virtual/restart", virtual: true, }, + BAILOUT: { + name: "bailout", + path: "/bailout", + }, + BAILOUT_SUCCESS: { + name: "bailout-success", + path: "/bailout-success", + }, }; export const FUNNEL_START_PAGE = routeData.VEHICLE; diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 77876a096..fcd259790 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -1,5 +1,6 @@ import { routeData } from "@/router/constants/routes"; import { navigationScenarios } from "@/router/constants/navigation-scenarios"; +import { FUNNEL_START_PAGE } from "@/router/constants/routes"; import store from "@/store"; import { paymentMethods } from "@/constants/payment-method-constants"; @@ -88,6 +89,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_VIN_RETRY, destinationPageData: routeData.ESTIMATE, }, + { + scenario: navigationScenarios.BAILOUT, + destinationPageData: routeData.BAILOUT, + }, ], }, { @@ -128,6 +133,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_VIN_RETRY, destinationPageData: routeData.ESTIMATE, }, + { + scenario: navigationScenarios.BAILOUT, + destinationPageData: routeData.BAILOUT, + }, ], }, { @@ -274,6 +283,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, destinationPageData: routeData.QUOTE, }, + { + scenario: navigationScenarios.BAILOUT, + destinationPageData: routeData.BAILOUT, + }, ], }, { @@ -479,6 +492,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_FORWARD, destinationPageData: routeData.POLICY_DRIVER, }, + { + scenario: navigationScenarios.CLICKED_CANCEL_VERIFICATION, + destinationPageData: routeData.INSURANCE_COMPANY, + }, ], }, { @@ -802,6 +819,24 @@ const routingTable = function () { }, ], }, + { + pageName: routeData.BAILOUT.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.BAILOUT_SUCCESS, + }, + ], + }, + { + pageName: routeData.BAILOUT_SUCCESS.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK_TO_HOMEPAGE, + destinationPageData: FUNNEL_START_PAGE, + }, + ], + }, ]; }; diff --git a/src/router/methods/before-each.js b/src/router/methods/before-each.js index 237014ee6..568cf312c 100644 --- a/src/router/methods/before-each.js +++ b/src/router/methods/before-each.js @@ -76,14 +76,27 @@ export async function beforeEach(to, from) { } // Block navigation if an order has been submitted. - if (window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) !== null) { - const exceptionPages = [FUNNEL_START_PAGE.name, routeData.CONFIRMATION.name]; + const submittedState = window.sessionStorage.getItem( + sessionStorageKeyConstants.SUBMITTED_STATE + ); + if (submittedState !== null) { + const isBailout = getIsBailout(submittedState); + const exceptionPages = isBailout + ? [FUNNEL_START_PAGE.name, routeData.BAILOUT_SUCCESS.name] + : [FUNNEL_START_PAGE.name, routeData.CONFIRMATION.name]; if (!exceptionPages.some((name) => to.name === name) && !isVirtualRoute(to.name)) { - router.push({ - name: routeData.CONFIRMATION.name, - }); - return false; + if (isBailout) { + router.push({ + name: routeData.BAILOUT_SUCCESS.name, + }); + return false; + } else { + router.push({ + name: routeData.CONFIRMATION.name, + }); + return false; + } } } @@ -142,3 +155,8 @@ export async function beforeEach(to, from) { return; } } + +function getIsBailout(submittedState) { + const submittedStateObj = JSON.parse(submittedState); + return !!submittedStateObj?.applicationUser?.bailoutCode; +} diff --git a/src/router/methods/navigate.js b/src/router/methods/navigate.js index b80d31769..63e20d96c 100644 --- a/src/router/methods/navigate.js +++ b/src/router/methods/navigate.js @@ -1,7 +1,8 @@ -import { saveSession } from "@/helpers/heritage-integration/order-helper"; +import { saveSession, submitBailout } from "@/helpers/heritage-integration/order-helper"; import { buildManualUrl } from "@/router/methods/helpers/build-manual-url"; import { getDestination } from "@/router/methods/helpers/get-destination"; import { savePageData } from "@/router/methods/helpers/save-page-data"; +import { navigationScenarios } from "@/router/constants/navigation-scenarios"; import router from "@/router"; import store from "@/store"; @@ -39,7 +40,14 @@ async function navigate(scenario, currentPageName, withSaving = false, forceTopL store.getters?.applicationUser?.savedSessionId || store.getters?.order?.customer?.emailAddress ) { - await saveSession({ pageNameToLog: nextPage.name }); + if ( + scenario.toUpperCase() === navigationScenarios.CLICKED_FORWARD && + currentPageName.toUpperCase() === navigationScenarios.BAILOUT + ) { + await submitBailout({ pageNameToLog: nextPage.name }); + } else { + await saveSession({ pageNameToLog: nextPage.name }); + } } } @@ -70,7 +78,13 @@ export async function navigateWithSaving(scenario, currentPageName) { export async function navigateWithPageData(scenario, currentPageName, pageData = {}) { const nextPage = getDestination(currentPageName, scenario); - await savePageData(nextPage.name, pageData); + + if (pageData && pageData.bailoutCode) { + pageData.AppName = "FixMyGlass"; + await savePageData(currentPageName, pageData); + } else { + await savePageData(nextPage.name, pageData); + } return await navigate(scenario, currentPageName, true); } diff --git a/src/router/methods/routes.js b/src/router/methods/routes.js index 88cc17f94..34056ba76 100644 --- a/src/router/methods/routes.js +++ b/src/router/methods/routes.js @@ -51,6 +51,8 @@ export const routes = [ createRoute(routeData.RECALIBRATION_INFO), createRoute(routeData.COVERAGE_STATEMENT), createRoute(routeData.VERIFY_DETAILS), + createRoute(routeData.BAILOUT), + createRoute(routeData.BAILOUT_SUCCESS), // Virtual pages (resolve to a non-virtual page.) createVirtualRoute(routeData.LANDING, landingBeforeEnter), createVirtualRoute(routeData.HERITAGE, heritageBeforeEnter), diff --git a/src/store/index.js b/src/store/index.js index 9f06b76cc..fab5bc6ac 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -224,6 +224,7 @@ const getDefaultState = () => { affiliateCookies: [], loggingOption: false, hasAlreadyTriggeredError: false, + bailoutCode: null, }, idempotencyKeyFields: { referralCorrelationId: null, @@ -499,9 +500,11 @@ export const mutations = { state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate; }, updateServiceZip(state, serviceZipInfo) { - state.order.serviceLocation.state = serviceZipInfo.state; - state.order.serviceLocation.zipCode = serviceZipInfo.zipCode; - state.order.serviceLocation.zipCodeCtu = serviceZipInfo.zipCodeCtu; + state.order.serviceLocation.state = serviceZipInfo.state || serviceZipInfo.payload?.state; + state.order.serviceLocation.zipCode = + serviceZipInfo.zipCode || serviceZipInfo.payload?.zipCode; + state.order.serviceLocation.zipCodeCtu = + serviceZipInfo.zipCodeCtu || serviceZipInfo.payload?.zipCodeCtu; }, updateServiceLocation(state, serviceLocationInfo) { state.order.serviceLocation.address = serviceLocationInfo.address; @@ -995,6 +998,9 @@ export const mutations = { state.idempotencyKeyFields.totalInCents = totalInCents; state.idempotencyKeyFields.expiryTime = expiryTime; }, + updateBailoutCode(state, bailoutCode) { + state.applicationUser.bailoutCode = bailoutCode; + }, }; // Export Getters @@ -1948,21 +1954,38 @@ export const actions = { // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); - const response = await globalMethods.callHttpClient({ - method: endpoints.GetPartsOrQuestions.method, - endpoint: endpoints.GetPartsOrQuestions.url, - payload: { - carId: carId, - glassPieces: glassArrayForPayload, - zip: zipCode, - vin: vin, - serviceType: serviceType, - referralSeqNumber: referralSeqNumber, - parentAccountNumber: parentAccountNumber, - }, - logApiCall: true, - pageNameToLog: pageNameToLog, - }); + const response = await globalMethods + .callHttpClient({ + method: endpoints.GetPartsOrQuestions.method, + endpoint: endpoints.GetPartsOrQuestions.url, + payload: { + carId: carId, + glassPieces: glassArrayForPayload, + zip: zipCode, + vin: vin, + serviceType: serviceType, + referralSeqNumber: referralSeqNumber, + parentAccountNumber: parentAccountNumber, + }, + logApiCall: true, + pageNameToLog: pageNameToLog, + }) + .catch((error) => { + // if (error.status == 500) { + // return { PartNotFound: true }; + // } + }); + + // Triggers bailout + // if (response.PartNotFound) { + // return response; + // } + + // Check if we only have MISC parts to trigger bailout + // const miscPartsResponse = checkIfMiscParts(response.data.partsOrQuestions); + // if (miscPartsResponse.PartNotFound) { + // return miscPartsResponse; + // } // Flatten location and name properties response.data.partsOrQuestions = convertGlassPieceNamingFromApi( @@ -3868,6 +3891,10 @@ export const actions = { context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey); } }, + + saveBailoutCode(context, bailoutCode) { + context.commit(storeMutations.UPDATE_BAILOUT_CODE, bailoutCode); + }, }; export default createStore({ @@ -4409,3 +4436,17 @@ const timeSlotCallFlags = { shop: false, mobile: false, }; + +function checkIfMiscParts(partsOrQuestions) { + if (!partsOrQuestions || partsOrQuestions.length === 0) { + return { PartsNotFound: true }; + } else if ( + partsOrQuestions.length === 1 && + partsOrQuestions[0].parts && + partsOrQuestions[0].parts.length === 1 && + partsOrQuestions[0].parts[0].partNumber.startsWith("MISC") + ) { + return { PartsNotFound: true }; + } + return { PartsNotFound: false }; +}