From c6ec570f4c761584ece1408b6c167fadc5d2bb30 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Mon, 6 Oct 2025 10:50:44 -0400 Subject: [PATCH 001/233] CASH-1527 | Initial attempt at updating salesforce Apparently it does not work in localhost. --- .../salesforce-helper-dev.js | 69 +++++-------------- .../salesforce-webchat/salesforce-webchat.vue | 13 ++-- .../sierra-webchat/sierra-webchat.vue | 16 ++--- src/helpers/webchat-helper.js | 2 +- src/styles/ux-variables.scss | 4 +- 5 files changed, 35 insertions(+), 69 deletions(-) diff --git a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js index d11ccf0d4..f7f2b24fd 100644 --- a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js +++ b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js @@ -7,9 +7,9 @@ function setupCustomSettings( onWebchatCloseCallback ) { modifyEmbeddedSvcCallback(); - embedded_svc.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback); - embedded_svc.addEventHandler("afterMaximize", onWebchatOpenCallback); - embedded_svc.addEventHandler("afterDestroy", onWebchatCloseCallback); + embeddedservice_bootstrap.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback); + embeddedservice_bootstrap.addEventHandler("afterMaximize", onWebchatOpenCallback); + embeddedservice_bootstrap.addEventHandler("afterDestroy", onWebchatCloseCallback); } /* @@ -36,24 +36,6 @@ export function initializeSalesforceWebchatForDev( onWebchatCloseCallback ) { var initESW = function (gslbBaseURL) { - embedded_svc.settings.displayHelpButton = true; //Or false - embedded_svc.settings.language = ""; //For example, enter 'en' or 'en-US' - //embedded_svc.settings.defaultMinimizedText = '...'; //(Defaults to Chat with an Expert) - //embedded_svc.settings.disabledMinimizedText = '...'; //(Defaults to Agent Offline) - //embedded_svc.settings.loadingText = ''; //(Defaults to Loading) - //embedded_svc.settings.storageDomain = 'yourdomain.com'; //(Sets the domain for your deployment so that visitors can navigate subdomains during a chat session) - // Settings for Chat - //embedded_svc.settings.directToButtonRouting = function(prechatFormData) { - // Dynamically changes the button ID based on what the visitor enters in the pre-chat form. - // Returns a valid button ID. - //}; - //embedded_svc.settings.prepopulatedPrechatFields = {}; //Sets the auto-population of pre-chat form fields - //embedded_svc.settings.fallbackRouting = []; //An array of button IDs, user IDs, or userId_buttonId - //embedded_svc.settings.offlineSupportMinimizedText = '...'; //(Defaults to Contact Us) - - embedded_svc.settings.enabledFeatures = ["LiveAgent"]; - embedded_svc.settings.entryFeature = "LiveAgent"; - setupCustomSettings( modifyEmbeddedSvcCallback, finishedLoadingCallback, @@ -61,36 +43,19 @@ export function initializeSalesforceWebchatForDev( onWebchatCloseCallback ); - embedded_svc.init( - "https://safelite2--dev.sandbox.my.salesforce.com", - "https://safelite2--dev.sandbox.my.salesforce-sites.com/chat", - gslbBaseURL, - "00D6s0000008r6A", - "Sales_Central_Snap_in", - { - baseLiveAgentContentURL: - "https://c.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/content", - deploymentId: "572Wr0000017xKX", - buttonId: "573Wr000000CpgH", - baseLiveAgentURL: "https://d.la12s-core1.sfdc-8tgtt5.salesforceliveagent.com/chat", - eswLiveAgentDevName: - "EmbeddedServiceLiveAgent_Parent04IWr000003yKq9MAE_1961681659a", - isOfflineSupportEnabled: false, - } - ); - }; + try { + embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US' - if (!window.embedded_svc) { - var s = document.createElement("script"); - s.setAttribute( - "src", - "https://safelite2--dev.sandbox.my.salesforce.com/embeddedservice/5.0/esw.min.js" - ); - s.onload = function () { - initESW(null); - }; - document.body.appendChild(s); - } else { - initESW("https://service.force.com"); - } + embeddedservice_bootstrap.init( + "00D6s0000008r6A", + "Sales_Central_TFN", + "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477", + { + scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com", + } + ); + } catch (err) { + console.error("Error loading Embedded Messaging: ", err); + } + }; } diff --git a/src/digital-components/salesforce-webchat/salesforce-webchat.vue b/src/digital-components/salesforce-webchat/salesforce-webchat.vue index 19ce71235..93e966f3e 100644 --- a/src/digital-components/salesforce-webchat/salesforce-webchat.vue +++ b/src/digital-components/salesforce-webchat/salesforce-webchat.vue @@ -33,7 +33,8 @@ export default { mounted() { // Load in script from salesforce CDN const script = document.createElement("script"); - script.src = "https://service.force.com/embeddedservice/5.0/esw.min.js"; + script.src = + "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477/assets/js/bootstrap.min.js"; script.onload = this.initializeSalesforceWebchat; document.body.appendChild(script); @@ -92,11 +93,11 @@ export default { } }, modifyEmbeddedSvcCallback() { - window.embedded_svc.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png"); - window.embedded_svc.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png"); - window.embedded_svc.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png"); - window.embedded_svc.settings.displayHelpButton = false; - window.embedded_svc.addEventHandler("onChatEstablished", () => { + window.embeddedservice_bootstrap.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png"); + window.embeddedservice_bootstrap.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png"); + window.embeddedservice_bootstrap.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png"); + window.embeddedservice_bootstrap.settings.displayHelpButton = false; + window.embeddedservice_bootstrap.addEventHandler("onChatEstablished", () => { this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true); }); }, diff --git a/src/digital-components/sierra-webchat/sierra-webchat.vue b/src/digital-components/sierra-webchat/sierra-webchat.vue index cc88b5433..59c833dbf 100644 --- a/src/digital-components/sierra-webchat/sierra-webchat.vue +++ b/src/digital-components/sierra-webchat/sierra-webchat.vue @@ -139,8 +139,8 @@ export default { // Pass data to Salesforce chat if available const transfer = event.detail; if ( - window.embedded_svc && - typeof window.embedded_svc.bootstrapEmbeddedService === "function" + window.embeddedservice_bootstrap && + typeof window.embeddedservice_bootstrap.bootstrapEmbeddedService === "function" ) { if ( transfer && @@ -149,13 +149,13 @@ export default { transfer.data.last_name && transfer.data.email ) { - window.embedded_svc.settings.prepopulatedPrechatFields = { + window.embeddedservice_bootstrap.settings.prepopulatedPrechatFields = { FirstName: transfer.data.first_name, LastName: transfer.data.last_name, Email: transfer.data.email, Subject: transfer.data.chat_summary, }; - window.embedded_svc.settings.extraPrechatFormDetails = [ + window.embeddedservice_bootstrap.settings.extraPrechatFormDetails = [ { label: "Scarlett AI Summary", value: transfer.data.chat_summary, @@ -164,14 +164,14 @@ export default { }, ]; if ( - window.embedded_svc.liveAgentAPI && - typeof window.embedded_svc.liveAgentAPI.startChat === "function" + window.embeddedservice_bootstrap.liveAgentAPI && + typeof window.embeddedservice_bootstrap.liveAgentAPI.startChat === "function" ) { - window.embedded_svc.liveAgentAPI.startChat(); + window.embeddedservice_bootstrap.liveAgentAPI.startChat(); return; } } - window.embedded_svc.bootstrapEmbeddedService(); + window.embeddedservice_bootstrap.bootstrapEmbeddedService(); } }; window.addEventListener("sierra-chat-transfer", this._handleSierraTransfer); diff --git a/src/helpers/webchat-helper.js b/src/helpers/webchat-helper.js index 917d6de0f..90fc4a29a 100644 --- a/src/helpers/webchat-helper.js +++ b/src/helpers/webchat-helper.js @@ -11,7 +11,7 @@ export const webchatHelper = () => { if (webchatGlobalNonpersistedState.shouldLaunchSierra) { window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat } else { - window.embedded_svc.bootstrapEmbeddedService(); // launches the salesForce prechat form + window.embeddedservice_bootstrap.bootstrapEmbeddedService(); // launches the salesForce prechat form } } diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index c0a69a112..bec02081f 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -123,8 +123,8 @@ $body-color: $gray-600; //Fonts $font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif; -$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; +$font-family-monospace: + UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; // stylelint-enable value-keyword-case $font-family-base: $font-family-sans-serif; $font-family-code: $font-family-monospace; From 36100083fe86b8d382cdd20a0613dddaa43436a6 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Mon, 6 Oct 2025 10:54:22 -0400 Subject: [PATCH 002/233] Add logging, remove wrapper function --- .../salesforce-helper-dev.js | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js index f7f2b24fd..2e56634cc 100644 --- a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js +++ b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js @@ -35,27 +35,27 @@ export function initializeSalesforceWebchatForDev( onWebchatOpenCallback, onWebchatCloseCallback ) { - var initESW = function (gslbBaseURL) { - setupCustomSettings( - modifyEmbeddedSvcCallback, - finishedLoadingCallback, - onWebchatOpenCallback, - onWebchatCloseCallback + setupCustomSettings( + modifyEmbeddedSvcCallback, + finishedLoadingCallback, + onWebchatOpenCallback, + onWebchatCloseCallback + ); + + try { + console.log("initializing embeddedservice_bootstrap"); + embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US' + + embeddedservice_bootstrap.init( + "00D6s0000008r6A", + "Sales_Central_TFN", + "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477", + { + scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com", + } ); + } catch (err) { + console.error("Error loading Embedded Messaging: ", err); + } - try { - embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US' - - embeddedservice_bootstrap.init( - "00D6s0000008r6A", - "Sales_Central_TFN", - "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477", - { - scrt2URL: "https://safelite2--dev.sandbox.my.salesforce-scrt.com", - } - ); - } catch (err) { - console.error("Error loading Embedded Messaging: ", err); - } - }; } From b6f497c13f908f54671613b6ccd9d83b5e551915 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Wed, 8 Oct 2025 09:46:08 -0400 Subject: [PATCH 003/233] Update listeners --- .../salesforce-webchat/salesforce-helper-dev.js | 7 +++---- .../salesforce-webchat/salesforce-webchat.vue | 2 +- src/digital-components/sierra-webchat/sierra-webchat.vue | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js index 2e56634cc..a171fb974 100644 --- a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js +++ b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js @@ -7,9 +7,9 @@ function setupCustomSettings( onWebchatCloseCallback ) { modifyEmbeddedSvcCallback(); - embeddedservice_bootstrap.addEventHandler("onSettingsCallCompleted", finishedLoadingCallback); - embeddedservice_bootstrap.addEventHandler("afterMaximize", onWebchatOpenCallback); - embeddedservice_bootstrap.addEventHandler("afterDestroy", onWebchatCloseCallback); + window.addEventListener("onEmbeddedMessagingReady", finishedLoadingCallback); + window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback); + window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback); } /* @@ -57,5 +57,4 @@ export function initializeSalesforceWebchatForDev( } catch (err) { console.error("Error loading Embedded Messaging: ", err); } - } diff --git a/src/digital-components/salesforce-webchat/salesforce-webchat.vue b/src/digital-components/salesforce-webchat/salesforce-webchat.vue index 93e966f3e..31719024e 100644 --- a/src/digital-components/salesforce-webchat/salesforce-webchat.vue +++ b/src/digital-components/salesforce-webchat/salesforce-webchat.vue @@ -97,7 +97,7 @@ export default { window.embeddedservice_bootstrap.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png"); window.embeddedservice_bootstrap.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png"); window.embeddedservice_bootstrap.settings.displayHelpButton = false; - window.embeddedservice_bootstrap.addEventHandler("onChatEstablished", () => { + window.addEventListener("onEmbeddedMessagingConversationStarted", () => { this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true); }); }, diff --git a/src/digital-components/sierra-webchat/sierra-webchat.vue b/src/digital-components/sierra-webchat/sierra-webchat.vue index 59c833dbf..6248ffa2d 100644 --- a/src/digital-components/sierra-webchat/sierra-webchat.vue +++ b/src/digital-components/sierra-webchat/sierra-webchat.vue @@ -165,7 +165,8 @@ export default { ]; if ( window.embeddedservice_bootstrap.liveAgentAPI && - typeof window.embeddedservice_bootstrap.liveAgentAPI.startChat === "function" + typeof window.embeddedservice_bootstrap.liveAgentAPI.startChat === + "function" ) { window.embeddedservice_bootstrap.liveAgentAPI.startChat(); return; From a6506db3187c0b86dd2898806f5072d2b547e3f6 Mon Sep 17 00:00:00 2001 From: JennyNou <167806377+JennyNou@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:20:44 -0500 Subject: [PATCH 004/233] Add test to cover OEM scenario --- playwright-tests/framework/TestPages.ts | 3 + .../pages/OrderConfirmationPage.ts | 2 +- playwright-tests/pages/PaymentMethodPage.ts | 17 ++++ .../pages/ProblemGlassQuestionsPage.ts | 43 +++++++++ playwright-tests/tests/0000__M.test.ts | 13 ++- .../tests/InsuranceOEMAllState.ts | 88 +++++++++++++++++++ 6 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 playwright-tests/pages/ProblemGlassQuestionsPage.ts create mode 100644 playwright-tests/tests/InsuranceOEMAllState.ts diff --git a/playwright-tests/framework/TestPages.ts b/playwright-tests/framework/TestPages.ts index e873a1c7b..b0baf2db1 100644 --- a/playwright-tests/framework/TestPages.ts +++ b/playwright-tests/framework/TestPages.ts @@ -24,6 +24,7 @@ import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage" import { DuplicateCheckPage } from "../pages/DuplicateCheckPage" import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage" import { PolicyInfoSubmittedPage } from "../pages/PolicyInfoSubmittedPage" +import { ProblemGlassQuestionsPage } from "../pages/ProblemGlassQuestionsPage" import RecalibrationInfoPage from "../pages/RecalibrationInfoPage" import { CoverageStatementPage } from "../pages/CoverageStatementPage" import { VerifyDetailsPage } from "../pages/VerifyDetailsPage" @@ -48,6 +49,7 @@ export interface ITestPages { paymentMethodPage: PaymentMethodPage, policyInfoSubmittedPage: PolicyInfoSubmittedPage, policyVehiclesPage: PolicyVehiclesPage, + problemGlassQuestionsPage: ProblemGlassQuestionsPage, recalibrationInfoPage: RecalibrationInfoPage, schedulePage: SchedulePage, mobileDetailsPage: MobileDetailsPage, @@ -83,6 +85,7 @@ export const createTestPages: TestPagesFactory = (page: Page) => { paymentMethodPage: new PaymentMethodPage(page), policyInfoSubmittedPage: new PolicyInfoSubmittedPage(page), policyVehiclesPage: new PolicyVehiclesPage(page), + problemGlassQuestionsPage: new ProblemGlassQuestionsPage(page), recalibrationInfoPage: new RecalibrationInfoPage(page), schedulePage: new SchedulePage(page), mobileDetailsPage: new MobileDetailsPage(page), diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts index 02951bdd2..6bf10926c 100644 --- a/playwright-tests/pages/OrderConfirmationPage.ts +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -47,7 +47,7 @@ export class OrderConfirmationPage extends BasePage { async validateOrderConfirmationPage(testData: Partial) { // Destructure data we use const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow, - isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified } = testData; + isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified, isOEM } = testData; await this.serviceText.waitFor({ state: "visible" }); Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase())); diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 4ea1a8b82..b4b00a151 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -496,6 +496,22 @@ export class PaymentMethodPage extends BasePage { let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); return updatedAppointmentDate; } + + async validateOEMPart( isOEM: boolean): Promise { + if (isOEM == true) { + // Get Vuex state from localStorage + const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + + // Validate the presence of an OEM part + if (vuexState.order?.lineItems?.glassParts?.length > 0) { + const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber; + + await expect(firstGlassPartNumber.includes("OEM")).toBe(true); + } else { + throw new Error("No glass parts found in the order"); + } + } + } @step("PaymentMethodPage >> Select Payment Method: ") async handlePaymentMethodPage(testData: Partial) { @@ -504,6 +520,7 @@ export class PaymentMethodPage extends BasePage { await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage); await this.validatePaymentDetailsPage(testData); await this.ValidateAfterPayBreakOutSection(); + await this.validateOEMPart(testData.isOEM!); // Verify VAPS wipers on backend for standard and premium packages if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) { diff --git a/playwright-tests/pages/ProblemGlassQuestionsPage.ts b/playwright-tests/pages/ProblemGlassQuestionsPage.ts new file mode 100644 index 000000000..08124eef0 --- /dev/null +++ b/playwright-tests/pages/ProblemGlassQuestionsPage.ts @@ -0,0 +1,43 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { IClaimDetails, ICustomerDetails } from 'safelite-playwright-core'; +import { InsuranceBasePage } from './InsuranceBasePage'; +import { ITestData } from 'framework/TestData'; +import { step } from 'framework/localTypes/Step'; + +export class ProblemGlassQuestionsPage extends InsuranceBasePage { + readonly provideVinManuallyButton: Locator; + readonly provideLicensePlateButton: Locator; + readonly provideHomeAddressButton: Locator; + readonly continueOnVinPopUpButton: Locator; + readonly dontShareVinButton: Locator; + + readonly yestoGlassQuestionButton: Locator; + readonly notoGlassQuestionButton: Locator; + readonly continueButton: Locator; + + + constructor(page: Page) { + super(page); + + this.provideVinManuallyButton = page.locator('label[for="VINManually"]'); + this.provideLicensePlateButton = page.locator('label[for="licensePlate"]'); + this.provideHomeAddressButton = page.locator('label[for="HomeAddress"]'); + this.continueOnVinPopUpButton = page.locator('button:has-text("Continue")'); + this.dontShareVinButton = page.getByRole('button', { name: "I would rather not share my VIN" }); + + this.yestoGlassQuestionButton = page.locator('label:has-text("Yes")'); + this.notoGlassQuestionButton = page.locator('label:has-text("No")'); + this.continueButton = page.locator('button[type="submit"].btn-success.nav-continue'); + + } + + + @step("ProblemGlassQuestionsPage >> Click continue button and select yes or no for problem glass question") + + async handleproblemGlassQuestionsPage(testData: Partial){ + + await this.dontShareVinButton.click(); + await this.yestoGlassQuestionButton.click(); + await this.continueButton.click(); + } +} \ No newline at end of file diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index c8788b04a..4209daad6 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -28,6 +28,7 @@ import insuranceITAC21stCenturyTests from "./InsuranceITAC21stCentury"; import insuranceGeicoTests from "./InsuranceGeico"; import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState"; import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive"; +import insuranceOEMAllstateTests from "./InsuranceOEMAllState"; import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay"; import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal"; import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff"; @@ -82,6 +83,7 @@ const allStandardTests = [ {name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests}, {name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests}, // {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests}, + {name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests}, {name: "InsuranceUnverified", tests: insuranceUnverifiedTests}, // {name: "InsuranceGeico", tests: insuranceGeicoTests}, // {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests} @@ -323,7 +325,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { } export async function handleInsuranceFlow(testCase: TestCase) { - const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, flow } = testCase.testData; + const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, isOEM, flow } = testCase.testData; // Check if the insurance policy has endorsements const hasEndorsements = endorsements && endorsements.length > 0; @@ -332,6 +334,15 @@ export async function handleInsuranceFlow(testCase: TestCase) { let insuranceCompanyPage = testCase.pages.insuranceCompanyPage; await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData); + // Handle OEM scenario + if (isOEM) { + // User should be on heritage problem glass questions page after selecting Allstate on insurance company page + if (testCase.testData.isOEM === true) { + let problemGlassQuestionsPage = testCase.pages.problemGlassQuestionsPage; + await problemGlassQuestionsPage.handleproblemGlassQuestionsPage(testCase.testData); + } + } + // Handle ccPolicyInfoPage let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage; await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData); diff --git a/playwright-tests/tests/InsuranceOEMAllState.ts b/playwright-tests/tests/InsuranceOEMAllState.ts new file mode 100644 index 000000000..c5fc0dc14 --- /dev/null +++ b/playwright-tests/tests/InsuranceOEMAllState.ts @@ -0,0 +1,88 @@ +//Imports here +import { ITestData } from 'framework/TestData' +import { ServiceLocation, DamageType, PaymentType, PartQuestionType } 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("InsuranceOEMAllstate"); + +// Now get the test data with the seeded faker +const insuranceOEMAllstateData: Partial = { + ...getDefaultTestData(), // Get default data with current seed + + // Key feature: Insurance flow with GEICO + paymentMethod: PaymentMethod.Insurance, + + // Insurance claim flags + isPolicyFound: false, + isPolicyUnverified: true, + isRecalVehicle: true, + isOEM: true, + + // Override customer details with specific name and California location + customerDetails: { + ...getDefaultTestData().customerDetails!, + firstName: 'Jane', + lastName: 'OEMTest', + address: { + ...getDefaultTestData().customerDetails!.address, + city: 'Richmond', + state: 'Virginia', + postalCode: '23219' + } + }, + + + // Insurance claim details + claimDetails: { + client: 'Allstate', + policyNumber: 'MockOEMTest', + policyDeductible: "Unverified", + policyZip: '43085', + damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}), + damageCause: DamageType.Rock + }, + + // Hyundai vehicle details with VIN lookup + vehicleDetails: { + ...getDefaultTestData().vehicleDetails!, + year: '2024', + make: 'Jeep', + model: 'Wrangler', + style: '4 door utility', + vehicleLookupType: VehicleLookupType.Zip, + }, + + // Part questions related to recalibration + partQuestions: [ + { + partQuestionType: PartQuestionType.GeneralQuestion1, + isOnPage: true, + optionToSelect: 'Yes' + }, + ], + + // Override for in-shop appointment + appointmentDetails: { + serviceLocation: ServiceLocation.InShop, + shopAddress: '5719 Brandt Pike, Dayton, OH 45424', + appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate + }, + + // Override payment details (empty because we skip payment method page in insurance flow) + paymentDetails: {} +} + +const insuranceOEMAllstateTests: ITestCase[] = []; + +const tc = { + name: `InsuranceOEMAllstate`, + tags: ['@E2E','@InsuranceOEMAllstate', '@test_report', '@Insurance'], + testData: insuranceOEMAllstateData +}; +insuranceOEMAllstateTests.push(tc); + +export default insuranceOEMAllstateTests; \ No newline at end of file From 05643bdcf342228971fb400d221a75eb79b6b877 Mon Sep 17 00:00:00 2001 From: JennyNou <167806377+JennyNou@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:25:18 -0500 Subject: [PATCH 005/233] Fix step name for clarity --- playwright-tests/pages/ProblemGlassQuestionsPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/ProblemGlassQuestionsPage.ts b/playwright-tests/pages/ProblemGlassQuestionsPage.ts index 08124eef0..e875ed262 100644 --- a/playwright-tests/pages/ProblemGlassQuestionsPage.ts +++ b/playwright-tests/pages/ProblemGlassQuestionsPage.ts @@ -32,7 +32,7 @@ export class ProblemGlassQuestionsPage extends InsuranceBasePage { } - @step("ProblemGlassQuestionsPage >> Click continue button and select yes or no for problem glass question") + @step("ProblemGlassQuestionsPage >> Handle VIN popup and answer glass question") async handleproblemGlassQuestionsPage(testData: Partial){ From defd461145e5e9ed706031ba552ae3578171f576 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Fri, 14 Nov 2025 11:08:00 -0500 Subject: [PATCH 006/233] Salesforcewebchat Changes --- .../salesforce-helper-dev.js | 3 +-- .../salesforce-webchat/salesforce-webchat.vue | 27 +++---------------- .../sierra-webchat/sierra-webchat.vue | 2 +- src/helpers/webchat-helper.js | 2 +- 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js index a171fb974..bc1b75dd3 100644 --- a/src/digital-components/salesforce-webchat/salesforce-helper-dev.js +++ b/src/digital-components/salesforce-webchat/salesforce-helper-dev.js @@ -7,7 +7,7 @@ function setupCustomSettings( onWebchatCloseCallback ) { modifyEmbeddedSvcCallback(); - window.addEventListener("onEmbeddedMessagingReady", finishedLoadingCallback); + window.addEventListener("onEmbeddedMessagingButtonCreated", finishedLoadingCallback); window.addEventListener("onEmbeddedMessagingWindowMaximized", onWebchatOpenCallback); window.addEventListener("onEmbeddedMessagingWindowClosed", onWebchatCloseCallback); } @@ -43,7 +43,6 @@ export function initializeSalesforceWebchatForDev( ); try { - console.log("initializing embeddedservice_bootstrap"); embeddedservice_bootstrap.settings.language = "en_US"; // For example, enter 'en' or 'en-US' embeddedservice_bootstrap.init( diff --git a/src/digital-components/salesforce-webchat/salesforce-webchat.vue b/src/digital-components/salesforce-webchat/salesforce-webchat.vue index 31719024e..1dca6de8c 100644 --- a/src/digital-components/salesforce-webchat/salesforce-webchat.vue +++ b/src/digital-components/salesforce-webchat/salesforce-webchat.vue @@ -16,7 +16,7 @@ export default { mixins: [analyticsMixin], data() { return { - isAgentAvailable: false, + isWebchatLoaded: false, isWebchatOpen: false, }; }, @@ -37,25 +37,9 @@ export default { "https://safelite2--dev.sandbox.my.site.com/ESWSalesCentralTFN1755660094477/assets/js/bootstrap.min.js"; script.onload = this.initializeSalesforceWebchat; document.body.appendChild(script); - - // Notify funnel-header about Salesforce chat open/close state - this._salesforceChatOpenObserver = new MutationObserver(() => { - const isOpen = !!document.querySelector(".embeddedServiceSidebar"); - window.dispatchEvent( - new CustomEvent("salesforce-chat-visibility", { detail: { open: isOpen } }) - ); - }); - this._salesforceChatOpenObserver.observe(document.body, { - childList: true, - subtree: true, - }); }, beforeUnmount() { // Clean up event listeners and observers - window.removeEventListener( - "salesforce-chat-visibility", - this._handleSalesforceChatVisibility - ); window.removeEventListener("sierra-chat-transfer", this._handleSierraToSalesforceTransfer); if (this._salesforceChatOpenObserver) { this._salesforceChatOpenObserver.disconnect(); @@ -93,16 +77,13 @@ export default { } }, modifyEmbeddedSvcCallback() { - window.embeddedservice_bootstrap.settings.avatarImgURL = require("@/assets/img/salesforce-webchat/avatar_webchat.png"); - window.embeddedservice_bootstrap.settings.prechatBackgroundImgURL = require("@/assets/img/salesforce-webchat/safelite_logo_webchat.png"); - window.embeddedservice_bootstrap.settings.smallCompanyLogoImgURL = require("@/assets/img/salesforce-webchat/minimized_chat_bubble_webchat.png"); - window.embeddedservice_bootstrap.settings.displayHelpButton = false; + window.embeddedservice_bootstrap.settings.hideChatButtonOnLoad = true; window.addEventListener("onEmbeddedMessagingConversationStarted", () => { this.pushEventForChatsToGA("support_chat", "chat_opened", "Live Agent", true); }); }, finishedLoadingCallback(data) { - this.isAgentAvailable = data.isAgentAvailable; + this.isWebchatLoaded = true; this.updateShouldShowWebchatButton(); }, onWebchatOpenCallback() { @@ -118,7 +99,7 @@ export default { updateShouldShowWebchatButton() { this.webchatGlobalNonpersistedState.showWebchatButton = !this.hideSalesforceWebchatLaunchButton && - this.isAgentAvailable && + this.isWebchatLoaded && !this.isWebchatOpen; }, }, diff --git a/src/digital-components/sierra-webchat/sierra-webchat.vue b/src/digital-components/sierra-webchat/sierra-webchat.vue index 6248ffa2d..4d43187e2 100644 --- a/src/digital-components/sierra-webchat/sierra-webchat.vue +++ b/src/digital-components/sierra-webchat/sierra-webchat.vue @@ -172,7 +172,7 @@ export default { return; } } - window.embeddedservice_bootstrap.bootstrapEmbeddedService(); + window.embeddedservice_bootstrap.utilAPI.launchChat(); } }; window.addEventListener("sierra-chat-transfer", this._handleSierraTransfer); diff --git a/src/helpers/webchat-helper.js b/src/helpers/webchat-helper.js index 90fc4a29a..e5254d08d 100644 --- a/src/helpers/webchat-helper.js +++ b/src/helpers/webchat-helper.js @@ -11,7 +11,7 @@ export const webchatHelper = () => { if (webchatGlobalNonpersistedState.shouldLaunchSierra) { window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat } else { - window.embeddedservice_bootstrap.bootstrapEmbeddedService(); // launches the salesForce prechat form + window.embeddedservice_bootstrap.utilAPI.launchChat(); // launches the salesForce prechat form } } From 1188f7a6e943a3100b14f9cc8324b710a5e860e5 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Mon, 17 Nov 2025 15:02:08 -0500 Subject: [PATCH 007/233] CASH-1884 CASH-1884 save quote broke in prod --- src/router/methods/after-each.js | 4 ++++ src/router/methods/helpers/consume-referral-info.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/router/methods/after-each.js b/src/router/methods/after-each.js index 1e1f80134..608119399 100644 --- a/src/router/methods/after-each.js +++ b/src/router/methods/after-each.js @@ -1,6 +1,10 @@ import analyticsMixin from "@/mixins/analytics-mixin"; +import store from "@/store"; +import { storeMutations } from "@/constants/store-mutations"; export async function afterEach(to, from) { + store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); + // digital consumer logging analyticsMixin.methods.logDigitalConsumer(); diff --git a/src/router/methods/helpers/consume-referral-info.js b/src/router/methods/helpers/consume-referral-info.js index 87700d500..125aed84f 100644 --- a/src/router/methods/helpers/consume-referral-info.js +++ b/src/router/methods/helpers/consume-referral-info.js @@ -10,6 +10,7 @@ export async function consumeReferralQuerystrings() { let parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT); let correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID); let referralDate = null; + let savedSessionId = null; // If no referral number in querystring, check if heritage funnel updated last. // This would indicate a customer that went to heritage but did not return through the @@ -20,6 +21,7 @@ export async function consumeReferralQuerystrings() { parentAccount = getFunnelCookie().ReferralParentAccountNumber; correlationId = getFunnelCookie().ReferralCorrelationId; referralDate = getFunnelCookie().ReferralDate; + savedSessionId = getFunnelCookie().SavedSessionId; } if (referralNumber) { @@ -30,6 +32,10 @@ export async function consumeReferralQuerystrings() { if (referralDate) { store.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); } + if (savedSessionId) { + store.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId); + } + updateOrCreateFunnelCookie(); } } From 61227af45d418a42c7fb81f266f9929179373be0 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Mon, 17 Nov 2025 15:08:14 -0500 Subject: [PATCH 008/233] CASH-1884 prettier --- src/router/methods/helpers/consume-referral-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/router/methods/helpers/consume-referral-info.js b/src/router/methods/helpers/consume-referral-info.js index 125aed84f..6393eedc7 100644 --- a/src/router/methods/helpers/consume-referral-info.js +++ b/src/router/methods/helpers/consume-referral-info.js @@ -35,7 +35,7 @@ export async function consumeReferralQuerystrings() { if (savedSessionId) { store.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId); } - + updateOrCreateFunnelCookie(); } } From 5090d2f2313c74a90f26323da9d0e19877eb6b24 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Mon, 17 Nov 2025 15:23:14 -0500 Subject: [PATCH 009/233] Update locator for Afterpay banner validation --- playwright-tests/pages/ServicePackagesPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright-tests/pages/ServicePackagesPage.ts b/playwright-tests/pages/ServicePackagesPage.ts index 63a60a6e6..ba95f67cd 100644 --- a/playwright-tests/pages/ServicePackagesPage.ts +++ b/playwright-tests/pages/ServicePackagesPage.ts @@ -44,7 +44,7 @@ export class ServicePackagesPage extends BasePage { this.promoCodeTextbox = this.page.getByLabel('Enter a promo code'); this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' }); this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); - this.afterPayBanner = this.page.locator('#afterpay-banner'); + this.afterPayBanner = this.page.locator('div.afterpay-modal-banner'); this.glassOnlypackagePrice = this.page.locator('fieldset:has(legend#ServicePackageQuestion) label').filter({ hasText: 'Glass service only' }).locator('.pricing-info'); } From d9ef3f65b5900e45c4f56fbc0218b3598f15f292 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Mon, 17 Nov 2025 16:18:00 -0500 Subject: [PATCH 010/233] Rename for clarification --- playwright-tests/framework/TestPages.ts | 6 +++--- ...tionsPage.ts => HeritageProblemGlassQuestionsPage.ts} | 9 ++++----- playwright-tests/pages/OrderConfirmationPage.ts | 2 +- playwright-tests/tests/0000__M.test.ts | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) rename playwright-tests/pages/{ProblemGlassQuestionsPage.ts => HeritageProblemGlassQuestionsPage.ts} (81%) diff --git a/playwright-tests/framework/TestPages.ts b/playwright-tests/framework/TestPages.ts index b0baf2db1..c2cc04b35 100644 --- a/playwright-tests/framework/TestPages.ts +++ b/playwright-tests/framework/TestPages.ts @@ -24,7 +24,7 @@ import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage" import { DuplicateCheckPage } from "../pages/DuplicateCheckPage" import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage" import { PolicyInfoSubmittedPage } from "../pages/PolicyInfoSubmittedPage" -import { ProblemGlassQuestionsPage } from "../pages/ProblemGlassQuestionsPage" +import { HeritageProblemGlassQuestionsPage } from "../pages/HeritageProblemGlassQuestionsPage" import RecalibrationInfoPage from "../pages/RecalibrationInfoPage" import { CoverageStatementPage } from "../pages/CoverageStatementPage" import { VerifyDetailsPage } from "../pages/VerifyDetailsPage" @@ -49,7 +49,7 @@ export interface ITestPages { paymentMethodPage: PaymentMethodPage, policyInfoSubmittedPage: PolicyInfoSubmittedPage, policyVehiclesPage: PolicyVehiclesPage, - problemGlassQuestionsPage: ProblemGlassQuestionsPage, + heritageProblemGlassQuestionsPage: HeritageProblemGlassQuestionsPage, recalibrationInfoPage: RecalibrationInfoPage, schedulePage: SchedulePage, mobileDetailsPage: MobileDetailsPage, @@ -85,7 +85,7 @@ export const createTestPages: TestPagesFactory = (page: Page) => { paymentMethodPage: new PaymentMethodPage(page), policyInfoSubmittedPage: new PolicyInfoSubmittedPage(page), policyVehiclesPage: new PolicyVehiclesPage(page), - problemGlassQuestionsPage: new ProblemGlassQuestionsPage(page), + heritageProblemGlassQuestionsPage: new HeritageProblemGlassQuestionsPage(page), recalibrationInfoPage: new RecalibrationInfoPage(page), schedulePage: new SchedulePage(page), mobileDetailsPage: new MobileDetailsPage(page), diff --git a/playwright-tests/pages/ProblemGlassQuestionsPage.ts b/playwright-tests/pages/HeritageProblemGlassQuestionsPage.ts similarity index 81% rename from playwright-tests/pages/ProblemGlassQuestionsPage.ts rename to playwright-tests/pages/HeritageProblemGlassQuestionsPage.ts index e875ed262..097fd5200 100644 --- a/playwright-tests/pages/ProblemGlassQuestionsPage.ts +++ b/playwright-tests/pages/HeritageProblemGlassQuestionsPage.ts @@ -1,10 +1,9 @@ import { expect, type Locator, type Page } from '@playwright/test'; -import { IClaimDetails, ICustomerDetails } from 'safelite-playwright-core'; import { InsuranceBasePage } from './InsuranceBasePage'; import { ITestData } from 'framework/TestData'; import { step } from 'framework/localTypes/Step'; -export class ProblemGlassQuestionsPage extends InsuranceBasePage { +export class HeritageProblemGlassQuestionsPage extends InsuranceBasePage { readonly provideVinManuallyButton: Locator; readonly provideLicensePlateButton: Locator; readonly provideHomeAddressButton: Locator; @@ -32,9 +31,9 @@ export class ProblemGlassQuestionsPage extends InsuranceBasePage { } - @step("ProblemGlassQuestionsPage >> Handle VIN popup and answer glass question") - - async handleproblemGlassQuestionsPage(testData: Partial){ + @step("HeritageProblemGlassQuestionsPage >> Handle VIN popup and answer glass question on heritage") + // Handles OEM scenario with vehicle that has only one glass question + async handleHeritageProblemGlassQuestionsPage(testData: Partial){ await this.dontShareVinButton.click(); await this.yestoGlassQuestionButton.click(); diff --git a/playwright-tests/pages/OrderConfirmationPage.ts b/playwright-tests/pages/OrderConfirmationPage.ts index 6bf10926c..a65c80fd2 100644 --- a/playwright-tests/pages/OrderConfirmationPage.ts +++ b/playwright-tests/pages/OrderConfirmationPage.ts @@ -47,7 +47,7 @@ export class OrderConfirmationPage extends BasePage { async validateOrderConfirmationPage(testData: Partial) { // Destructure data we use const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow, - isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified, isOEM } = testData; + isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified} = testData; await this.serviceText.waitFor({ state: "visible" }); Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase())); diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 4209daad6..4f0bbe6bf 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -338,8 +338,8 @@ export async function handleInsuranceFlow(testCase: TestCase) { if (isOEM) { // User should be on heritage problem glass questions page after selecting Allstate on insurance company page if (testCase.testData.isOEM === true) { - let problemGlassQuestionsPage = testCase.pages.problemGlassQuestionsPage; - await problemGlassQuestionsPage.handleproblemGlassQuestionsPage(testCase.testData); + let HeritageProblemGlassQuestionsPage = testCase.pages.heritageProblemGlassQuestionsPage; + await HeritageProblemGlassQuestionsPage.handleHeritageProblemGlassQuestionsPage(testCase.testData); } } From 9f1e230ea23d82499b068637a170582495958f4b Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Tue, 18 Nov 2025 13:35:15 -0500 Subject: [PATCH 011/233] Re-add CASH-1620 to develop. --- src/constants/cart-item-categories.js | 2 +- src/constants/cart-item-types.js | 2 +- src/constants/part-type-strings.js | 1 + src/constants/quote-page-discounts.js | 80 +++++++++++++ .../service-package-radio-for-afterpay.vue | 4 +- .../service-package-radio.vue | 9 +- src/fmg-components/cart/cart.vue | 79 ++++++------- src/layouts/quote/quote.spec.js | 2 +- src/layouts/quote/quote.vue | 104 ++++++++++------- .../service-package-question.spec.js | 53 ++++----- .../service-package-question.vue | 110 ++++++++---------- .../service-package-radio.vue | 7 +- src/mixins/base-mixin.js | 4 +- 13 files changed, 260 insertions(+), 197 deletions(-) create mode 100644 src/constants/quote-page-discounts.js diff --git a/src/constants/cart-item-categories.js b/src/constants/cart-item-categories.js index 6bbb960b9..5c8faafcf 100644 --- a/src/constants/cart-item-categories.js +++ b/src/constants/cart-item-categories.js @@ -3,7 +3,7 @@ const cartItemCategories = { GLASS_PARTS: "glassParts", SUPPORTING_ITEMS: "supportingItems", PROMOS: "promos", - SERVICE_PACKAGE_DISCOUNT: "servicePackageDiscount", + QUOTE_PAGE_DISCOUNT: "quotePageDiscount", }; export { cartItemCategories }; diff --git a/src/constants/cart-item-types.js b/src/constants/cart-item-types.js index 0bb418d43..8f70aeaed 100644 --- a/src/constants/cart-item-types.js +++ b/src/constants/cart-item-types.js @@ -10,7 +10,7 @@ const cartItemTypes = { PROMOS: "PROMOS", EARLY_BIRD: "EARLY BIRD", SUPPLIES_REPAIR: "SUPPLIES-REPAIR", - SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", + QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT", DONATION: "DONATION", }; diff --git a/src/constants/part-type-strings.js b/src/constants/part-type-strings.js index 741132e28..17b96d2fb 100644 --- a/src/constants/part-type-strings.js +++ b/src/constants/part-type-strings.js @@ -10,6 +10,7 @@ const partTypeStrings = { REPAIR_FEE: "REPAIR FEE", EARLY_BIRD: "EARLY BIRD", SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", + QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT", DONATION: "DONATION", }; diff --git a/src/constants/quote-page-discounts.js b/src/constants/quote-page-discounts.js new file mode 100644 index 000000000..2dd32d915 --- /dev/null +++ b/src/constants/quote-page-discounts.js @@ -0,0 +1,80 @@ +import { packageNames } from "./package-names"; +import { partTypeStrings } from "./part-type-strings"; + +export const quotePageDiscountTable = [ + // Repair + { + name: "Repair Glass Only", + experimentCode: "Show_GlassOnly_Repair_Discount", + partNumber: "GL RPR CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "GLASS RPR GEOTARGET DISC", + packageLevel: packageNames.TIER_ONE, + }, + { + name: "Repair Standard", + experimentCode: "Show_Standard_Repair_Discount", + partNumber: "STD RPR CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "STD RPR GEOTARGET DISC", + packageLevel: packageNames.TIER_TWO, + }, + { + name: "Repair Premium", + experimentCode: "Show_Premium_Repair_Discount", + partNumber: "PRM RPR CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "PREM RPR GEOTARGET DISC", + packageLevel: packageNames.TIER_THREE, + }, + // Replace - No recal + { + name: "Replace Glass Only", + experimentCode: "Show_GlassOnly_Replace_Discount", + partNumber: "GL RPL CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "GLASS RPL GEOTARGET DISC", + packageLevel: packageNames.TIER_ONE, + }, + { + name: "Replace Standard", + experimentCode: "Show_Standard_Replace_Discount", + partNumber: "STD RPL CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "STD RPL GEOTARGET DISC", + packageLevel: packageNames.TIER_TWO, + }, + { + name: "Replace Premium", + experimentCode: "Show_Premium_Replace_Discount", + partNumber: "PRM RPL CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "PREM RPL GEOTARGET DISC", + packageLevel: packageNames.TIER_THREE, + }, + // Replace - with recal + { + name: "Recal Glass Only", + experimentCode: "Show_GlassOnly_Recal_Discount", + partNumber: "GL RCL CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "GLASS RCL GEOTARGET DISC", + packageLevel: packageNames.TIER_ONE, + }, + { + name: "Recal Standard", + experimentCode: "Show_Standard_Recal_Discount", + partNumber: "STD RCL CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "STD RCL GEOTARGET DISC", + packageLevel: packageNames.TIER_TWO, + }, + { + name: "Recal Premium", + experimentCode: "Show_Premium_Recal_Discount", + partNumber: "PRM RCL CSH PKG", + partType: partTypeStrings.QUOTE_PAGE_DISCOUNT, + description: "PREM RCL GEOTARGET DISC", + packageLevel: packageNames.TIER_THREE, + }, +]; diff --git a/src/experiment-components/service-package-radio-for-afterpay.vue b/src/experiment-components/service-package-radio-for-afterpay.vue index 2e746e4f2..7f7e91084 100644 --- a/src/experiment-components/service-package-radio-for-afterpay.vue +++ b/src/experiment-components/service-package-radio-for-afterpay.vue @@ -145,9 +145,7 @@ export default { return "$" + decimalPrice.toFixed(2); }, hasPackageDiscount() { - return ( - this.additionalButtonData.servicePackageDiscount && this.additionalButtonData.Text - ); + return this.additionalButtonData.hasDiscount && this.additionalButtonData.Text; }, }, }; diff --git a/src/experiment-components/service-package-radio.vue b/src/experiment-components/service-package-radio.vue index ee7e12712..a116fecd8 100644 --- a/src/experiment-components/service-package-radio.vue +++ b/src/experiment-components/service-package-radio.vue @@ -4,11 +4,11 @@ class="package-label pricing-by-day-pkg-lbl" :class="[ this.buttonLabelSubCopy ? 'has-subheader' : '', - !this.additionalButtonData.servicePackageDiscount ? 'adjust-top' : '', + !this.additionalButtonData.hasDiscount ? 'adjust-top' : '', ]" for="testradio">
-
+

@@ -84,10 +84,7 @@

diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index d1bd2a939..e890e53cc 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -190,6 +190,7 @@ import { cartItemCategories } from "@/constants/cart-item-categories"; import { cartItemTypes } from "@/constants/cart-item-types"; import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance"; import { experimentSettings } from "@/constants/experiments"; +import { quotePageDiscountTable } from "@/constants/quote-page-discounts"; export default { name: "cart", @@ -256,11 +257,11 @@ export default { return subTotal; }, - getServicePackageDiscount() { - const servicePackageDiscountLineItems = this.servicePackageDiscountCartItem; + getQuotePageDiscount() { + const quotePageDiscountLineItems = this.quotePageDiscountCartItem; let subTotal = 0; - subTotal += servicePackageDiscountLineItems?.subTotal ?? 0; + subTotal += quotePageDiscountLineItems?.subTotal ?? 0; return subTotal; }, getVapsCartItemsForSelectedPackage(packageName) { @@ -303,7 +304,8 @@ export default { getLineItemAmount(amount, useVerifyingText, category) { if (useVerifyingText) return this.verifyingCoverageText; - return category == "promos" || category == "servicePackageDiscount" + return category == cartItemCategories.PROMOS || + category == cartItemCategories.QUOTE_PAGE_DISCOUNT ? "(" + this.currencyFormatter.format(amount * -1) + ")" : this.currencyFormatter.format(amount); }, @@ -316,16 +318,21 @@ export default { if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) { this.saveVaps(this.lineItems); // Check if service package discount should be removed after vaps change - if ( - this.servicePackageDiscountCartItem && - this.discountPackageNames != this.packageLevel - ) { - this.lineItems.supportingItems = this.lineItems.supportingItems.filter( - (lineItemsToKeep) => - lineItemsToKeep.cartItemType != - this.servicePackageDiscountCartItem.cartItemType + if (this.quotePageDiscountCartItem) { + const existingLineItem = this.quotePageDiscountCartItem.lineItems.at(0); + const discountInfo = quotePageDiscountTable.find( + (info) => info.partNumber === existingLineItem?.partNumber ); - shouldSaveSupportingItems = true; + const packageLevelForDiscount = discountInfo?.packageLevel; + + if (this.packageLevel !== packageLevelForDiscount) { + this.lineItems.supportingItems = this.lineItems.supportingItems.filter( + (lineItemsToKeep) => + lineItemsToKeep.cartItemType != + this.quotePageDiscountCartItem.cartItemType + ); + shouldSaveSupportingItems = true; + } } } if (shouldSaveSupportingItems) { @@ -483,8 +490,8 @@ export default { cartItems.push(this.mobileFeeCartItem); } - if (this.servicePackageDiscountCartItem) { - cartItems.push(this.servicePackageDiscountCartItem); + if (this.quotePageDiscountCartItem) { + cartItems.push(this.quotePageDiscountCartItem); } if (this.premiumAppointmentDiscountCartItem) { @@ -500,12 +507,6 @@ export default { return cartItems; }, }, - discountPackageNames() { - const discountServicePackage = experimentMixin.methods.getSettingValue( - experimentSettings.PROMO_ON_PACKAGE - ); - return getDiscountedPackageName(discountServicePackage); - }, servicePackageTitleWidget() { const servicePackageNames = this.getCmsContent( this.servicePackageOptionsCmsName, @@ -550,8 +551,8 @@ export default { } packagePrice += this.getVapsPrice(this.packageLevel); - if (this.servicePackageDiscountCartItem) { - packagePrice -= this.getServicePackageDiscount(); + if (this.quotePageDiscountCartItem) { + packagePrice -= this.getQuotePageDiscount(); } return packagePrice; @@ -928,25 +929,25 @@ export default { } return cartItem; }, - servicePackageDiscountCartItemName() { + quotePageDiscountCartItemName() { return this.getCmsContent("ServicePackageDiscountTextWidget", "Text"); }, - servicePackageDiscountCartItem() { + quotePageDiscountCartItem() { let cartItem = null; - const servicePackageDiscountLineItem = this.supportingItems.find( - (lineItem) => lineItem.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT + const quotePageDiscountLineItem = this.supportingItems.find( + (lineItem) => lineItem.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT ); - if (servicePackageDiscountLineItem) { + if (quotePageDiscountLineItem) { cartItem = { name: - this.servicePackageDiscountCartItemName + + this.quotePageDiscountCartItemName + Math.abs( - baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem) + baseMixin.methods.getTotalLineItemPrice(quotePageDiscountLineItem) ), - category: cartItemCategories.SERVICE_PACKAGE_DISCOUNT, - cartItemType: cartItemTypes.SERVICE_PACKAGE_DISCOUNT, + category: cartItemCategories.QUOTE_PAGE_DISCOUNT, + cartItemType: cartItemTypes.QUOTE_PAGE_DISCOUNT, isDisplayed: true, isRemovable: false, subTotal: 0, @@ -955,15 +956,15 @@ export default { isCoveredByInsurance: false, }; - servicePackageDiscountLineItem.cartItemType = cartItem.cartItemType; - cartItem.lineItems.push(servicePackageDiscountLineItem); + quotePageDiscountLineItem.cartItemType = cartItem.cartItemType; + cartItem.lineItems.push(quotePageDiscountLineItem); cartItem.subTotal += - (servicePackageDiscountLineItem.kitPrice ?? 0) + - (servicePackageDiscountLineItem.laborAmount ?? 0) + - (servicePackageDiscountLineItem.sellingPrice ?? 0); + (quotePageDiscountLineItem.kitPrice ?? 0) + + (quotePageDiscountLineItem.laborAmount ?? 0) + + (quotePageDiscountLineItem.sellingPrice ?? 0); - cartItem.salesTax += servicePackageDiscountLineItem.salesTax ?? 0; + cartItem.salesTax += quotePageDiscountLineItem.salesTax ?? 0; } return cartItem; @@ -972,7 +973,7 @@ export default { let cartItem = null; const otherSupportingItems = this.supportingItems.filter((item) => { - return item.partType != partTypeStrings.SERVICE_PACKAGE_DISCOUNT; + return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT; }); // Don't include fees they are part of the package total let otherSupportingItemsLineItems = diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index aea32211e..5d9f3bacb 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -82,7 +82,7 @@ jest.mock("@/mixins/base-mixin", () => ({ filterOutFees(items) { return null; }, - filterOutServicePackageDiscountPart(items) { + filterOutQuotePageDiscountPart(items) { return null; }, isFormValid(form) { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 7f9ea7efc..17326ec8e 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -31,10 +31,11 @@ :isRecalibrationOnOrder="isRecalibrationOnOrder" :shouldHideRecalibration="shouldHideRecalibration" @vapsItemsSelected="vapsItemsSelectedAction" - @servicePackageDiscountSelected="servicePackageDiscountSelectedAction" + @quotePageDiscountSelected="quotePageDiscountSelectedAction" @currentNumberOfPackages="currentNumberOfPackagesAction" :servicePackage="servicePackage" :activePromos="lineItems.promos" + :availableQuotePageDiscounts="quoteDiscountPartsInfo" v-on="{ 'buttonEvent.openModal': openModalAction }" validationRules="option-required" isRequired /> @@ -174,6 +175,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas import { storeMutations } from "@/constants/store-mutations"; import { debugLog } from "@/helpers/debug-log-helper"; import { savePageData } from "@/router/methods/helpers/save-page-data"; +import { quotePageDiscountTable } from "../../constants/quote-page-discounts"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); @@ -184,6 +186,14 @@ const ACCEPTED_QUOTE_TYPES = { INSURANCE: "insurance", }; +function getAvailableQuotePageDiscounts() { + const activeDiscounts = quotePageDiscountTable?.filter((discountEntry) => + experimentMixin?.methods?.hasSettingEqualTo(discountEntry?.experimentCode, "true") + ); + + return activeDiscounts ?? []; +} + export default { name: "quote", async beforeRouteEnter(to, from, next) { @@ -300,29 +310,22 @@ export default { const lineItems = deepClone(store.getters.order.lineItems); lineItems.vaps = lineItems.vaps ?? []; const nullSafeGlassParts = lineItems.glassParts ?? []; - const servicePackageDiscountSettingValue = experimentMixin.methods.getSettingValue( - experimentSettings.SERVICE_PACKAGE_DISCOUNT - ); - const isServicePackageDiscount = servicePackageDiscountSettingValue === "True"; - //Service package cash discount api call when experiment is active - var servicePackageDiscountPart = []; - if (isServicePackageDiscount) { - const servicePackageDiscountPartResponse = - await baseMixin.methods.dispatchStoreActionWithLogging( - storeActions.GET_SERVICE_PACKAGE_DISCOUNT_PART, - null, - "quote" - ); - if (servicePackageDiscountPartResponse.data) { - servicePackageDiscountPart.push(servicePackageDiscountPartResponse.data); - } - } + + const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts(); + + const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({ + partNumber: partInfo.partNumber, + partType: partInfo.partType, + description: partInfo.description, + isInsurable: null, + })); + const availableLineItems = [ resultMap.rainRepel, ...resultMap.supportingItems, ...resultMap.wipers, ...nullSafeGlassParts, - ...servicePackageDiscountPart, + ...quotePageDiscountParts, ]; // When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards @@ -349,7 +352,7 @@ export default { false ); - // This will remove any servicePackageDiscount item from lineItems.supportingItems + // This will remove any discount item from lineItems.supportingItems baseMixin.methods.dispatchStoreAction( storeActions.SAVE_SUPPORTING_ITEMS, resultMap.supportingItems, @@ -451,10 +454,9 @@ export default { ) : baseMixin.methods.filterOutFees(availableLineItems); - const lineItemsNoDiscount = - baseMixin.methods.filterOutServicePackageDiscountPart( - lineItemsForCalculatingPrice - ); + const lineItemsNoDiscount = baseMixin.methods.filterOutQuotePageDiscountPart( + lineItemsForCalculatingPrice + ); if (isInsuranceFromQueryString != null) { if (isInsuranceFromQueryString.toLowerCase() === "true") { @@ -578,17 +580,8 @@ export default { isRecalibrationOnOrder() { return store.getters.isRecalibrationOnOrder; }, - isServicePackageDiscountOnOrder() { - return containsLineItemWithPartType( - partTypeStrings.SERVICE_PACKAGE_DISCOUNT, - this.availableLineItems - ); - }, - servicePackageDiscountParts() { - return findLineItemsWithPartType( - partTypeStrings.SERVICE_PACKAGE_DISCOUNT, - this.availableLineItems - ); + isQuotePageDiscountOnOrder() { + return this.quotePageDiscountPartsInfo?.length > 0; }, shouldHideRecalibration() { return ( @@ -617,6 +610,23 @@ export default { thresholdAmount && (thresholdAmount = parseInt(thresholdAmount)); return thresholdAmount; }, + quoteDiscountPartsInfo() { + const defs = getAvailableQuotePageDiscounts(); + const withPrices = defs + .map((def) => { + const match = this.availableLineItems?.find?.( + (pricedItem) => pricedItem.partNumber === def.partNumber + ); + + return { + packageLevel: def.packageLevel, + item: match, + }; + }) + .filter((result) => result.item); + + return withPrices; + }, }, methods: { getColCount() { @@ -660,7 +670,7 @@ export default { vapsItemsSelectedAction(vapsItemsSelected) { this.lineItems.vaps = vapsItemsSelected; }, - servicePackageDiscountSelectedAction(supportingItemsSelected) { + quotePageDiscountSelectedAction(supportingItemsSelected) { this.lineItems.supportingItems = supportingItemsSelected; }, backButtonAction() { @@ -722,10 +732,10 @@ export default { false ); } - if (this.isInsuranceSelected && this.isServicePackageDiscountOnOrder) { + if (this.isInsuranceSelected && this.isQuotePageDiscountOnOrder) { let supportingItems = this.lineItems.supportingItems; supportingItems = supportingItems.filter((item) => { - return item.partType != this.servicePackageDiscountParts[0].partType; + return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT; }); this.dispatchStoreAction( this.storeActions.SAVE_SUPPORTING_ITEMS, @@ -811,19 +821,23 @@ export default { lineItemsToPrice = baseMixin.methods.filterOutRecalibration(lineItemsToPrice); } - if (this.isServicePackageDiscountOnOrder) { + if (this.isQuotePageDiscountOnOrder) { lineItemsToPrice = lineItemsToPrice?.filter((item) => { - return ( - item.partType != this.servicePackageDiscountParts[0].partType - ); + return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT; }); } let price = 0; if ( - tier.additionalButtonData?.servicePackageDiscount && - this.isServicePackageDiscountOnOrder + tier.additionalButtonData?.hasDiscount && + this.isQuotePageDiscountOnOrder ) { - lineItemsToPrice.push(...this.servicePackageDiscountParts); + const parts = this.quoteDiscountPartsInfo; + const matchPart = parts.find( + (discountInfo) => discountInfo.packageLevel === tier + ); + if (matchPart) { + lineItemsToPrice.push(matchPart); + } } price += baseMixin.methods.getTierOnePackagePrice( baseMixin.methods.filterOutFees(lineItemsToPrice) diff --git a/src/layouts/quote/service-package-question/service-package-question.spec.js b/src/layouts/quote/service-package-question/service-package-question.spec.js index 523c0edce..a44dc463c 100644 --- a/src/layouts/quote/service-package-question/service-package-question.spec.js +++ b/src/layouts/quote/service-package-question/service-package-question.spec.js @@ -14,6 +14,7 @@ import { experimentSettings } from "@/constants/experiments"; import baseMixin from "@/mixins/base-mixin.js"; import { partTypeStrings } from "@/constants/part-type-strings"; import { nextTick } from "vue"; +import { packageNames } from "../../../constants/package-names"; jest.mock("@/store", () => ({ commit: jest.fn(), @@ -73,9 +74,9 @@ describe("service-package-question.vue", () => { price: 35.5, }, { - partNumber: "DISC CASHSAVE70", + partNumber: "PRM RCL CSH PKG", description: null, - partType: "SERVICE PACKAGE DISCOUNT", + partType: "QUOTE PAGE DISCOUNT", price: -70, }, ], @@ -105,13 +106,27 @@ describe("service-package-question.vue", () => { description: null, kitPrice: 0, laborAmount: 0, - partNumber: "DISC CASHSAVE70", - partType: "SERVICE PACKAGE DISCOUNT", + partNumber: "PRM RCL CSH PKG", + partType: "QUOTE PAGE DISCOUNT", salesTax: null, sellingPrice: -70, }, ], }, + availableQuotePageDiscounts: [ + { + packageLevel: packageNames.TIER_THREE, + item: { + description: null, + kitPrice: 0, + laborAmount: 0, + partNumber: "PRM RCL CSH PKG", + partType: "QUOTE PAGE DISCOUNT", + salesTax: null, + sellingPrice: -70, + }, + }, + ], activePromos: [], }; const packageNameKey = "05_01_CSR_Quote_Standard_Repair"; @@ -136,41 +151,17 @@ describe("service-package-question.vue", () => { }); it("should return price when there is discount", () => { const wrapper = setupMocks({}); - const partType = partTypeStrings.SERVICE_PACKAGE_DISCOUNT; - wrapper.vm.isServicePackageDiscountOnOrder = containsLineItemWithPartType( - partType, - mockProps.lineItems.supportingItems - ); - const servicePackageDiscountLineItem = findLineItemsWithPartType( - partType, - mockProps.lineItems.supportingItems - ); - wrapper.vm.getServicePackageDiscountPrice(); - const price = baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem[0]); + const price = wrapper.vm.getDiscountPrice(packageNames.TIER_THREE); expect(Math.abs(price)).toEqual(70); }); - it("isServicePackageDiscountOnOrder returns true if it contains service package discount lineItems", () => { + it("isDiscountOnOrder returns true if it contains quote package discount lineItems", () => { // Arrange const wrapper = setupMocks({}); // Assert - expect(wrapper.vm.isServicePackageDiscountOnOrder).toBe(true); - }); - it("servicePackageDiscountParts should returns service package discount lineItems", () => { - // Arrange - const wrapper = setupMocks({}); - - // Assert - expect(wrapper.vm.servicePackageDiscountParts).toEqual([ - { - partNumber: "DISC CASHSAVE70", - description: null, - partType: "SERVICE PACKAGE DISCOUNT", - price: -70, - }, - ]); + expect(wrapper.vm.isDiscountOnOrder).toBe(true); }); it("should have the correct insurance pricing text when insurance is selected", () => { // Arrange diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index d1a99da08..e45147295 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -60,6 +60,7 @@ export default { servicePackage: null, isRecalibrationOnOrder: Boolean, shouldHideRecalibration: Boolean, + availableQuotePageDiscounts: null, }, data() { return { @@ -85,27 +86,18 @@ export default { selectedPackageName(newValue) { const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue); this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage); - const servicePackageDiscountPartInSelectedPackage = - this.getServicePackageDiscountPartForSelectedPackage(newValue); + const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue); let supportingItems = this.supportingLineItems; - if ( - containsLineItemWithPartType( - partTypeStrings.SERVICE_PACKAGE_DISCOUNT, - supportingItems - ) - ) { - supportingItems = supportingItems.filter( - (item) => item.partType !== partTypeStrings.SERVICE_PACKAGE_DISCOUNT - ); - } - if ( - servicePackageDiscountPartInSelectedPackage?.length > 0 && - supportingItems != null - ) { - supportingItems.push(servicePackageDiscountPartInSelectedPackage[0]); + + supportingItems = supportingItems.filter( + (item) => item.partType !== partTypeStrings.QUOTE_PAGE_DISCOUNT + ); + + if (discountPartInSelectedPackage) { + supportingItems.push(discountPartInSelectedPackage); } - this.$emit("servicePackageDiscountSelected", supportingItems); + this.$emit("quotePageDiscountSelected", supportingItems); }, servicePackage(newValue) { if (newValue !== null) { @@ -118,7 +110,7 @@ export default { return this.availableLineItems ?? []; }, supportingLineItems() { - return this.$store.getters.lineItems?.supportingItems; + return this.$store.getters.lineItems?.supportingItems ?? []; }, servicePackageAnswers() { const cmsWidgetName = this.isInsuranceSelected @@ -149,18 +141,15 @@ export default { ? this.getDiscountedPackagePriceString(answer.Name, false) : this.getDiscountedPackagePriceString( answer.Name, - this.isServicePackageDiscountOnOrder && - this.discountPackageNames == answer.Name + this.hasDiscountForPackage(answer.Name) ), buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName), additionalButtonData: { strikeThroughPrice: this.getPackagePriceString(answer.Name), - servicePackageDiscount: - this.isServicePackageDiscountOnOrder && - this.discountPackageNames == answer.Name, + hasDiscount: this.hasDiscountForPackage(answer.Name), Text: this.isInsuranceSelected ? false - : "Special: Save $" + this.getServicePackageDiscountPrice(), + : "Special: Save $" + this.getDiscountPrice(answer.Name), isInsuranceSelected: this.isInsuranceSelected, }, })); @@ -168,17 +157,8 @@ export default { this.$emit("currentNumberOfPackages", modifiedAnswers.length); return modifiedAnswers; }, - isServicePackageDiscountOnOrder() { - return containsLineItemWithPartType( - partTypeStrings.SERVICE_PACKAGE_DISCOUNT, - this.nullSafeAvailableLineItems - ); - }, - discountPackageNames() { - const discountServicePackage = experimentMixin.methods.getSettingValue( - experimentSettings.PROMO_ON_PACKAGE - ); - return getDiscountedPackageName(discountServicePackage); + isDiscountOnOrder() { + return this.availableQuotePageDiscounts?.length > 0; }, frontWipersApplicableForTierTwo() { return shouldFrontWipersBeAvailable( @@ -226,12 +206,6 @@ export default { isRepair() { return this.$store.getters.order.damage.isRepair; }, - servicePackageDiscountParts() { - return findLineItemsWithPartType( - partTypeStrings.SERVICE_PACKAGE_DISCOUNT, - this.nullSafeAvailableLineItems - ); - }, showRecalInServicePackages() { return this.isRecalibrationOnOrder && !this.shouldHideRecalibration; }, @@ -280,16 +254,16 @@ export default { const formattedPriceFloat = parseFloat( this.getPackagePrice(packageName, { discountedPrice: false, - servicePackageDiscount: false, + quotePageDiscount: false, }) ).toFixed(2); return "$" + formattedPriceFloat; }, - getDiscountedPackagePriceString(packageName, servicePackageDiscount) { + getDiscountedPackagePriceString(packageName, quotePageDiscount) { const formattedPriceFloat = parseFloat( this.getPackagePrice(packageName, { discountedPrice: true, - servicePackageDiscount, + quotePageDiscount, }) ).toFixed(2); @@ -298,7 +272,7 @@ export default { } return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat; }, - getPackagePrice(packageName, { discountedPrice = false, servicePackageDiscount = false }) { + getPackagePrice(packageName, { discountedPrice = false, quotePageDiscount = false }) { let lineItemsToPrice = [...this.nullSafeAvailableLineItems]; if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) { @@ -306,17 +280,20 @@ export default { } //remove service package discount part - if (this.isServicePackageDiscountOnOrder) { + if (this.isDiscountOnOrder) { lineItemsToPrice = lineItemsToPrice.filter((item) => { - return item.partType != this.servicePackageDiscountParts[0].partType; + return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT; }); } if (discountedPrice) { lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos)); } - if (servicePackageDiscount) { - lineItemsToPrice.push(...this.servicePackageDiscountParts); + if (quotePageDiscount) { + const part = this.getDiscountPartForSelectedPackage(packageName); + if (part) { + lineItemsToPrice.push(part); + } } let priceFloat = this.isInsuranceSelected ? 0 @@ -328,14 +305,16 @@ export default { return priceFloat; }, - getServicePackageDiscountPrice() { - if (this.isServicePackageDiscountOnOrder) { - let price = 0; - price += baseMixin.methods.getTotalLineItemPrice( - this.servicePackageDiscountParts[0] - ); + getDiscountPrice(packageName) { + const part = this.getDiscountPartForSelectedPackage(packageName); + + if (part) { + const price = baseMixin.methods.getTotalLineItemPrice(part); + return Math.abs(price); } + + return null; }, getVapsPrice(packageName, applyPromoDiscounts = false) { const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName); @@ -398,13 +377,18 @@ export default { return vapsLineItemsForSelectedPackage; }, - getServicePackageDiscountPartForSelectedPackage(packageName) { - const selectedPackage = this.servicePackageAnswers.filter( - (item) => item.value === packageName - ); - if (selectedPackage?.[0]?.additionalButtonData?.servicePackageDiscount) { - return this.servicePackageDiscountParts; - } else return []; + getDiscountPartForSelectedPackage(packageName) { + if (this.availableQuotePageDiscounts?.length > 0) { + const match = this.availableQuotePageDiscounts.find( + (discountInfo) => discountInfo.packageLevel === packageName + ); + return match?.item; + } + + return null; + }, + hasDiscountForPackage(packageName) { + return !!this.getDiscountPartForSelectedPackage(packageName); }, combineLineItemsWithoutDuplicates(lineItemsOne, lineItemsTwo) { const combinedLineItemArray = [...lineItemsTwo]; diff --git a/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue b/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue index 441a4f5cb..8bb7e921a 100644 --- a/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue +++ b/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue @@ -5,7 +5,7 @@ :class="[this.buttonLabelSubCopy ? 'has-subheader' : '']" for="testradio">
-
+

@@ -77,10 +77,7 @@

diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index 5204178de..813a3189a 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -129,9 +129,9 @@ export default { }); return lineItemsArray; }, - filterOutServicePackageDiscountPart(lineItems) { + filterOutQuotePageDiscountPart(lineItems) { const filteredLineItems = lineItems?.filter((item) => { - return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT); + return !item?.partType?.includes(partTypeStrings.QUOTE_PAGE_DISCOUNT); }); return filteredLineItems; }, From 9812c874df821eb9a431f34b01c292086ffbc0ec Mon Sep 17 00:00:00 2001 From: JennyNou <167806377+JennyNou@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:49:45 -0500 Subject: [PATCH 012/233] Move method to base page for reusability --- playwright-tests/pages/BasePage.ts | 16 ++++++++++++++++ playwright-tests/pages/PaymentMethodPage.ts | 16 ---------------- playwright-tests/tests/0000__M.test.ts | 4 +--- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index 6596986df..1562677a2 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -169,4 +169,20 @@ export class BasePage { Soft.expect(actualProgressPercentage).toBe(progressPercentage); console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`); } + + async validateOEMPart( isOEM: boolean): Promise { + if (isOEM!) { + // Get Vuex state from localStorage + const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); + + // Validate the presence of an OEM part + if (vuexState.order?.lineItems?.glassParts?.length > 0) { + const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber; + + await expect(firstGlassPartNumber.includes("OEM")).toBe(true); + } else { + throw new Error("No glass parts found in the order"); + } + } + } } \ No newline at end of file diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index b4b00a151..cbf6d01c8 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -496,22 +496,6 @@ export class PaymentMethodPage extends BasePage { let updatedAppointmentDate = parsedAppointmentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); return updatedAppointmentDate; } - - async validateOEMPart( isOEM: boolean): Promise { - if (isOEM == true) { - // Get Vuex state from localStorage - const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')')); - - // Validate the presence of an OEM part - if (vuexState.order?.lineItems?.glassParts?.length > 0) { - const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber; - - await expect(firstGlassPartNumber.includes("OEM")).toBe(true); - } else { - throw new Error("No glass parts found in the order"); - } - } - } @step("PaymentMethodPage >> Select Payment Method: ") async handlePaymentMethodPage(testData: Partial) { diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index 4f0bbe6bf..d2d85f4b9 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -335,12 +335,10 @@ export async function handleInsuranceFlow(testCase: TestCase) { await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData); // Handle OEM scenario - if (isOEM) { + if (isOEM === true) { // User should be on heritage problem glass questions page after selecting Allstate on insurance company page - if (testCase.testData.isOEM === true) { let HeritageProblemGlassQuestionsPage = testCase.pages.heritageProblemGlassQuestionsPage; await HeritageProblemGlassQuestionsPage.handleHeritageProblemGlassQuestionsPage(testCase.testData); - } } // Handle ccPolicyInfoPage From a154a93b36cde71ecf3f2b3e2f0d855431d5e9af Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:33:56 -0500 Subject: [PATCH 013/233] minor changes related to OEM scenario --- playwright-tests/framework/TestData.ts | 7 ++++--- playwright-tests/pages/PaymentMethodPage.ts | 7 +++++-- playwright-tests/tests/0000__M.test.ts | 4 ++-- playwright-tests/tests/InsuranceOEMAllState.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/playwright-tests/framework/TestData.ts b/playwright-tests/framework/TestData.ts index 9ed648426..1af123c41 100644 --- a/playwright-tests/framework/TestData.ts +++ b/playwright-tests/framework/TestData.ts @@ -3,7 +3,8 @@ import { PaymentMethod } from './localTypes/Enums' export interface ITestData extends base { // Put any project-specific data here. Anything useful to other Safelite projects should be submitted as a pull request to safelite-playwright-core. - paymentMethod: PaymentMethod - isOptedInForTextMessages: boolean - totalAmount?: number + paymentMethod: PaymentMethod, + isOptedInForTextMessages: boolean, + totalAmount?: number, + isForcedOEM?: boolean } \ No newline at end of file diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index cbf6d01c8..015a088ae 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -499,12 +499,15 @@ export class PaymentMethodPage extends BasePage { @step("PaymentMethodPage >> Select Payment Method: ") async handlePaymentMethodPage(testData: Partial) { - const { servicePackage, isRecalVehicle, paymentDetails } = testData; + const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData; await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage); await this.validatePaymentDetailsPage(testData); await this.ValidateAfterPayBreakOutSection(); - await this.validateOEMPart(testData.isOEM!); + + if (isForcedOEM){ + await this.validateOEMPart(isForcedOEM); + } // Verify VAPS wipers on backend for standard and premium packages if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) { diff --git a/playwright-tests/tests/0000__M.test.ts b/playwright-tests/tests/0000__M.test.ts index d2d85f4b9..cd8270a8a 100644 --- a/playwright-tests/tests/0000__M.test.ts +++ b/playwright-tests/tests/0000__M.test.ts @@ -325,7 +325,7 @@ async function runWorkflow(page: Page, testCase: TestCase) { } export async function handleInsuranceFlow(testCase: TestCase) { - const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, isOEM, flow } = testCase.testData; + const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, isForcedOEM, flow } = testCase.testData; // Check if the insurance policy has endorsements const hasEndorsements = endorsements && endorsements.length > 0; @@ -335,7 +335,7 @@ export async function handleInsuranceFlow(testCase: TestCase) { await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData); // Handle OEM scenario - if (isOEM === true) { + if (isForcedOEM) { // User should be on heritage problem glass questions page after selecting Allstate on insurance company page let HeritageProblemGlassQuestionsPage = testCase.pages.heritageProblemGlassQuestionsPage; await HeritageProblemGlassQuestionsPage.handleHeritageProblemGlassQuestionsPage(testCase.testData); diff --git a/playwright-tests/tests/InsuranceOEMAllState.ts b/playwright-tests/tests/InsuranceOEMAllState.ts index c5fc0dc14..c55939a4c 100644 --- a/playwright-tests/tests/InsuranceOEMAllState.ts +++ b/playwright-tests/tests/InsuranceOEMAllState.ts @@ -20,7 +20,7 @@ const insuranceOEMAllstateData: Partial = { isPolicyFound: false, isPolicyUnverified: true, isRecalVehicle: true, - isOEM: true, + isForcedOEM: true, // Override customer details with specific name and California location customerDetails: { From 37facdfecd992c9e87b51fe9fe20f77368f90f42 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Thu, 20 Nov 2025 11:46:54 -0500 Subject: [PATCH 014/233] CASH-1925 CASH-1925 add new endpoints for work order submit --- src/constants/endpoints.js | 8 ++++++++ src/store/index.js | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 17a8f9a08..c1771b3f5 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -124,6 +124,14 @@ const endpoints = { url: "/order/api/v1/order/save-session/fmg", method: "POST", }, + SubmitOrder: { + url: "/order/api/v1/order/submit-order", + method: "POST", + }, + SubmitOrderForPia: { + url: "/order/api/v1/order/submit-order-for-pia", + method: "POST", + }, LoadSession: { url: "/order/api/v1/order/load-session", method: "POST", diff --git a/src/store/index.js b/src/store/index.js index 6bccc5969..ca1d76f6f 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2363,9 +2363,17 @@ export const actions = { //affiliate cookie save only when submitAfterSave const affiliateCookies = submitAfterSave ? applicationUser.affiliateCookies : null; + let endPoint = endpoints.SaveSession.url; + if (createUnscheduledStatusWorkOrderForPIA) { + endPoint = endpoints.SubmitOrderForPia.url; + } + if (submitAfterSave) { + endPoint = endpoints.SubmitOrder.url; + } + return globalMethods.callHttpClient({ method: endpoints.SaveSession.method, - endpoint: endpoints.SaveSession.url, + endpoint: endPoint, payload: { submitAfterSave: submitAfterSave, userAgent: navigator.userAgent, From 9c94d7da6d01e4f440ecca308f0525dd92af8a82 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Thu, 20 Nov 2025 15:22:55 -0500 Subject: [PATCH 015/233] CASH-1873: Show Mobile First modal on page load --- .../mobile-first-modal/mobile-first-modal.vue | 1 + src/layouts/schedule/schedule.vue | 27 +++++++++++-------- src/styles/ux-variables.scss | 4 +-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue b/src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue index 2dbde3bd9..1ef9fbc54 100644 --- a/src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue +++ b/src/layouts/schedule/mobile-first-modal/mobile-first-modal.vue @@ -76,6 +76,7 @@ export default { let selectedTimeSlot = { timeSlot: this.selectedAppontment.timeSlot, routeCode: this.selectedAppontment.timeSlot.id, + date: this.selectedAppontment.date, }; this.$emit("confirm-appointment", selectedTimeSlot); this.modal.closeModal(); diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 3b4fec04c..1bc3c0945 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -469,6 +469,7 @@ export default { navigatingForward: false, shopListButton: shopListButton, lastSelectedInshopOrDropoffProvider: null, + selectedMobileFirstAppointment: false, }; }, async beforeRouteEnter(to, from, next) { @@ -597,7 +598,9 @@ export default { pricedMobileFeePart, shopProviderData.data ); - vm.initializeDatePicker(); + vm.initializeDatePicker().then(() => { + vm.showMobileFirstModal(); + }); }); }, computed: { @@ -858,7 +861,9 @@ export default { ); }, isMobileFirstModalOpen() { - return this.isMobileSelected ? this.$refs.mobileFirstModal?.getIsModalOpen() : false; + return this.selectableDatesMobile.days + ? this.$refs.mobileFirstModal?.getIsModalOpen() + : false; }, }, methods: { @@ -1187,8 +1192,6 @@ export default { moreShopTimeSlots.mobileTimeSlotsData.days ); - this.showMobileFirstModal(); - return moreShopTimeSlots; }, @@ -1678,7 +1681,9 @@ export default { this.zipCode = newZipCode.zipCode; this.zipCodeCtu = newZipCode.zipCodeCtu; this.selectedDate = null; - this.initializeDatePicker(); + this.initializeDatePicker().then(() => { + this.showMobileFirstModal(); + }); }); }, updateSelectedProviderAndZipCode(newZipCode, newProvider) { @@ -1716,7 +1721,6 @@ export default { } this.appointmentType = AppointmentTypeStrings.MOBILE; this.updateSelectedProvider(); - this.showMobileFirstModal(); } else if (newAppointmentType) { if (this.appointmentType != AppointmentTypeStrings.MOBILE) { // Clear last shop selected if appointment type was changed in any manner other than from Mobile @@ -1742,7 +1746,7 @@ export default { this.selectedDate = this.getFirstAvailableDate(); }, async showMobileFirstModal() { - if (this.appointmentType == AppointmentTypeStrings.MOBILE) { + if (!this.selectedRouteCodeData?.routeCode) { const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo( experimentSettings.SHOW_MOBILE_FIRST_APPT, "true" @@ -1824,9 +1828,6 @@ export default { } }, async getFirstAvailableMobileApptByTOD(timeOfDay) { - if (!this.selectableDatesMobile.days) { - await this.initializeDatePicker(); - } if (!this.selectableDatesMobile?.days?.length) { return null; } else { @@ -1853,6 +1854,10 @@ export default { } }, updateTimeSlotandNavigateForward(timeSlotObj) { + this.selectedMobileFirstAppointment = true; + this.appointmentType = AppointmentTypeStrings.MOBILE; + this.selectedDate = timeSlotObj.date; + this.updateSelectedProvider(); this.updateTimeSlot(timeSlotObj); this.forwardButtonAction(); }, @@ -1872,7 +1877,7 @@ export default { const hasValueChanged = newValue !== oldValue; const isDateDifferent = (newValue || oldValue) !== selectedDate; - if (hasValueChanged && isDateDifferent) { + if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) { this.selectedTimeSlotInfo = { timeSlot: { date: null, diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 770d95b42..0c275ad51 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -123,8 +123,8 @@ $body-color: $gray-600; //Fonts $font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif; -$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", - monospace; +$font-family-monospace: + UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; // stylelint-enable value-keyword-case $font-family-base: $font-family-sans-serif; $font-family-code: $font-family-monospace; From 2eb3b2722f8148867b541c697d9f3b214cfe52d2 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Fri, 21 Nov 2025 14:28:24 -0500 Subject: [PATCH 016/233] CASH-1873: Show waiting modal for Mobile First --- src/layouts/schedule/schedule.vue | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 1bc3c0945..246ce2986 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -470,6 +470,10 @@ export default { shopListButton: shopListButton, lastSelectedInshopOrDropoffProvider: null, selectedMobileFirstAppointment: false, + isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo( + experimentSettings.SHOW_MOBILE_FIRST_APPT, + "true" + ), }; }, async beforeRouteEnter(to, from, next) { @@ -1196,6 +1200,8 @@ export default { }, async initializeDatePicker() { + this.isShowMobileFirstAppt && this.showLoadingModal(); + this.selectedDate = null; const includeMobileTimeSlots = this.isServiceableMobile; @@ -1233,6 +1239,8 @@ export default { this.setDisplayWaitList(); } } + + this.isShowMobileFirstAppt && this.hideLoadingModal(); }, getScheduleApiResponse, getServiceZipCtuCodeFromStore() { @@ -1349,6 +1357,9 @@ export default { showLoadingModal() { this.$refs.loadingModal.showModal(); }, + hideLoadingModal() { + this.$refs.loadingModal.hideModal(); + }, async forwardButtonAction() { this.navigatingForward = true; var ctu = this.zipCodeCtu; @@ -1747,10 +1758,6 @@ export default { }, async showMobileFirstModal() { if (!this.selectedRouteCodeData?.routeCode) { - const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo( - experimentSettings.SHOW_MOBILE_FIRST_APPT, - "true" - ); const pmMobileDays = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_MOBILE_DAYS ); @@ -1805,7 +1812,7 @@ export default { ); } - if (isShowMobileFirstAppt) { + if (this.isShowMobileFirstAppt) { if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) { preSelectedMobileAppointment = firstMobilePMAppt; } else if ( From 81f005a77fb6cc4a19c634c907bcb6bd9c50f45f Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Mon, 24 Nov 2025 12:53:14 -0500 Subject: [PATCH 017/233] Fix typo for flag value --- src/layouts/quote/quote.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 17326ec8e..01b585349 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -581,7 +581,7 @@ export default { return store.getters.isRecalibrationOnOrder; }, isQuotePageDiscountOnOrder() { - return this.quotePageDiscountPartsInfo?.length > 0; + return this.quoteDiscountPartsInfo?.length > 0; }, shouldHideRecalibration() { return ( From 2738e5ceffdc064b45368422c5d4a2db885e42b9 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Mon, 24 Nov 2025 14:02:25 -0500 Subject: [PATCH 018/233] CASH-1891 font updates from Urbanist to UrbanistRegular, UrbanistSemibold. --- src/fmg-components/funnel-footer/funnel-footer.vue | 10 ++-------- src/layouts/payment-method/payment-method.vue | 5 +++-- .../service-package-radio/service-package-radio.vue | 6 ++---- src/layouts/schedule/shop-location/shop-location.vue | 2 +- src/ux-components/button-main/button-main.vue | 7 +------ 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/fmg-components/funnel-footer/funnel-footer.vue b/src/fmg-components/funnel-footer/funnel-footer.vue index a195c446b..572bed1ee 100644 --- a/src/fmg-components/funnel-footer/funnel-footer.vue +++ b/src/fmg-components/funnel-footer/funnel-footer.vue @@ -75,17 +75,11 @@ export default { :deep(a) { text-decoration: none; - &.new-window-link { - font-family: Urbanist, Arial, Helvetica, sans-serif; - margin: 0 0.75rem; - color: $gray-600; - font-weight: 400; - } + &.new-window-link, &.navigation-link { - font-family: Urbanist, Arial, Helvetica, sans-serif; + font-family: UrbanistRegular, Arial, Helvetica, sans-serif; margin: 0 0.75rem; color: $gray-600; - font-weight: 400; } } diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 21c16d80b..955d106b5 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -986,8 +986,9 @@ export default { } } } -:deep(span.label) { - font-weight: 600; +:deep(span.label), +:deep(span) { + font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; } :deep(div.service-questions span) { color: $black; diff --git a/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue b/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue index 8bb7e921a..959c7345e 100644 --- a/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue +++ b/src/layouts/quote/service-package-question/service-package-radio/service-package-radio.vue @@ -294,12 +294,11 @@ export default { .pricing-info .insurance-pricing span { font-size: 0.875rem; - font-family: Urbanist, Arial, Helvetica, sans-serif; - font-weight: 500; + font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; } p { - font-family: Urbanist, Arial, Helvetica, sans-serif; + font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; display: flex; justify-content: space-between; align-items: center; @@ -312,7 +311,6 @@ export default { color: $green; font-size: 0.875rem; font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; - font-weight: 600; @include media-breakpoint-up(md) { position: absolute; bottom: 0.75rem; diff --git a/src/layouts/schedule/shop-location/shop-location.vue b/src/layouts/schedule/shop-location/shop-location.vue index 33ec097e8..f22436eda 100644 --- a/src/layouts/schedule/shop-location/shop-location.vue +++ b/src/layouts/schedule/shop-location/shop-location.vue @@ -109,10 +109,10 @@ export default { border-radius: $border-radius-pill; a { + font-family: UrbanistSemibold; display: inline-block; text-decoration: none; color: $white; - font-weight: $font-weight-600; line-height: 1.5rem; padding: 0.75rem 3rem; } diff --git a/src/ux-components/button-main/button-main.vue b/src/ux-components/button-main/button-main.vue index f5db583bb..87eb28841 100644 --- a/src/ux-components/button-main/button-main.vue +++ b/src/ux-components/button-main/button-main.vue @@ -63,13 +63,13 @@ export default { From bd739cae917939a06fce79090ab69e805094e04e Mon Sep 17 00:00:00 2001 From: CarlNation Date: Tue, 9 Dec 2025 09:32:13 -0500 Subject: [PATCH 036/233] prettier --- src/styles/ux-variables.scss | 4 ++-- src/ux-components/text-link/text-link.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss index 0c275ad51..770d95b42 100644 --- a/src/styles/ux-variables.scss +++ b/src/styles/ux-variables.scss @@ -123,8 +123,8 @@ $body-color: $gray-600; //Fonts $font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif; -$font-family-monospace: - UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; // stylelint-enable value-keyword-case $font-family-base: $font-family-sans-serif; $font-family-code: $font-family-monospace; diff --git a/src/ux-components/text-link/text-link.vue b/src/ux-components/text-link/text-link.vue index d9d9df115..92714e740 100644 --- a/src/ux-components/text-link/text-link.vue +++ b/src/ux-components/text-link/text-link.vue @@ -89,7 +89,7 @@ a { &.new-window-link { margin-bottom: 1.5rem; } - &.text-link { + &.text-link { white-space: nowrap; } } From 3bd6d314b1d5b8860a212acc3769fe17ca15e636 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Tue, 9 Dec 2025 11:41:54 -0500 Subject: [PATCH 037/233] CASH-1687: Update vehicle keys messaging --- src/layouts/mobile-details/mobile-details.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/layouts/mobile-details/mobile-details.vue b/src/layouts/mobile-details/mobile-details.vue index 1c1a48249..b6978a7ff 100644 --- a/src/layouts/mobile-details/mobile-details.vue +++ b/src/layouts/mobile-details/mobile-details.vue @@ -25,6 +25,11 @@ cmsWidgetName="PleaseProvideAddressWidget" class="provide-address-header" /> + Date: Wed, 10 Dec 2025 15:46:41 -0500 Subject: [PATCH 038/233] CASH-1790 remove unused props --- src/layouts/schedule/schedule.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 00ad09b46..898be4a1b 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -133,8 +133,6 @@ :isOvernightDropoff="isOvernightDropoff" />
Date: Wed, 10 Dec 2025 17:15:11 -0500 Subject: [PATCH 039/233] CASH-1790: refactor to remove another watch on time-slot-question --- src/layouts/schedule/schedule.vue | 13 ++++++------- .../time-slot-question/time-slot-question.vue | 8 -------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 898be4a1b..d78c37115 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -636,12 +636,12 @@ export default { showTimeSlotQuestion() { if ( this.selectedDate && - this.selectedDate.includes("mobile") && - !this.isMobileSelected + this.appointmentTypeFromAppointmentTypeQuestion && + this.timeSlotsForSelectedDate ) { - this.setSelectedDateToFirstAvailable(); + return true; } - return this.selectedDate && this.appointmentTypeFromAppointmentTypeQuestion; + return false; }, isMobileStaticRecalibrationApplicable() { return ( @@ -1572,12 +1572,12 @@ export default { }, handleDateSelected(date) { const previousDate = this.selectedDate; - + // check if date actually changed if (previousDate !== date.dateString) { this.handleDateChanged({ newDate: date.dateString, - oldDate: previousDate + oldDate: previousDate, }); } @@ -1877,7 +1877,6 @@ export default { textBlock, timeSlotQuestion, mobileFirstModal, - alert, serviceZipModalQuestion, appointmentTypeQuestion, diff --git a/src/layouts/schedule/time-slot-question/time-slot-question.vue b/src/layouts/schedule/time-slot-question/time-slot-question.vue index d2e43054c..3690370e0 100644 --- a/src/layouts/schedule/time-slot-question/time-slot-question.vue +++ b/src/layouts/schedule/time-slot-question/time-slot-question.vue @@ -190,14 +190,6 @@ export default { selectedRouteCodeData(newValue) { this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(newValue); }, - timeSlotsForSelectedDate() { - if (!this.isDatePickerDoneInitializing) { - return; - } // needed otherwise it nulls these too early and prevents a previously selected time slot from auto-selecting - this.selectedAnswerForDropOffOrInshop = null; - this.selectedAnswerForTimeSlots = null; - this.autoSelectTimeSlotIfOnlyOneIsAvailable(); - }, }, computed: { supplementalInformationBlock() { From 21ca0f2d27cd984411826cbb97e7b4fa5b301823 Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Thu, 11 Dec 2025 09:52:57 -0500 Subject: [PATCH 040/233] Initial Try to pass prechat details from scarlett --- src/helpers/webchat-helper.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/webchat-helper.js b/src/helpers/webchat-helper.js index e5254d08d..45b0ff8dd 100644 --- a/src/helpers/webchat-helper.js +++ b/src/helpers/webchat-helper.js @@ -11,6 +11,12 @@ export const webchatHelper = () => { if (webchatGlobalNonpersistedState.shouldLaunchSierra) { window.dispatchEvent(new CustomEvent("launch-sierra-webchat")); // launches the sierra chat } else { + window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({ + ScarlettAISummary: "sScarletAISummary", + FirstName: "sFirstName", + LastName: "sLastName", + Email: "sEmail", + }); window.embeddedservice_bootstrap.utilAPI.launchChat(); // launches the salesForce prechat form } } From cc45d3b521bdec35d999d625e22ff684fd68addb Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Thu, 11 Dec 2025 11:48:33 -0500 Subject: [PATCH 041/233] CASH-1790: removing date-picker watch bc it was causing issues and redundant --- src/digital-components/date-picker/date-picker.vue | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index db1eba7ff..9d620ec77 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -846,12 +846,6 @@ export default { }, }, watch: { - isLoading(newValue) { - // if done loading dates, then set to first available date - if (newValue === false && this.firstAvailableSelectableDate) { - this.selectedDate = this.firstAvailableSelectableDate; - } - }, modelValue(newValue) { this.resetField({ value: newValue, From 290ea914e2d8fed5d26e2dd2dbc85915b2b6c53d Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Thu, 11 Dec 2025 12:38:19 -0500 Subject: [PATCH 042/233] CASH-1790: refactoring and restoring watch on time-slot-question --- src/layouts/schedule/schedule.vue | 13 ++----------- .../time-slot-question/time-slot-question.vue | 5 +++++ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index d78c37115..a58d7182c 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1179,8 +1179,6 @@ export default { async initializeDatePicker() { this.isShowMobileFirstAppt && this.showLoadingModal(); - this.selectedDate = null; - const includeMobileTimeSlots = this.isServiceableMobile; const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff; const datePickerInitialData = await this.$refs.datePicker.loadInitialData({ @@ -1575,10 +1573,8 @@ export default { // check if date actually changed if (previousDate !== date.dateString) { - this.handleDateChanged({ - newDate: date.dateString, - oldDate: previousDate, - }); + this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); + this.updateFooterButtonText(this.selectedTimeSlotInfo); } // TODO: REMOVE AS PART OF CASH-1634 @@ -1589,11 +1585,6 @@ export default { // this.includePricingByDayUpcharge = false; // } }, - handleDateChanged(newDate) { - // Clear time slot selection when date changes - this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); - this.updateFooterButtonText(this.selectedTimeSlotInfo); - }, getEmptyTimeSlot() { return { timeSlot: { diff --git a/src/layouts/schedule/time-slot-question/time-slot-question.vue b/src/layouts/schedule/time-slot-question/time-slot-question.vue index 3690370e0..647d7c48b 100644 --- a/src/layouts/schedule/time-slot-question/time-slot-question.vue +++ b/src/layouts/schedule/time-slot-question/time-slot-question.vue @@ -190,6 +190,11 @@ export default { selectedRouteCodeData(newValue) { this.updateDropoffAndTimeSlotAnswersFromSelectedRouteCodeData(newValue); }, + timeSlotsForSelectedDate() { + this.selectedAnswerForDropOffOrInshop = null; + this.selectedAnswerForTimeSlots = null; + this.autoSelectTimeSlotIfOnlyOneIsAvailable(); + }, }, computed: { supplementalInformationBlock() { From 165157ae4728b53f184a9f502997673404576762 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Thu, 11 Dec 2025 12:45:34 -0500 Subject: [PATCH 043/233] CASH-1790: remove no longer needed boolean check --- src/layouts/schedule/schedule.vue | 4 ---- .../schedule/time-slot-question/time-slot-question.vue | 1 - 2 files changed, 5 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index a58d7182c..d2a4e8038 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -165,7 +165,6 @@ :timeSlotsForSelectedDate="timeSlotsForSelectedDate" :isSameDay="isSameDay" :selectedRouteCodeData="selectedRouteCodeData" - :isDatePickerDoneInitializing="isDatePickerDoneInitializing" @waitListRequested="handleWaitListRequested" /> Date: Thu, 11 Dec 2025 12:54:37 -0500 Subject: [PATCH 044/233] CASH-1790: copilot suggestion --- src/layouts/schedule/schedule.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index d2a4e8038..bee86a537 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1594,7 +1594,7 @@ export default { isPremiumAppointment: null, }; }, - updateTimeSlot(timeSlotObj) { + updateTimeSlot(timeSlotObj) { if (!timeSlotObj?.routeCode) { this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); From b3e243585a2f988d9366147adbfeb37250e5e6ed Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Mon, 15 Dec 2025 13:49:24 -0500 Subject: [PATCH 045/233] CASH-1953 Attempt to correct possible provider number bug for shop-time-slots --- src/digital-components/date-picker/date-picker.vue | 2 +- src/layouts/schedule/schedule.vue | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 9d620ec77..42e24adf2 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -466,7 +466,7 @@ export default { const response = config.getSelectableDatesCallback({ startDateString: initialViewStartDate, endDateString: initialViewEndDate, - providerNumber: config.providerNumber, + inshopProviderNumber: config.inshopProviderNumber, // TODO FOR CASH-1953 zipCode: config.zipCode, includeMobileTimeSlots: config.includeMobileTimeSlots, includeInshopTimeSlots: config.includeInshopTimeSlots, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index bee86a537..a99c0b36a 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -285,6 +285,7 @@ const getScheduleApiResponse = async ({ startDateString, endDateString, providerNumber, + inshopProviderNumber, // TODO FOR CASH-1953 zipCode, includeMobileTimeSlots = true, includeInshopTimeSlots = true, @@ -327,7 +328,7 @@ const getScheduleApiResponse = async ({ startDate: apiStartDate, endDate: apiEndDate, shopAppointmentType: "InshopOrDropoff", - providerNumber: providerNumber, + providerNumber: inshopProviderNumber, // TODO FOR CASH-1953 }, }; if (apiStartDate < apiEndDate) { @@ -631,6 +632,9 @@ export default { return this.isGlassServiceableMobile; } }, + closestInshopProviderNumber() { // TODO FOR CASH-1953 + return this.shopProviderData?.shopProviders[0]?.providerNumber; + }, showTimeSlotQuestion() { if ( this.selectedDate && @@ -1158,6 +1162,7 @@ export default { startDateString: startDate, endDateString: endDate, providerNumber: this.selectedProvider?.providerNumber, + inshopProviderNumber: this.isMobileSelected ? this.closestInshopProviderNumber : this.selectedProvider?.providerNumber, // TODO FOR CASH-1953 zipCode: this.zipCode, includeMobileTimeSlots: this.isServiceableMobile, includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff, @@ -1186,6 +1191,7 @@ export default { getSelectableDatesCallback: getScheduleApiResponse, preSelectedDate: this.preSelectedDate, providerNumber: this.selectedProvider?.providerNumber, + inshopProviderNumber: this.isMobileSelected ? this.closestInshopProviderNumber : this.selectedProvider?.providerNumber, // TODO FOR CASH-1953 zipCode: this.zipCode, includeMobileTimeSlots: includeMobileTimeSlots, includeInshopTimeSlots: includeInshopTimeSlots, From 779cf9415a228df5fa2246bd8bb42bf66736e768 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Mon, 15 Dec 2025 17:47:38 -0500 Subject: [PATCH 046/233] CASH-1790 another bug fix for no mobile available --- src/layouts/schedule/schedule.vue | 47 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index bee86a537..ca66fed8c 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -24,6 +24,7 @@ { vm.showMobileFirstModal(); + vm.setSelectedDateToFirstAvailable(); }); }); }, @@ -827,13 +829,13 @@ export default { }, estimatedServiceMinutesMinimum() { return this.isMobileSelected - ? this.selectableDatesMobile.estimatedServiceMinutesMinimum - : this.selectableDatesInshop.estimatedServiceMinutesMinimum; + ? this.selectableDatesMobile?.estimatedServiceMinutesMinimum + : this.selectableDatesInshop?.estimatedServiceMinutesMinimum; }, estimatedServiceMinutesMaximum() { return this.isMobileSelected - ? this.selectableDatesMobile.estimatedServiceMinutesMaximum - : this.selectableDatesInshop.estimatedServiceMinutesMaximum; + ? this.selectableDatesMobile?.estimatedServiceMinutesMaximum + : this.selectableDatesInshop?.estimatedServiceMinutesMaximum; }, timeSlotsForSelectedDate() { if (!this.selectedDate) return null; @@ -841,11 +843,11 @@ export default { if (this.isMobileSelected) { const selectedDateString = selectedDate.replace("-mobile", ""); - return this.selectableDatesMobile.days?.find( + return this.selectableDatesMobile?.days?.find( (selectableDate) => selectableDate.date === selectedDateString ); } else { - return this.selectableDatesInshop.days?.find( + return this.selectableDatesInshop?.days?.find( (selectableDate) => selectableDate.date === this.selectedDate ); } @@ -863,7 +865,7 @@ export default { ); }, isMobileFirstModalOpen() { - return this.selectableDatesMobile.days + return this.selectableDatesMobile?.days ? this.$refs.mobileFirstModal?.getIsModalOpen() : false; }, @@ -1015,7 +1017,9 @@ export default { return store.getters.order.serviceLocation.isVehicleProtected; }, getAppointmentTypeFromStore() { - return store.getters.order.serviceLocation.appointmentType; + const appointmentTypeFromStore = store.getters.order.serviceLocation.appointmentType; + this.handleAppointmentTypeChange(appointmentTypeFromStore); + return appointmentTypeFromStore; }, getSelectedProviderFromStore() { return store.getters.order.serviceLocation.provider; @@ -1036,6 +1040,7 @@ export default { this.appointmentType = null; this.selectedProvider = null; this.appointmentTypeFromAppointmentTypeQuestion = null; + this.handleAppointmentTypeChange(); this.preSelectedDate = null; }, setServiceabilityDetails(serviceabilityDetails) { @@ -1288,7 +1293,7 @@ export default { } } */ - this.$refs.navbar.updateButtonText(navbarButtonText); + this.$refs.navbar?.updateButtonText(navbarButtonText); }, convertSelectedDateToShortMonthAndDay(selectedDate) { // This conversion ensures we don't get get GMT induced date changes @@ -1463,9 +1468,12 @@ export default { this.showLoadingModal(); }, setDisplayWaitList() { - const dateString = this.isMobileSelected - ? this.selectableDatesMobile?.days[0]?.date - : this.selectableDatesInshop?.days[0]?.date; + let dateString; + if (this.isMobileSelected && this.selectableDatesMobile?.days?.length) { + dateString = this.selectableDatesMobile.days[0].date; + } else if (this.selectableDatesInshop?.days?.length) { + dateString = this.selectableDatesInshop.days[0].date; + } if ( experimentMixin.methods.hasSettingEqualTo( experimentSettings.DISPLAY_WAITLIST, @@ -1634,9 +1642,9 @@ export default { }, getFirstAvailableDate() { let dateToSelect; - if (this.isMobileSelected) { + if (this.isMobileSelected && this.selectableDatesMobile?.days?.length) { dateToSelect = returnFirstDate(this.selectableDatesMobile); - } else { + } else if (this.selectableDatesInshop?.days?.length) { dateToSelect = returnFirstDate(this.selectableDatesInshop); } if (!dateToSelect) return null; @@ -1846,13 +1854,6 @@ export default { this.forwardButtonAction(); }, }, - watch: { - appointmentTypeFromAppointmentTypeQuestion: { - handler(newValue, oldValue) { - this.handleAppointmentTypeChange(newValue); - }, - }, - }, components: { funnelHeader, navbar, From 8d98a53f0c1e04ba502949bdebe868d2fa839024 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Mon, 15 Dec 2025 17:48:46 -0500 Subject: [PATCH 047/233] CASH-1790 prettier change --- src/layouts/schedule/schedule.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ca66fed8c..7ae428e59 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1473,7 +1473,7 @@ export default { dateString = this.selectableDatesMobile.days[0].date; } else if (this.selectableDatesInshop?.days?.length) { dateString = this.selectableDatesInshop.days[0].date; - } + } if ( experimentMixin.methods.hasSettingEqualTo( experimentSettings.DISPLAY_WAITLIST, From a212894a26d48d9fcfba15bf819363894e8851b0 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 10 Dec 2025 16:12:58 -0500 Subject: [PATCH 048/233] Adyen POC 0.0 --- src/constants/endpoints.js | 8 + src/layouts/payment-adyen/payment-adyen.vue | 319 ++++++++++++++++++ .../payment-pia-return/payment-pia-return.vue | 3 + src/router/constants/routes.js | 4 + src/router/constants/routing-table.js | 15 +- src/router/methods/routes.js | 1 + 6 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 src/layouts/payment-adyen/payment-adyen.vue diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index c1771b3f5..214431e5c 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -216,6 +216,14 @@ const endpoints = { url: "/order/api/v1/order/add-donation-part", method: "POST", }, + InitializeAdyenPayment: { + url: "/order/api/v1/order/payment/session/initialize-session", + method: "POST", + }, + GetAdyenSessionResult: { + url: "/order/api/v1/order/payment/session/session-result", + method: "POST", + }, }; export { endpoints }; diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue new file mode 100644 index 000000000..d105382d6 --- /dev/null +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -0,0 +1,319 @@ + + diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index b1ec2b120..c905f3d78 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -156,6 +156,9 @@ export default { lastFour: lastFour, }; + console.log(`CCToken =`); + console.log(ccToken); + baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); baseMixin.methods.dispatchStoreAction( storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index 98d6c503a..4b51ff857 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -89,6 +89,10 @@ export const routeData = { name: "payment-pia-return", path: "/payment-pia-return", }, + PAYMENT_ADYEN: { + name: "payment-adyen", + path: "/payment-adyen", + }, CONFIRMATION: { name: "confirmation", path: "/confirmation", diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index 02a0d9384..cd41e8fcf 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -535,7 +535,7 @@ const routingTable = function () { }, { scenario: navigationScenarios.CLICKED_PAY_NOW, - destinationPageData: routeData.PAYMENT, + destinationPageData: routeData.PAYMENT_ADYEN, }, { scenario: navigationScenarios.CLICKED_INSURANCE, @@ -563,6 +563,19 @@ const routingTable = function () { }, ], }, + { + pageName: routeData.PAYMENT_ADYEN.name, + maps: [ + { + scenario: navigationScenarios.CLICKED_BACK, + destinationPageData: routeData.PAYMENT_METHOD, + }, + { + scenario: navigationScenarios.CLICKED_FORWARD, + destinationPageData: routeData.CONFIRMATION, + }, + ], + }, { pageName: routeData.PAYMENT_PIA_RETURN.name, maps: [ diff --git a/src/router/methods/routes.js b/src/router/methods/routes.js index 97af04068..cf0392ccf 100644 --- a/src/router/methods/routes.js +++ b/src/router/methods/routes.js @@ -36,6 +36,7 @@ export const routes = [ createRoute(routeData.PAYMENT_METHOD, paymentMethodBeforeEnter), createRoute(routeData.PAYMENT, paymentBeforeEnter), createRoute(routeData.PAYMENT_PIA_RETURN), + createRoute(routeData.PAYMENT_ADYEN), createRoute(routeData.CONFIRMATION), createRoute(routeData.RETURN_USER), createRoute(routeData.MOBILE_DETAILS), From 3b1b000a1387df7bf8f64a17f2a73edc1d624ce2 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 11 Dec 2025 11:58:11 -0500 Subject: [PATCH 049/233] In progress --- src/constants/endpoints.js | 4 +- src/layouts/payment-adyen/payment-adyen.vue | 77 ++++++++++++++++----- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 214431e5c..e40fce79a 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -217,11 +217,11 @@ const endpoints = { method: "POST", }, InitializeAdyenPayment: { - url: "/order/api/v1/order/payment/session/initialize-session", + url: "/payment/api/v1/payment/payment/session/initialize-session", method: "POST", }, GetAdyenSessionResult: { - url: "/order/api/v1/order/payment/session/session-result", + url: "/payment/api/v1/payment/payment/session/session-result", method: "POST", }, }; diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index d105382d6..86c8a67d2 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -42,6 +42,10 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import globalMethods from "@/global-methods"; import { endpoints } from "../../constants/endpoints"; import { AppointmentTypeStrings } from "../../constants/schedule-constants"; +import baseMixin from "@/mixins/base-mixin.js"; +import { storeActions } from "@/constants/store-actions"; +import { getAmountDue } from "@/helpers/pricing-helper.js"; +import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; export default { name: "payment-adyen", @@ -90,6 +94,9 @@ export default { }, async initializeAdyen() { + console.log(`Price = ${this.amountDue}`); + console.log(`Adyen Price = ${this.adyenPriceTotal}`); + const requestBody = this.adyenInitRequestInfo; console.log(`Calling with:`); @@ -153,23 +160,23 @@ export default { window.checkout = checkOut; // eslint-disable-next-line const dropin = new AdyenWeb.Dropin(checkOut, { - paymentMethodsConfiguration: { - card: { - showBrandIcon: true, // when false not showing the brand logo - hasHolderName: true, // show holder name - holderNameRequired: true, // make holder name mandatory - billingAddressRequired: true, - billingAddressAllowedCountries: ["US"], - // configure placeholders - placeholders: { - cardNumber: "1234 5678 9012 3456", - expiryDate: "MM/YY", - securityCodeThreeDigits: "123", - securityCodeFourDigits: "1234", - holderName: "J. Smith", - }, - }, - }, + // paymentMethodsConfiguration: { + // card: { + // showBrandIcon: true, // when false not showing the brand logo + // hasHolderName: true, // show holder name + // holderNameRequired: true, // make holder name mandatory + // billingAddressRequired: true, + // billingAddressAllowedCountries: ["US"], + // // configure placeholders + // placeholders: { + // cardNumber: "1234 5678 9012 3456", + // expiryDate: "MM/YY", + // securityCodeThreeDigits: "123", + // securityCodeFourDigits: "1234", + // holderName: "J. Smith", + // }, + // }, + // }, }).mount("#adyen-container"); }, @@ -218,9 +225,39 @@ export default { console.log(`CC Token generated =`); console.log(ccToken); + + await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); + await baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, + this.amountDue, + false + ); + + await this.saveAndSubmitWorkOrder(); }, handleFailedPayment(result) {}, handleError(error) {}, + + async saveAndSubmitWorkOrder() { + // Work order submission after successful payment. + await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); + try { + await submitWorkOrder({ + pageNameToLog: "payment-adyen", + submitAfterSave: true, + }); + } catch (error) { + console.log(`error: response from submit work order: ${error.message}`); + + // navigate to error page + + //this.$refs.loadingModal.isModalVisible = false; + } + + //this.$refs.loadingModal.isModalVisible = false; + // clear order + // navigate forward + }, }, computed: { @@ -304,8 +341,12 @@ export default { return "FMG-2.0"; }, + amountDue() { + return getAmountDue(this.$store.getters.order.lineItems); + }, + adyenPriceTotal() { - return 10000; + return this.amountDue * 100; }, }, From 75417c4a34efc311020622ca98db225b25829294 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Fri, 12 Dec 2025 14:12:20 -0500 Subject: [PATCH 050/233] Attempts to resolve PayPal --- src/layouts/payment-adyen/payment-adyen.vue | 56 +++++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 86c8a67d2..f7e85b019 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -79,6 +79,10 @@ export default { methods: { async backButtonAction() { // Stub, for navigating back via nav-bar. + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_BACK, + this.pageName + ); }, async forwardButtonAction() { @@ -130,14 +134,15 @@ export default { value: this.adyenPriceTotal, currency: "USD", }, - locale: "en-US", + locale: "en_US", countryCode: "US", showPayButton: true, translations: { - "en-US": { + "en_US": { "creditCard.securityCode.label": "CVV/CVC", }, }, + onPaymentCompleted: (result, component) => { console.log(`Payment completed from Adyen.`); this.handleCompletedPayment(result); @@ -160,23 +165,27 @@ export default { window.checkout = checkOut; // eslint-disable-next-line const dropin = new AdyenWeb.Dropin(checkOut, { - // paymentMethodsConfiguration: { - // card: { - // showBrandIcon: true, // when false not showing the brand logo - // hasHolderName: true, // show holder name - // holderNameRequired: true, // make holder name mandatory - // billingAddressRequired: true, - // billingAddressAllowedCountries: ["US"], - // // configure placeholders - // placeholders: { - // cardNumber: "1234 5678 9012 3456", - // expiryDate: "MM/YY", - // securityCodeThreeDigits: "123", - // securityCodeFourDigits: "1234", - // holderName: "J. Smith", - // }, - // }, - // }, + paymentMethodsConfiguration: { + ideal: { + showImage: true, + }, + paypal: { + amount: { + value: this.adyenPriceTotal, + currency: "USD", + }, + environment: "test", // Change this to "live" when you're ready to accept live PayPal payments + countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production. + blockPayPalVenmoButton: false, + //blockPayPalPayLaterButton: true + }, + card: { + hasHolderName: true, + holderNameRequired: true, + billingAddressRequired: true, + name: "Credit or debit card", + }, + }, }).mount("#adyen-container"); }, @@ -235,7 +244,10 @@ export default { await this.saveAndSubmitWorkOrder(); }, - handleFailedPayment(result) {}, + async handleFailedPayment(result) { + console.log(`Result =`); + console.log(result); + }, handleError(error) {}, async saveAndSubmitWorkOrder() { @@ -273,12 +285,12 @@ export default { street: this.splitStreetAddress.street, zipCode: this.locationInfo.zipCode, stateOrProvince: this.locationInfo.state, - returnUrl: "localhost:8080/fmg/confirmation", + returnUrl: "http://localhost:8080/fmg/confirmation", email: this.$store.getters.order.customer.emailAddress, IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, + idempotencyKey: crypto.randomUUID(), // `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, }; }, From a8453c76ae50c27edd04effb3da57a26a02d98b8 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Mon, 15 Dec 2025 10:17:57 -0500 Subject: [PATCH 051/233] Misc --- src/layouts/payment-adyen/payment-adyen.vue | 35 ++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index f7e85b019..e38578e54 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -46,6 +46,7 @@ import baseMixin from "@/mixins/base-mixin.js"; import { storeActions } from "@/constants/store-actions"; import { getAmountDue } from "@/helpers/pricing-helper.js"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; +import { paymentMethods } from "@/constants/payment-method-constants"; export default { name: "payment-adyen", @@ -138,7 +139,7 @@ export default { countryCode: "US", showPayButton: true, translations: { - "en_US": { + en_US: { "creditCard.securityCode.label": "CVV/CVC", }, }, @@ -235,6 +236,11 @@ export default { console.log(`CC Token generated =`); console.log(ccToken); + // Save payment type. + // For now, CC. + // Need to determine payment type from Adyen response + await this.dispatchStoreAction(storeActions.SAVE_PAYMENT_METHOD_CHOICE, paymentMethods.CREDIT_CARD, false); + await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); await baseMixin.methods.dispatchStoreAction( storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, @@ -247,6 +253,33 @@ export default { async handleFailedPayment(result) { console.log(`Result =`); console.log(result); + + // Fetch session info + const paymentSessionRequest = { + zipCodeCtu: this.locationInfo.zipCodeCtu, + workOrderNumber: this.workOrderNumberLastSixDigits, + sourceSystem: this.sourceSystem, + sessionId: this.sessionId, + sessionResult: result?.sessionResult, + }; + + console.log(`Get session payload =`); + console.log(paymentSessionRequest); + + try { + const response = await globalMethods.callHttpClient({ + method: endpoints.GetAdyenSessionResult.method, + endpoint: endpoints.GetAdyenSessionResult.url, + payload: paymentSessionRequest, + logApiCall: true, + pageNameToLog: "payment-adyen", + }); + + console.log(response); + } catch (error) { + console.log(`Error getting session info after failure.`); + } + }, handleError(error) {}, From 72a3d8588eb64e8136937b1c74d0b38e8fef19e0 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Tue, 16 Dec 2025 09:45:44 -0500 Subject: [PATCH 052/233] Afterpay return page --- package-lock.json | 41 +++++++ package.json | 1 + src/helpers/adyen-helper.js | 116 ++++++++++++++++++ src/layouts/payment-adyen/payment-adyen.vue | 67 +++++----- src/router/constants/routes.js | 5 + .../methods/route-logic/adyen-return.js | 73 +++++++++++ src/router/methods/routes.js | 2 + 7 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 src/helpers/adyen-helper.js create mode 100644 src/router/methods/route-logic/adyen-return.js diff --git a/package-lock.json b/package-lock.json index 7d44b181a..2e45411d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "Safelite", "version": "0.1.0", "dependencies": { + "@adyen/adyen-web": "^6.27.0", "@iframe-resizer/child": "^5.3.3", "axios": "^0.30.2", "bootstrap": "^5.3.3", @@ -59,6 +60,18 @@ "node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22" } }, + "node_modules/@adyen/adyen-web": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.27.0.tgz", + "integrity": "sha512-6E6hqEkMkoY/FevjO53zhgeCWMt+i/PDc11MX6eVM7Fvd+Sdgc9cIwTBz+NKIXs4ZerG6n2gd+ZIZEywqF/aBQ==", + "license": "MIT", + "dependencies": { + "@types/applepayjs": "14.0.9", + "@types/googlepay": "0.7.8", + "classnames": "2.5.1", + "preact": "10.22.1" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3173,6 +3186,12 @@ "node": ">=10.13.0" } }, + "node_modules/@types/applepayjs": { + "version": "14.0.9", + "resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.9.tgz", + "integrity": "sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3328,6 +3347,12 @@ "@types/send": "*" } }, + "node_modules/@types/googlepay": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.8.tgz", + "integrity": "sha512-EHl7G7jIeit/Px/fi/Du/19SZDvqrRhy0DjJX40Vzs/97m3sro9mzVyAYNC0pfzpWGDY+zPjtTLEfhV+OYWUcQ==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -5789,6 +5814,12 @@ "dev": true, "license": "MIT" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -13681,6 +13712,16 @@ "dev": true, "license": "MIT" }, + "node_modules/preact": { + "version": "10.22.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz", + "integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 7d06ad112..c55259920 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@adyen/adyen-web": "^6.27.0", "@iframe-resizer/child": "^5.3.3", "axios": "^0.30.2", "bootstrap": "^5.3.3", diff --git a/src/helpers/adyen-helper.js b/src/helpers/adyen-helper.js new file mode 100644 index 000000000..93bd53bc8 --- /dev/null +++ b/src/helpers/adyen-helper.js @@ -0,0 +1,116 @@ +import { AdyenCheckout } from "@adyen/adyen-web/auto"; +import store from "@/store"; +import globalMethods from "@/global-methods"; +import { endpoints } from "@/constants/endpoints"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; + +export async function getNewSession(sessionConfig) {} + +export async function getSessionInfo(sessionId, encryptedSessionResult, pageNameToLog = "") { + const order = store.getters.order; + const location = getLocationInfo(order); + const workOrder = getWorkOrderLastSixDigits(order); + const system = getSourceSystem(); + + const request = { + sessionId: sessionId, + sessionResult: encryptedSessionResult, + zipCodeCtu: location.zipCodeCtu, + workOrderNumber: workOrder, + sourceSystem: system, + }; + + const response = await globalMethods.callHttpClient({ + method: endpoints.GetAdyenSessionResult.method, + endpoint: endpoints.GetAdyenSessionResult.url, + payload: request, + logApiCall: true, + pageNameToLog: pageNameToLog, + }); + + return response?.data; +} + +export async function createAdyenCheckout({ + session: { id, sessionData }, + handlers: { onPaymentCompleted, onPaymentFailed, onError }, + options: { showPayButton = true, amount }, +}) { + const config = { + session: { + id: id, + sessionData: sessionData, + }, + clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", + environment: "test", + locale: "en_US", + countryCode: "US", + showPayButton: showPayButton, + translations: { + en_US: { + "creditCard.securityCode.label": "CVV/CVC", + }, + }, + onPaymentCompleted: onPaymentCompleted, + onPaymentFailed: onPaymentFailed, + onError: onError, + }; + + if (amount !== undefined && amount !== null) { + config.amount = { + value: amount, + currency: "USD", + }; + } + + console.log(`Creating checkout with configuration:`); + console.log(config); + + return await AdyenCheckout(config); +} + +function getLocationInfo(order) { + const serviceLocation = order.serviceLocation; + const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; + if (isMobile) { + return { + address: serviceLocation.address, + address2: serviceLocation.address2, + city: serviceLocation.city, + state: serviceLocation.state, + zipCode: serviceLocation.zipCode, + zipCodeCtu: serviceLocation.zipCodeCtu, + }; + } else { + const providerInfo = serviceLocation.provider.address; + return { + address: providerInfo.streetAddress, + address2: "", + city: providerInfo.city, + state: providerInfo.state, + zipCode: providerInfo.zipCode, + zipCodeCtu: providerInfo.zipCodeCtu, + }; + } +} + +function getWorkOrderLastSixDigits(order) { + const workOrderNumber = order.workOrderNumber ?? ""; + const tokens = workOrderNumber.split("-"); + const lastSegment = tokens[tokens.length - 1] ?? ""; + const numDigits = lastSegment.length; + const numZeroes = 6 - numDigits; + + let result = ""; + for (let i = 0; i < numZeroes; i++) { + result += "0"; + } + + result += lastSegment; + + return result; +} + +function getSourceSystem() { + return "FMG-2.0"; +} diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index e38578e54..a2d7651a2 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -47,6 +47,8 @@ import { storeActions } from "@/constants/store-actions"; import { getAmountDue } from "@/helpers/pricing-helper.js"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; import { paymentMethods } from "@/constants/payment-method-constants"; +import { createAdyenCheckout } from "@/helpers/adyen-helper"; +import { Dropin } from "@adyen/adyen-web/auto"; export default { name: "payment-adyen", @@ -127,45 +129,31 @@ export default { this.sessionId = responseJson.sessionId; - const configuration = { + const checkout = await createAdyenCheckout({ session: session, - clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", - environment: "test", - amount: { - value: this.adyenPriceTotal, - currency: "USD", - }, - locale: "en_US", - countryCode: "US", - showPayButton: true, - translations: { - en_US: { - "creditCard.securityCode.label": "CVV/CVC", + handlers: { + onPaymentCompleted: (result, component) => { + console.log(`Payment completed from Adyen.`); + this.handleCompletedPayment(result); + }, + onPaymentFailed: (result, component) => { + console.log(`Payment failed from Adyen`); + this.handleFailedPayment(result); + }, + onError: (error, component) => { + console.log(`Error from Adyen`); + this.handleError(error); }, }, - - onPaymentCompleted: (result, component) => { - console.log(`Payment completed from Adyen.`); - this.handleCompletedPayment(result); + options: { + amount: this.adyenPriceTotal, }, - onPaymentFailed: (result, component) => { - console.log(`Payment failed from Adyen`); - this.handleFailedPayment(result); - }, - onError: (error, component) => { - console.log(`Error from Adyen`); - this.handleError(error); - }, - }; + }); - console.log(`Configuration =`); - console.log(configuration); + console.log(checkout); - // eslint-disable-next-line - const checkOut = await AdyenWeb.AdyenCheckout(configuration); - window.checkout = checkOut; - // eslint-disable-next-line - const dropin = new AdyenWeb.Dropin(checkOut, { + window.checkout = checkout; + const dropin = new Dropin(checkout, { paymentMethodsConfiguration: { ideal: { showImage: true, @@ -239,7 +227,11 @@ export default { // Save payment type. // For now, CC. // Need to determine payment type from Adyen response - await this.dispatchStoreAction(storeActions.SAVE_PAYMENT_METHOD_CHOICE, paymentMethods.CREDIT_CARD, false); + await this.dispatchStoreAction( + storeActions.SAVE_PAYMENT_METHOD_CHOICE, + paymentMethods.CREDIT_CARD, + false + ); await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); await baseMixin.methods.dispatchStoreAction( @@ -279,7 +271,6 @@ export default { } catch (error) { console.log(`Error getting session info after failure.`); } - }, handleError(error) {}, @@ -312,18 +303,18 @@ export default { referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, workOrderNumber: this.workOrderNumberLastSixDigits, zipCodeCtu: this.locationInfo.zipCodeCtu, - amount: this.adyenPriceTotal, // TODO + amount: this.adyenPriceTotal, city: this.locationInfo.city, houseNumberOrName: this.splitStreetAddress.number, street: this.splitStreetAddress.street, zipCode: this.locationInfo.zipCode, stateOrProvince: this.locationInfo.state, - returnUrl: "http://localhost:8080/fmg/confirmation", + returnUrl: "http://localhost:8080/fmg/virtual/payment/return", email: this.$store.getters.order.customer.emailAddress, IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: crypto.randomUUID(), // `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, + idempotencyKey: `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, }; }, diff --git a/src/router/constants/routes.js b/src/router/constants/routes.js index 4b51ff857..5090df2b3 100644 --- a/src/router/constants/routes.js +++ b/src/router/constants/routes.js @@ -93,6 +93,11 @@ export const routeData = { name: "payment-adyen", path: "/payment-adyen", }, + PAYMENT_ADYEN_RETURN: { + name: "adyen-return", + path: "/virtual/payment/return", + virtual: true, + }, CONFIRMATION: { name: "confirmation", path: "/confirmation", diff --git a/src/router/methods/route-logic/adyen-return.js b/src/router/methods/route-logic/adyen-return.js new file mode 100644 index 000000000..9bf1940ba --- /dev/null +++ b/src/router/methods/route-logic/adyen-return.js @@ -0,0 +1,73 @@ +import { AdyenCheckout } from "@adyen/adyen-web"; +import { routeData } from "@router/constants/routes"; +import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper"; + +export async function adyenReturnBeforeEnter(to, from) { + console.log(`In adyen return.`); + console.log(JSON.parse(JSON.stringify(to))); + + const sessionId = to?.query?.sessionId; + const redirectResult = to?.query?.redirectResult; + + if (sessionId && redirectResult) { + const result = await finalizeAdyenPayment(sessionId, redirectResult); + console.log(`Out of promise`); + console.log(result); + + const sessionInfo = await getSessionInfo(sessionId, result.sessionResult); + + console.log(`Got session info:`); + console.log(sessionInfo); + + // TODO once afterpay info is returned, create cc token and submit order. + + return { + name: routeData.CONFIRMATION.name, + replace: true, + }; + } + + return { + name: routeData.ERROR.name, + replace: true, + }; +} + +function finalizeAdyenPayment(sessionId, redirectResult) { + return new Promise((resolve, reject) => { + createAdyenCheckout({ + session: { + id: sessionId, + }, + handlers: { + onPaymentCompleted: (result, component) => { + console.log(`Payment successful`); + console.log(result); + resolve(result); + }, + onPaymentFailed: (result, component) => { + console.log(`Payment failed`); + console.log(result); + reject("Payment failed from Adyen"); + }, + onError: (error, component) => { + console.log(`Error occured`); + console.log(error); + reject("Error from Adyen"); + }, + }, + options: {}, + }).then( + (checkout) => { + checkout.submitDetails({ + details: { + redirectResult: redirectResult, + }, + }); + }, + (error) => { + reject(error); + } + ); + }); +} diff --git a/src/router/methods/routes.js b/src/router/methods/routes.js index cf0392ccf..ec6077270 100644 --- a/src/router/methods/routes.js +++ b/src/router/methods/routes.js @@ -13,6 +13,7 @@ import { restartBeforeEnter } from "@/router/methods/route-logic/restart"; import { paymentMethodBeforeEnter } from "@/router/methods/route-logic/payment-method"; import { paymentBeforeEnter } from "@/router/methods/route-logic/payment"; import { serviceLocationBeforeEnter } from "@/router/methods/route-logic/service-location"; +import { adyenReturnBeforeEnter } from "@/router/methods/route-logic/adyen-return"; export const routes = [ // Non-virtual pages. @@ -48,4 +49,5 @@ export const routes = [ createVirtualRoute(routeData.AUTO_ROUTE, autoRouteBeforeEnter), createVirtualRoute(routeData.ERROR, errorBeforeEnter), createVirtualRoute(routeData.RESTART, restartBeforeEnter), + createVirtualRoute(routeData.PAYMENT_ADYEN_RETURN, adyenReturnBeforeEnter), ]; From 8385928d8c5c14061f0acf9b5b076888b4c3c94d Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Tue, 16 Dec 2025 11:10:56 -0500 Subject: [PATCH 053/233] Changes for Dev --- src/constants/application-config.js | 1 + src/constants/experiments.js | 1 + src/layouts/payment-adyen/payment-adyen.vue | 26 ++++++++++++++----- src/layouts/payment-method/payment-method.vue | 14 +++++++--- src/router/constants/navigation-scenarios.js | 1 + src/router/constants/routing-table.js | 4 +++ .../methods/route-logic/adyen-return.js | 2 +- 7 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 93a6f70d8..b739d9ff9 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -22,6 +22,7 @@ const applicationConfig = { PIA_ERROR_URL: location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel", CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/confirmation", + PIA_ADYEN_RETURN_URL: location.protocol + "//" + location.host + "/fmg/virtual/payment/return", GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE", YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60", OUTLOOK_CALENDAR: diff --git a/src/constants/experiments.js b/src/constants/experiments.js index f358fc89f..0ca3987f0 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -33,6 +33,7 @@ const experimentSettings = { SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays", SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays", SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays", + USE_ADYEN_PAYMENT: "UseAdyenPayment", }; const experimentTriggers = { diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index a2d7651a2..5e74692d5 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -49,6 +49,7 @@ import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js" import { paymentMethods } from "@/constants/payment-method-constants"; import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { Dropin } from "@adyen/adyen-web/auto"; +import { applicationConfig } from "@/constants/application-config"; export default { name: "payment-adyen", @@ -279,20 +280,29 @@ export default { await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); try { await submitWorkOrder({ - pageNameToLog: "payment-adyen", + pageNameToLog: this.pageName, submitAfterSave: true, }); } catch (error) { console.log(`error: response from submit work order: ${error.message}`); // navigate to error page + const errorPayload = { + cause: `Error while submitting work order after Adyen payment.`, + currentPage: this.pageName, + sessionId: this.sessionId, + idempotencyKey: this.idempotencyKey, + }; - //this.$refs.loadingModal.isModalVisible = false; + this.$router.handleSoftError(errorPayload); } - //this.$refs.loadingModal.isModalVisible = false; // clear order - // navigate forward + await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE); + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_FORWARD, + this.pageName + ); }, }, @@ -309,12 +319,12 @@ export default { street: this.splitStreetAddress.street, zipCode: this.locationInfo.zipCode, stateOrProvince: this.locationInfo.state, - returnUrl: "http://localhost:8080/fmg/virtual/payment/return", + returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL, email: this.$store.getters.order.customer.emailAddress, IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`, + idempotencyKey: this.idempotencyKey, }; }, @@ -373,6 +383,10 @@ export default { }; }, + idempotencyKey() { + return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`; + }, + sourceSystem() { return "FMG-2.0"; }, diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 955d106b5..0fa02e80a 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -170,6 +170,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas import { debugLog } from "@/helpers/debug-log-helper"; import { ErrorMessage } from "vee-validate"; import { Field } from "vee-validate"; +import experimentMixin from "../../mixins/experiment-mixin"; defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); @@ -609,10 +610,17 @@ export default { } this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - this.$router.navigateWithoutSaving( - this.navigationScenarios.CLICKED_PAY_NOW, - this.pageName + + const shouldUseAdyen = experimentMixin.methods.hasSettingEqualTo( + experimentSettings.USE_ADYEN_PAYMENT, + "true" ); + + const scenarioName = shouldUseAdyen + ? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN + : this.navigationScenarios.CLICKED_PAY_NOW; + + this.$router.navigateWithoutSaving(scenarioName, this.pageName); }, async evaluatePromosAndTaxItemsOnOrder() { //Revalidate promos if there are any inactive, or active promos diff --git a/src/router/constants/navigation-scenarios.js b/src/router/constants/navigation-scenarios.js index b660afca3..c2247d59b 100644 --- a/src/router/constants/navigation-scenarios.js +++ b/src/router/constants/navigation-scenarios.js @@ -61,6 +61,7 @@ const navigationScenarios = { // Payment CLICKED_INSURANCE: "CLICKED_INSURANCE", CLICKED_PAY_NOW: "CLICKED_PAY_NOW", + CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN", CLICKED_PAY_LATER: "CLICKED_PAY_LATER", PIA_ERROR: "PIA_ERROR", PIA_CC_ERROR: "PIA_CC_ERROR", diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index cd41e8fcf..192e1572e 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -535,6 +535,10 @@ const routingTable = function () { }, { scenario: navigationScenarios.CLICKED_PAY_NOW, + destinationPageData: routeData.PAYMENT, + }, + { + scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN, destinationPageData: routeData.PAYMENT_ADYEN, }, { diff --git a/src/router/methods/route-logic/adyen-return.js b/src/router/methods/route-logic/adyen-return.js index 9bf1940ba..a516518ac 100644 --- a/src/router/methods/route-logic/adyen-return.js +++ b/src/router/methods/route-logic/adyen-return.js @@ -1,5 +1,5 @@ import { AdyenCheckout } from "@adyen/adyen-web"; -import { routeData } from "@router/constants/routes"; +import { routeData } from "@/router/constants/routes"; import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper"; export async function adyenReturnBeforeEnter(to, from) { From f11e7dd9ec724e7e1ec784c98ee53e7152753044 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Tue, 16 Dec 2025 13:21:11 -0500 Subject: [PATCH 054/233] CASH-2036: Mobile first popup changes. --- src/layouts/schedule/schedule.vue | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index bee86a537..ff723d9cb 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1793,8 +1793,9 @@ export default { if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) { preSelectedMobileAppointment = firstMobilePMAppt; } else if ( - firstMobilePMDate && - numberOfDaysToFirstMobilePMDate >= noPMMobileDays + (firstMobilePMDate && + numberOfDaysToFirstMobilePMDate >= noPMMobileDays) || + pmMobileDays == 0 ) { preSelectedMobileAppointment = firstMobileAMAppt ? firstMobileAMAppt @@ -1803,10 +1804,12 @@ export default { (preSelectedMobileAppointment = firstMobilePMAppt); } - this.$refs.mobileFirstModal.setSelectedAppointment( - preSelectedMobileAppointment - ); - this.$refs.mobileFirstModal.openModal(); + if (preSelectedMobileAppointment) { + this.$refs.mobileFirstModal.setSelectedAppointment( + preSelectedMobileAppointment + ); + this.$refs.mobileFirstModal.openModal(); + } } } } From 652684782150b8e286421ec3468e4018459d52ef Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:13:20 -0500 Subject: [PATCH 055/233] Updated few playwright scripts --- playwright-tests/pages/HomePage.ts | 9 +++++---- playwright-tests/pages/PaypalPage.ts | 2 ++ playwright-tests/pages/SchedulePage.ts | 5 +++++ .../tests/CashReplaceMultiSlidingGlassDropoff.ts | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/playwright-tests/pages/HomePage.ts b/playwright-tests/pages/HomePage.ts index 39e349790..3d44b023a 100644 --- a/playwright-tests/pages/HomePage.ts +++ b/playwright-tests/pages/HomePage.ts @@ -47,7 +47,7 @@ export class HomePage extends BasePage { //Zip Entry this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox'); - this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextboxButton'); + this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextbox'); this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' }); } @@ -61,10 +61,11 @@ export class HomePage extends BasePage { async letsGetStarted(zip: string, enterFunnelWithZip: boolean) { if (enterFunnelWithZip) { - await this.letsGetStartedButton.evaluate((element, zip) => { + await this.zipEntryLetsGetStartedButton.fill(zip); + /* await this.letsGetStartedButton.evaluate((element, zip) => { const currentHref = element.getAttribute('href') || ''; - element.setAttribute('href', `${currentHref}&zipCode=${zip}`); - }, zip); + element.setAttribute('href', `${currentHref}?zipCode=${zip}`); + }, zip);*/ } // Wait until modal pop up opens and close pop up diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index ce5547f3c..e67e0a57b 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -41,6 +41,8 @@ export class PaypalPage extends BasePage { await this.paypalLoginButton.click(); await this.completePurchaseButton.click(); } else { + await this.usernameTextBox.waitFor({ state: 'visible' }); + await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true }); await this.usernameTextBox.fill(paymentDetails.username!); await this.nextButton.click(); await this.tryAnotherWayButton.click(); diff --git a/playwright-tests/pages/SchedulePage.ts b/playwright-tests/pages/SchedulePage.ts index 77b330aff..944b84136 100644 --- a/playwright-tests/pages/SchedulePage.ts +++ b/playwright-tests/pages/SchedulePage.ts @@ -35,6 +35,7 @@ export class SchedulePage extends BasePage { readonly timeSlots: Locator; readonly allDayDropOffButton: Locator; readonly pickATimeButton: Locator; + readonly mobileFirstModalCloseButton: Locator; constructor(page: Page) { super(page); @@ -65,6 +66,7 @@ export class SchedulePage extends BasePage { this.viewMoreDatesLink = this.page.getByText(/View more dates/).first(); this.appointmentDuration = this.page.locator('.duration-text-block'); this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label').filter({ visible: true}); + this.mobileFirstModalCloseButton = this.page.getByRole('dialog').getByRole('button', { name: 'Close' }); } async selectLocation(testData: Partial) { @@ -229,6 +231,9 @@ export class SchedulePage extends BasePage { async handleSchedulePage(testData: Partial) { const { appointmentDetails } = testData; await this.validateProgressBar(ProgressBarPercentages.SchedulePage); + if (await this.mobileFirstModalCloseButton.isVisible()) { + await this.mobileFirstModalCloseButton.click(); + } await this.selectLocation(testData); await this.scheduleFirstAppointment(testData); await this.nextPage(); diff --git a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts index f488a3555..4042c67de 100644 --- a/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts +++ b/playwright-tests/tests/CashReplaceMultiSlidingGlassDropoff.ts @@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial = { appointmentDetails: { serviceLocation: ServiceLocation.DropOff, appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate, - shopAddress: "6826 sawmill rd, Columbus, OH 43235" + shopAddress: "1343 cameron ave, lewis center, oh 43035" }, // Override vehicle details From ad69af36a0a482b64cf4dbe29338ae227880ae59 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Tue, 16 Dec 2025 15:14:47 -0500 Subject: [PATCH 056/233] CASH-2036: If ShowPMMobileDays experiment setting is 0, choose first available appointment --- src/layouts/schedule/schedule.vue | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ff723d9cb..65eec08d0 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1790,18 +1790,26 @@ export default { } if (this.isShowMobileFirstAppt) { - if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) { + if (pmMobileDays == 0) { + // Selects the first available time slot + preSelectedMobileAppointment = + numberOfDaysToFirstMobileAMDate < numberOfDaysToFirstMobilePMDate + ? firstMobileAMAppt + : firstMobilePMAppt; + } else if ( + firstMobilePMDate && + numberOfDaysToFirstMobilePMDate <= pmMobileDays + ) { + // Selects the first available PM time slot preSelectedMobileAppointment = firstMobilePMAppt; } else if ( - (firstMobilePMDate && - numberOfDaysToFirstMobilePMDate >= noPMMobileDays) || - pmMobileDays == 0 + firstMobilePMDate && + numberOfDaysToFirstMobilePMDate >= noPMMobileDays ) { + // Selects the first available AM time slot. If none, selects the first available PM time slot preSelectedMobileAppointment = firstMobileAMAppt ? firstMobileAMAppt - : null; - !preSelectedMobileAppointment && - (preSelectedMobileAppointment = firstMobilePMAppt); + : firstMobilePMAppt; } if (preSelectedMobileAppointment) { From dbd7681b4748b598d3a5eac95daa08c63ecab659 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Tue, 16 Dec 2025 16:42:23 -0500 Subject: [PATCH 057/233] CASH-1790 quick fix; missed event emit --- .../appointment-type-question/appointment-type-question.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index fc4d08b92..924b2e62a 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -80,6 +80,7 @@ export default { }, set: function (newValue) { this.$emit("update:modelValue", newValue); + this.$emit("handle-appointment-type-change", newValue); }, }, isMobileOnly() { From f78d1c4192b5fe93db51bb279a739f4113a350e3 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Wed, 17 Dec 2025 05:38:15 -0500 Subject: [PATCH 058/233] CASH-2037 CASH-2037 change the products logged in the session name to use the part number and part type. Also add the service package selected for the data analytics team. --- src/helpers/service-package-helper.js | 13 +++++++++++++ src/layouts/quote/quote.vue | 7 ++++++- .../service-package-question.vue | 8 ++++++++ src/mixins/analytics-mixin.js | 17 ++++++++++++++++- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/helpers/service-package-helper.js b/src/helpers/service-package-helper.js index 4144c44e7..d3fc3f01b 100644 --- a/src/helpers/service-package-helper.js +++ b/src/helpers/service-package-helper.js @@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) { return package_names[discountPackage] || null; } +export function getPackageLabel(packageName) { + switch (packageName) { + case packageNames.TIER_ONE: + return externalParamPackageLabels.GLASS_ONLY; + case packageNames.TIER_TWO: + return externalParamPackageLabels.STANDARD; + case packageNames.TIER_THREE: + return externalParamPackageLabels.PREMIUM; + default: + return null; + } +} + function maxTier(tierA, tierB) { if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) { return packageNames.TIER_THREE; diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 01b585349..26dc1af16 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -33,6 +33,7 @@ @vapsItemsSelected="vapsItemsSelectedAction" @quotePageDiscountSelected="quotePageDiscountSelectedAction" @currentNumberOfPackages="currentNumberOfPackagesAction" + @servicePackageSelected="servicePackageSelectedAction" :servicePackage="servicePackage" :activePromos="lineItems.promos" :availableQuotePageDiscounts="quoteDiscountPartsInfo" @@ -564,6 +565,7 @@ export default { isSaveProgressComplete: null, packageNumber: null, skipToInsurance: null, + servicePackageSelected: null, }; }, async mounted() { @@ -670,6 +672,9 @@ export default { vapsItemsSelectedAction(vapsItemsSelected) { this.lineItems.vaps = vapsItemsSelected; }, + servicePackageSelectedAction(newValue) { + this.servicePackageSelected = newValue; + }, quotePageDiscountSelectedAction(supportingItemsSelected) { this.lineItems.supportingItems = supportingItemsSelected; }, @@ -758,7 +763,7 @@ export default { false ); - await savePageData(this.pageName, { saveProgressPopupSkipped: true }, true); + await savePageData(this.pageName, { saveProgressPopupSkipped: true, servicePackageSelected: this.servicePackageSelected }, true); const payment = this.$store.getters.payment; if (payment.isInsurance) { diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index e45147295..43910c61e 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -34,6 +34,7 @@ import { findLineItemsWithPartType, getPackageNameByType, getDiscountedPackageName, + getPackageLabel, } from "@/helpers/service-package-helper"; import { getPromosThatMatchLineItemsOnOrder, @@ -45,6 +46,7 @@ import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-h import experimentMixin from "@/mixins/experiment-mixin.js"; import { experimentSettings } from "@/constants/experiments"; import { savePageData } from "@/router/methods/helpers/save-page-data"; +import { debugLog } from "@/helpers/debug-log-helper"; export default { name: "servicePackageQuestion", @@ -84,6 +86,12 @@ export default { if (this.activePromos?.length) this.selectDefaultPackage(); }, selectedPackageName(newValue) { + debugLog("selectedPackageName:", newValue); + const packageLabel = getPackageLabel(newValue); + + debugLog("getPackageLabel:", packageLabel); + this.$emit("servicePackageSelected", `ServicePackage-${packageLabel}`); + const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue); this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage); const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue); diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index a77fe930a..0be50a8be 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -232,6 +232,21 @@ export default { (part) => `${part.glassLocation}-${part.glassName}` ); + const format = (part) => `${part?.partNumber}-${part?.partType ? part.partType : ""}`; + const mapParts = (arr) => (arr ?? []).map(format); + + const glassParts = mapParts(order?.lineItems?.glassParts); + const supportingItems = mapParts(order?.lineItems?.supportingItems); + const vaps = mapParts(order?.lineItems?.vaps); + const childParts = [].concat( + ...(order?.lineItems?.glassParts ?? []).map((gp) => mapParts(gp?.childParts)) + ); + const allParts = [...glassParts, ...supportingItems, ...vaps, ...childParts]; + + if (applicationUser?.pageData?.quote?.servicePackageSelected) { + allParts.push(applicationUser.pageData.quote.servicePackageSelected); + } + var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`; var sessionData = {}; @@ -251,7 +266,7 @@ export default { ? "Insurance" : "Cash"; sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace"; - sessionData.productType = glassProducts; + sessionData.productType = allParts; sessionData.eon = order?.eon; sessionData.referralNumber = order?.referralNumber; sessionData.referralSequenceNumber = order?.referralSequenceNumber; From 882076f259081527ed5bc842b9f7526ffc25565c Mon Sep 17 00:00:00 2001 From: CarlNation Date: Wed, 17 Dec 2025 07:06:17 -0500 Subject: [PATCH 059/233] prettier --- src/layouts/quote/quote.vue | 9 ++++++++- .../service-package-question.vue | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 26dc1af16..dabc39fd7 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -763,7 +763,14 @@ export default { false ); - await savePageData(this.pageName, { saveProgressPopupSkipped: true, servicePackageSelected: this.servicePackageSelected }, true); + await savePageData( + this.pageName, + { + saveProgressPopupSkipped: true, + servicePackageSelected: this.servicePackageSelected, + }, + true + ); const payment = this.$store.getters.payment; if (payment.isInsurance) { diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index 43910c61e..436e98b24 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -88,7 +88,7 @@ export default { selectedPackageName(newValue) { debugLog("selectedPackageName:", newValue); const packageLabel = getPackageLabel(newValue); - + debugLog("getPackageLabel:", packageLabel); this.$emit("servicePackageSelected", `ServicePackage-${packageLabel}`); From cfb8af306c8d574d8280fe3ab5f8dea96a81e41a Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 17 Dec 2025 12:22:16 -0500 Subject: [PATCH 060/233] CASH-2027 - Add call to oem-after-market-parts endpoint --- src/constants/endpoints.js | 4 +++ src/constants/store-actions.js | 1 + src/layouts/quote/quote.vue | 7 +++++ src/store/index.js | 48 ++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index c1771b3f5..4471b1011 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -216,6 +216,10 @@ const endpoints = { url: "/order/api/v1/order/add-donation-part", method: "POST", }, + GetOemAfterMarketParts: { + url: "/parts/api/v1/parts/oem-after-market-parts", + method: "POST", + }, }; export { endpoints }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 513891ea1..2bbfc87fa 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -23,6 +23,7 @@ const storeActions = { GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", GET_PARTS: "getParts", + GET_OEM_AFTERMARKET_PARTS: "getOemAfterMarketParts", GET_WIPERS: "getWipers", GET_RAIN_REPEL: "getRainRepel", GET_SUPPORTING_ITEMS: "getSupportingItems", diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index dabc39fd7..5b874ad0c 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -219,6 +219,13 @@ export default { "quote" ); + // call API oem-after-market-parts method + const oemAfterMarketParts = baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.GET_OEM_AFTERMARKET_PARTS, + null, + "quote" + ); + const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder; const skipQuoteExperiment = store.getters.applicationUser.experiments.find( (e) => e.universeName === experimentUniverses.IGQ_SkipQuote diff --git a/src/store/index.js b/src/store/index.js index ca1d76f6f..b94fe17a8 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1836,6 +1836,54 @@ export const actions = { return response; }, + // Parts API Actions + async getOemAfterMarketParts(context, { pageNameToLog }) { + const vehicle = context.getters.vehicle; + const damage = context.getters.damage; + const order = context.state.order; + + const carId = vehicle.carId; + const glassArray = damage.glassToReplace; + const resultsArray = damage.partQuestionAnswers; + const zipCode = order.serviceLocation.zipCode; + const vin = vehicle.vin; + const make = vehicle.make; + const serviceType = order.serviceLocation?.appointmentType; + const referralSeqNumber = order.referralSequenceNumber; + const parentAccountNumber = applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; + const oemEndorsementFlag = false; // Need to update from the store when available + + // create a new array to avoid mutating state + const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); + const resultsArrayForPayload = convertResultsForApi(resultsArray); + + const response = await globalMethods.callHttpClient({ + method: endpoints.GetOemAfterMarketParts.method, + endpoint: endpoints.GetOemAfterMarketParts.url, + payload: { + carId: carId, + glassPieces: glassArrayForPayload, + answerResults: resultsArrayForPayload, + zip: zipCode, + vin: vin, + serviceType: serviceType, + referralSeqNumber: referralSeqNumber, + make: make, + parentAccountNumber: parentAccountNumber.toString(), + oemEndorsementFlag: oemEndorsementFlag, + }, + logApiCall: true, + pageNameToLog: pageNameToLog, + }); + + // Flatten location and name properties + response.data.glassPieceParts = convertGlassPieceNamingFromApi( + response.data.glassPieceParts + ); + + return response; + }, + async getWipers(context, { payload, pageNameToLog }) { const carId = payload.carId; const serviceZipCode = payload.serviceZipCode; From 7c2fb6eee025937948f43d8218e1689779081314 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:53:32 -0500 Subject: [PATCH 061/233] fixed alert validation playwright tests --- playwright-tests/pages/LookupPage.ts | 14 +++++++------- playwright-tests/pages/ServiceZipPage.ts | 4 ++-- playwright-tests/pages/VehicleLookupAddressPage.ts | 4 ++-- playwright-tests/pages/VehicleLookupLicensePage.ts | 4 ++-- playwright-tests/pages/VinLookupPage.ts | 6 +++--- playwright-tests/pages/forms/AddressForm.ts | 2 +- .../alert-validation/alert0007_VinNotFound.ts | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/playwright-tests/pages/LookupPage.ts b/playwright-tests/pages/LookupPage.ts index e5f2fca04..7d6c1df03 100644 --- a/playwright-tests/pages/LookupPage.ts +++ b/playwright-tests/pages/LookupPage.ts @@ -81,9 +81,9 @@ export class LookupPage extends BasePage { // Map of expected messages by lookup type const expectedMessages = { - 'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.", - 'license plate': "Your license plate didn’t return a VIN match.Please re-enter the information above or provide your VIN in a different way.", - 'VIN': "Your VIN didn’t return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.", + 'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.", + 'license plate': "Your license plate didn’t return a VIN match.Please re-enter the information above or provide your VIN in a different way.", + 'VIN': "Your VIN didn’t return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.", 'ZIP code': "Your ZIP code didn't return a match.Please re-enter the information above or provide your VIN in a different way." }; @@ -106,19 +106,19 @@ export class LookupPage extends BasePage { } } - async handleZipValidation(testData: Partial, zip: string, lookupType: VehicleLookupType, alertFlags?: IAlertFlags): Promise { + async handleZipValidation(testData: Partial): Promise { - let { vehicleDamage } = testData; + let { customerDetails,vehicleDamage, vehicleDetails, alertFlags } = testData; // Only proceed with validation if alertFlags is provided if (alertFlags) { if (alertFlags.isUnserviceableZip) { - await this.checkForUnserviceableZipAlertMessage(zip); + await this.checkForUnserviceableZipAlertMessage(customerDetails!.address.postalCode!); throw new TestSuccessAlert('Unserviceable ZIP validation successful.'); } else if (alertFlags.isInvalidZip) { await this.checkForInvalidZipAlertMessage(); throw new TestSuccessAlert('Invalid ZIP validation successful.'); } else if (alertFlags.isVinNotFound) { - await this.checkForVinNotFoundAlertMessage(lookupType); + await this.checkForVinNotFoundAlertMessage(vehicleDetails!.vehicleLookupType!); throw new TestSuccessAlert('Vin not found validation successful.'); } else { diff --git a/playwright-tests/pages/ServiceZipPage.ts b/playwright-tests/pages/ServiceZipPage.ts index 91393f892..6c1a178f5 100644 --- a/playwright-tests/pages/ServiceZipPage.ts +++ b/playwright-tests/pages/ServiceZipPage.ts @@ -12,9 +12,9 @@ export class ServiceZipPage extends LookupPage { @step("ZipLookupPage >> Lookup by service ZIP: ") async handleServiceZipPage(testData: Partial) { - const { customerDetails, vehicleDetails, alertFlags } = testData; + const { customerDetails } = testData; await this.validateProgressBar(ProgressBarPercentages.ServiceZipPage); await this.enterZip(customerDetails!.address.postalCode!); - await this.handleZipValidation(testData, customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + await this.handleZipValidation(testData); } } \ No newline at end of file diff --git a/playwright-tests/pages/VehicleLookupAddressPage.ts b/playwright-tests/pages/VehicleLookupAddressPage.ts index c5f37ee3d..50a7f1b24 100644 --- a/playwright-tests/pages/VehicleLookupAddressPage.ts +++ b/playwright-tests/pages/VehicleLookupAddressPage.ts @@ -29,10 +29,10 @@ export class VehicleLookupAddressPage extends LookupPage { @step("VehicleLookupAddressPage >> Lookup by address: ") async handleVehicleLookupAddressPage(testData: Partial) { - const { customerDetails, vehicleDetails, alertFlags } = testData; + const { customerDetails, vehicleDetails } = testData; await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage); await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!); - await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + await this.handleZipValidation(testData); } } \ No newline at end of file diff --git a/playwright-tests/pages/VehicleLookupLicensePage.ts b/playwright-tests/pages/VehicleLookupLicensePage.ts index e20828b6e..a19d2156b 100644 --- a/playwright-tests/pages/VehicleLookupLicensePage.ts +++ b/playwright-tests/pages/VehicleLookupLicensePage.ts @@ -34,11 +34,11 @@ export class VehicleLookupLicensePage extends LookupPage { @step("VehicleLookupLicensePage >> Lookup by license plate: ") async handleVehicleLookupLicensePage(testData: Partial) { - const { customerDetails, vehicleDetails, alertFlags } = testData; + const { customerDetails, vehicleDetails } = testData; await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage); await this.enterPlateDetails(vehicleDetails!); await this.enterZip(customerDetails!.address.postalCode!); - await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + await this.handleZipValidation(testData); } } \ No newline at end of file diff --git a/playwright-tests/pages/VinLookupPage.ts b/playwright-tests/pages/VinLookupPage.ts index 3b69fc2d6..2f508acda 100644 --- a/playwright-tests/pages/VinLookupPage.ts +++ b/playwright-tests/pages/VinLookupPage.ts @@ -24,11 +24,11 @@ export class VinLookupPage extends LookupPage { @step("VinLookupPage >> Lookup by VIN: ") async handleVehicleLookupVinPage(testData: Partial) { - const { customerDetails, vehicleDetails, alertFlags } = testData; + const { customerDetails, vehicleDetails } = testData; let vin: string; if (Array.isArray(vehicleDetails?.vin)) { - vin = vehicleDetails.vin[0]; + vin = vehicleDetails!.vin[0]; } else { vin = vehicleDetails?.vin || ''; } @@ -36,6 +36,6 @@ export class VinLookupPage extends LookupPage { await this.validateProgressBar(ProgressBarPercentages.VinLookupPage); await this.enterVin(vin); await this.enterZip(customerDetails!.address.postalCode!); - await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!); + await this.handleZipValidation(testData); } } \ No newline at end of file diff --git a/playwright-tests/pages/forms/AddressForm.ts b/playwright-tests/pages/forms/AddressForm.ts index 4ddb6a512..92f270320 100644 --- a/playwright-tests/pages/forms/AddressForm.ts +++ b/playwright-tests/pages/forms/AddressForm.ts @@ -57,7 +57,7 @@ export class AddressForm extends BasePage { ); }); - await this.page.hover(".pac-container .pac-item"); + await this.addressSuggestionList.hover(); await new Promise(resolve => setTimeout(resolve, 1000)); await this.addressSuggestionList.click(); // Select the first suggestion //await this.streetAddressTextBox.press('Tab'); // Move focus to ZIP code field diff --git a/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts b/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts index d88ceeab6..7c01188cb 100644 --- a/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts +++ b/playwright-tests/tests/alert-validation/alert0007_VinNotFound.ts @@ -75,10 +75,10 @@ const lookupTypesToTest: LookupTestCase[] = [ }, customerDetails: { address: { - street: '4076 Spectacle Drive', + street: '4219 Turpin Ln', city: 'Columbus', state: 'OH', - postalCode: '59261', + postalCode: '43230', country: 'United States' } }, From dc8d7b4c27f7dec274d3c5586bc5b89504f153cf Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Thu, 18 Dec 2025 10:14:07 -0500 Subject: [PATCH 062/233] CASH-2036: Defect fix --- src/layouts/schedule/schedule.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 6c0b3506f..71ee19d35 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -1801,7 +1801,7 @@ export default { if (pmMobileDays == 0) { // Selects the first available time slot preSelectedMobileAppointment = - numberOfDaysToFirstMobileAMDate < numberOfDaysToFirstMobilePMDate + numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate ? firstMobileAMAppt : firstMobilePMAppt; } else if ( From 5802ccead3094091634516915e7edcf0a86e90cc Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 18 Dec 2025 11:15:36 -0500 Subject: [PATCH 063/233] Paypal + Afterpay + Apple Pay Submission --- src/helpers/adyen-helper.js | 36 ++++++++++++++++ src/layouts/payment-adyen/payment-adyen.vue | 21 ++++++---- .../methods/route-logic/adyen-return.js | 41 ++++++++++++++++++- 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/helpers/adyen-helper.js b/src/helpers/adyen-helper.js index 93bd53bc8..96d04f767 100644 --- a/src/helpers/adyen-helper.js +++ b/src/helpers/adyen-helper.js @@ -3,6 +3,7 @@ import store from "@/store"; import globalMethods from "@/global-methods"; import { endpoints } from "@/constants/endpoints"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; +import { paymentMethods } from "@/constants/payment-method-constants"; export async function getNewSession(sessionConfig) {} @@ -69,6 +70,41 @@ export async function createAdyenCheckout({ return await AdyenCheckout(config); } +export function mapAdyenToFmgPaymentMethod(adyenMethod) { + const paymentMethodMap = { + ["scheme"]: paymentMethods.CREDIT_CARD, + ["afterpaytouch"]: paymentMethods.AFTERPAY, + ["paypal"]: paymentMethods.PAYPAL, + ["applepay"]: paymentMethods.APPLE_PAY, + }; + + const match = paymentMethodMap[adyenMethod]; + + return match ?? paymentMethods.CREDIT_CARD; +} + +export function generateCcToken(adyenSessionInfo) { + const order = store.getters.order; + const locationInfo = getLocationInfo(order); + + const ccToken = { + subscriptionId: adyenSessionInfo?.storedToken, + expMonth: adyenSessionInfo?.cardExpiryMonth, + expYear: adyenSessionInfo?.cardExpiryYear, + cardType: adyenSessionInfo?.cardType, + billToPostalCode: locationInfo.zipCode, // TODO - real data? + billToFirstName: order.customer.firstName, // TODO - real data? + billToLastName: order.customer.lastName, // TODO - real data? + referenceNumber: adyenSessionInfo?.workOrderNumber, + authCode: adyenSessionInfo?.authCode, + transactionId: adyenSessionInfo?.transactionReference, + transReferenceNumber: adyenSessionInfo?.transactionReference, + lastFour: adyenSessionInfo?.last4DigitsOfCard, + }; + + return ccToken; +} + function getLocationInfo(order) { const serviceLocation = order.serviceLocation; const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 5e74692d5..2d351472c 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -22,13 +22,6 @@
- - + From 3f9df4c331e7a5f4172bf8d9d220680ea549456f Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 7 Jan 2026 10:27:20 -0500 Subject: [PATCH 097/233] Page prereqs --- src/layouts/payment-adyen/payment-adyen.vue | 109 +++++++++++++++++++- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 3e6853aaf..a8b1704a1 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -66,6 +66,8 @@ import { showFmgLoadingModal } from "../../helpers/loading-modal-helper"; import cart from "@/fmg-components/cart/cart"; import { coverageStatus } from "@/constants/insurance"; import { deepClone } from "@/helpers/object-helper"; +import store from "@/store"; +import { debugLog } from "@/helpers/debug-log-helper.js"; export default { name: "payment-adyen", @@ -110,11 +112,110 @@ export default { }, arePagePrerequisitesValid() { - // Stub, for checking if application is capable of entering this page - // And/or, is all information required for this page present? - // To prevent sequence-breaking from the end-user. + // Service Location + const serviceLocation = store.getters.order.serviceLocation; + const mobileReqs = !!( + serviceLocation.address && + serviceLocation.city && + serviceLocation.state && + serviceLocation.zipCode + ); - return true; + const providerLocation = serviceLocation.provider.address; + const dropOffInshopReqs = !!( + providerLocation.streetAddress && + providerLocation.city && + providerLocation.state && + providerLocation.zipCode + ); + + const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE; + + const serviceLocationReqs = + (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs); + + // Insurance + const isInsuranceSet = store.getters.order.payment.isInsurance !== null; + + // Schedule + const schedule = store.getters.order.schedule; + const scheduleReqs = !!( + schedule.date && + schedule.startTime && + schedule.endTime && + schedule.jobMaxMinutes && + schedule.jobMinMinutes + ); + + // Customer + const customer = store.getters.order.customer; + const customerReqs = !!( + customer.firstName && + customer.lastName && + customer.phoneNumber && + customer.emailAddress + ); + + const paymentMethodReqs = + store.getters.order.payment.isPia !== null && + (store.getters.order.payment.isPia || !!store.getters.order.payment.piaType); + + const preReqResult = + serviceLocationReqs && + isInsuranceSet && + scheduleReqs && + customerReqs && + paymentMethodReqs; + + // prettier-ignore + { + debugLog("--- payment-adyen.vue pagePrereqs start ---", null, !preReqResult); + debugLog("mobileReqs:", mobileReqs, !preReqResult); + debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult); + debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult); + debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult); + debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult); + + debugLog("", null, !preReqResult); + + debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult); + debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult); + debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult); + debugLog("providerLocation.city:", providerLocation.city, !preReqResult); + debugLog("providerLocation.state:", providerLocation.state, !preReqResult); + debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult); + + debugLog("", null, !preReqResult); + + debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult); + + debugLog("", null, !preReqResult); + + debugLog("scheduleReqs:", scheduleReqs, !preReqResult); + debugLog("schedule.date:", schedule.date, !preReqResult); + debugLog("schedule.startTime:", schedule.startTime, !preReqResult); + debugLog("schedule.endTime:", schedule.endTime, !preReqResult); + debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult); + debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult); + + debugLog("", null, !preReqResult); + + debugLog("customerReqs:", customerReqs, !preReqResult); + debugLog("customer.firstName:", customer.firstName, !preReqResult); + debugLog("customer.lastName:", customer.lastName, !preReqResult); + debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult); + debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult); + + debugLog("", null, !preReqResult); + + debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult); + debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult); + debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult); + + debugLog("--- payment-adyen.vue pagePrereqs end ---", null, !preReqResult); + } + + return preReqResult; }, async initializeAdyen() { From 3d3d48a1ec2722102c252a1db6f85463d58ece40 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 7 Jan 2026 11:17:14 -0500 Subject: [PATCH 098/233] Move hardcoded test values to env variables --- environment-variables.js | 2 ++ src/constants/application-config.js | 2 ++ src/helpers/adyen-helper.js | 5 +++-- src/layouts/payment-adyen/payment-adyen.vue | 1 - 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/environment-variables.js b/environment-variables.js index 4d894b006..17c797cfd 100644 --- a/environment-variables.js +++ b/environment-variables.js @@ -13,6 +13,8 @@ const environmentVariables = { ['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx", ['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30, ['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "", + ['__VUE_APP_ADYEN_ENVIRONMENT__']: "test", + ['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", }; const keysOnly = Object.keys(environmentVariables); diff --git a/src/constants/application-config.js b/src/constants/application-config.js index b739d9ff9..91873d455 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -32,6 +32,8 @@ const applicationConfig = { COOKIE_NAME_LENGTH_MAX_LIMIT: 44, SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__, RETURN_USER_PAGE: "/fmg/return-user", + ADYEN_CLIENT_KEY: process.env.__VUE_APP_ADYEN_CLIENT_KEY__, + ADYEN_ENVIRONMENT: process.env.__VUE_APP_ADYEN_ENVIRONMENT__, }; export { applicationConfig }; diff --git a/src/helpers/adyen-helper.js b/src/helpers/adyen-helper.js index 96d04f767..8de92bf89 100644 --- a/src/helpers/adyen-helper.js +++ b/src/helpers/adyen-helper.js @@ -4,6 +4,7 @@ import globalMethods from "@/global-methods"; import { endpoints } from "@/constants/endpoints"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { paymentMethods } from "@/constants/payment-method-constants"; +import { applicationConfig } from "../constants/application-config"; export async function getNewSession(sessionConfig) {} @@ -42,8 +43,8 @@ export async function createAdyenCheckout({ id: id, sessionData: sessionData, }, - clientKey: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD", - environment: "test", + clientKey: applicationConfig.ADYEN_CLIENT_KEY, + environment: applicationConfig.ADYEN_ENVIRONMENT, locale: "en_US", countryCode: "US", showPayButton: showPayButton, diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index a8b1704a1..d09ffd14e 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -281,7 +281,6 @@ export default { value: this.adyenPriceTotal, currency: "USD", }, - environment: "test", // Change this to "live" when you're ready to accept live PayPal payments countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production. blockPayPalVenmoButton: true, blockPayPalPayLaterButton: true, From 5488b3d6a341aabdf79d0067eb64d23a1a5d4cc1 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 7 Jan 2026 11:55:26 -0500 Subject: [PATCH 099/233] Prevent unwanted execution in error scenario --- src/layouts/payment-adyen/payment-adyen.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index d09ffd14e..d4420f437 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -430,6 +430,8 @@ export default { }; this.$router.handleSoftError(errorPayload); + + return; } // clear order From f02751ad6fd57f959606d41ab1ec3329eccd4640 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 7 Jan 2026 12:14:17 -0500 Subject: [PATCH 100/233] Remove window.checkout Assignment as it is Unneeded --- src/layouts/payment-adyen/payment-adyen.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index d4420f437..fffd56b21 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -270,7 +270,6 @@ export default { console.log(checkout); - window.checkout = checkout; const dropin = new Dropin(checkout, { paymentMethodsConfiguration: { ideal: { From e3d705702cffbe337fb0ba4035025c7f00855b18 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 7 Jan 2026 12:46:47 -0500 Subject: [PATCH 101/233] Add back environment setting per Adyen documentation. --- src/layouts/payment-adyen/payment-adyen.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index fffd56b21..b407f9d55 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -280,6 +280,7 @@ export default { value: this.adyenPriceTotal, currency: "USD", }, + environment: applicationConfig.ADYEN_ENVIRONMENT, countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production. blockPayPalVenmoButton: true, blockPayPalPayLaterButton: true, From 9d8e8c851bb931b819628ecccbd887f2805fb0ad Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 7 Jan 2026 13:32:22 -0500 Subject: [PATCH 102/233] CASH-1936 - Added parentAccountNumber property to partsorquestion endpoint --- src/store/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index ca1d76f6f..14ecceece 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1765,7 +1765,8 @@ export const actions = { const glassArray = damage.glassToReplace; const zipCode = order.serviceLocation.zipCode; const vin = vehicle.vin; - const serviceType = order.serviceLocation?.appointmentType; + const serviceType = damage.isRepair ? "Repair" : "Install"; + const parentAccountNumber = context.getters.payment.parentAccountNumber; const referralSeqNumber = order.referralSequenceNumber; // create a new array to avoid mutating state @@ -1781,6 +1782,7 @@ export const actions = { vin: vin, serviceType: serviceType, referralSeqNumber: referralSeqNumber, + parentAccountNumber: parentAccountNumber, }, logApiCall: true, pageNameToLog: pageNameToLog, From 09084dab48818bcfa09333bbd5e98cdcc9b1df84 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 8 Jan 2026 13:59:26 -0500 Subject: [PATCH 103/233] Simplify + Add logging --- src/layouts/payment-adyen/payment-adyen.vue | 34 +++++---------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index b407f9d55..45bdc4961 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -378,37 +378,19 @@ export default { await this.saveAndSubmitWorkOrder(); }, - async handleFailedPayment(result) { + handleFailedPayment(result) { console.log(`Result =`); console.log(result); - // Fetch session info - const paymentSessionRequest = { - zipCodeCtu: this.locationInfo.zipCodeCtu, - workOrderNumber: this.workOrderNumberLastSixDigits, - sourceSystem: this.sourceSystem, - sessionId: this.sessionId, - sessionResult: result?.sessionResult, - }; + const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`; - console.log(`Get session payload =`); - console.log(paymentSessionRequest); - - try { - const response = await globalMethods.callHttpClient({ - method: endpoints.GetAdyenSessionResult.method, - endpoint: endpoints.GetAdyenSessionResult.url, - payload: paymentSessionRequest, - logApiCall: true, - pageNameToLog: "payment-adyen", - }); - - console.log(response); - } catch (error) { - console.log(`Error getting session info after failure.`); - } + global.$logger.logError(stringToLog); + }, + handleError(error) { + const stringToLog = `ADYEN ERROR. Name = ${error.name}. Details = ${error.message}.`; + + global.$logger.logError(stringToLog); }, - handleError(error) {}, async saveAndSubmitWorkOrder() { // Work order submission after successful payment. From 6534b06238bb296b245491a33d26d3ece3591f86 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 8 Jan 2026 16:40:43 -0500 Subject: [PATCH 104/233] Parity experience for failed payments. --- src/layouts/payment-adyen/payment-adyen.vue | 59 +++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 45bdc4961..4ea759e2c 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -4,6 +4,16 @@
+
+ +
@@ -45,6 +55,7 @@ import Form from "vee-validate"; 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 alert from "@/ux-components/alert/alert"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; @@ -75,6 +86,8 @@ export default { data() { return { sessionId: String, + dropinComponent: null, + hasPaymentFailureError: false, }; }, @@ -292,7 +305,11 @@ export default { name: "Credit or debit card", }, }, - }).mount("#adyen-container"); + }); + + this.dropinComponent = dropin; + + dropin.mount("#adyen-container"); }, async handleCompletedPayment(result) { @@ -378,13 +395,17 @@ export default { await this.saveAndSubmitWorkOrder(); }, - handleFailedPayment(result) { + async handleFailedPayment(result) { console.log(`Result =`); console.log(result); const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`; - global.$logger.logError(stringToLog); + await global.$logger.logError(stringToLog); + + this.hasPaymentFailureError = true; + + this.dropinComponent?.update(); }, handleError(error) { const stringToLog = `ADYEN ERROR. Name = ${error.name}. Details = ${error.message}.`; @@ -423,6 +444,37 @@ export default { this.pageName ); }, + + async paymentFailedPayLater() { + console.log(`Doing final work order submission...`); + showFmgLoadingModal(true); + + await this.dispatchStoreAction( + storeActions.SAVE_PAYMENT_METHOD_CHOICE, + paymentMethods.PAY_LATER, + false + ); + + await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); + + try { + await submitWorkOrder({ + pageNameToLog: this.pageName, + submitAfterSave: true, + }); + + await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE); + + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_FORWARD, + this.pageName + ); + } catch (error) { + console.log(`Final submission failed after failed payment.`); + this.$router.handleSoftError(); + return; + } + }, }, computed: { @@ -563,6 +615,7 @@ export default { funnelSubHeader, navbar, cart, + alert, }, }; From 6aa8d3434cb6c1890b8dd303147368bfafed037a Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 8 Jan 2026 17:30:49 -0500 Subject: [PATCH 105/233] Parity for adyen failures in redirect flow --- src/layouts/payment-adyen/payment-adyen.vue | 6 +++++ .../methods/route-logic/adyen-return.js | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 4ea759e2c..262614235 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -105,9 +105,15 @@ export default { const resultMap = await settleAllPromises(promiseResultMap); + const hasExistingPiaError = to.query?.piaFailure; + // Hydrate page with results next(async (vm) => { vm.setCmsContent(resultMap.cmsContent); + + if(hasExistingPiaError) { + vm.hasPaymentFailureError = true; + } }); }, diff --git a/src/router/methods/route-logic/adyen-return.js b/src/router/methods/route-logic/adyen-return.js index d94de4c38..1e423e056 100644 --- a/src/router/methods/route-logic/adyen-return.js +++ b/src/router/methods/route-logic/adyen-return.js @@ -15,7 +15,21 @@ export async function adyenReturnBeforeEnter(to, from) { const redirectResult = to?.query?.redirectResult; if (sessionId && redirectResult) { - const result = await finalizeAdyenPayment(sessionId, redirectResult); + let result = null; + + try { + result = await finalizeAdyenPayment(sessionId, redirectResult); + } catch (error) { + // Payment fails, so return user to payment-adyen screen to try again or pay later. + return { + name: routeData.PAYMENT_ADYEN.name, + query: { + piaFailure: true, + }, + replace: true, + }; + } + console.log(`Out of promise`); console.log(result); @@ -93,12 +107,18 @@ function finalizeAdyenPayment(sessionId, redirectResult) { onPaymentFailed: (result, component) => { console.log(`Payment failed`); console.log(result); - reject("Payment failed from Adyen"); + reject({ + paymentFailure: true, + message: "Payment failed from Adyen", + }); }, onError: (error, component) => { console.log(`Error occured`); console.log(error); - reject("Error from Adyen"); + reject({ + paymentFailure: false, + message: "Error from Adyen", + }); }, }, options: {}, From 3cbddd8cda0741951d17b5da0c5e46a228cc0aba Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Fri, 9 Jan 2026 10:58:04 -0500 Subject: [PATCH 106/233] CASH-2025: updates to store actions --- src/constants/store-actions.js | 1 + src/constants/store-mutations.js | 1 + src/store/index.js | 13 +++++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index e09a4c78b..2fc0c88eb 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -111,6 +111,7 @@ const storeActions = { SAVE_PAYPAL_TOKEN: "savePaypalToken", SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount", SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn", + SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable", CREATE_SUBMITTED_STATE: "createSubmittedState", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 2112c789c..5167581e0 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -69,6 +69,7 @@ const storeMutations = { LOCK_TOKEN: "updateLockToken", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", + UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", diff --git a/src/store/index.js b/src/store/index.js index 1c1f4038f..8dd6d7502 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -190,6 +190,7 @@ const getDefaultState = () => { settledTenderAmount: 0, isRecalAckOptIn: false, isMSRFeeApplicable: false, + isOemGlassSelected: false, }, applicationUser: { eventBus: [], @@ -342,6 +343,9 @@ export const mutations = { updateIsRecalAckOptIn(state, isRecalAckOptIn) { state.order.isRecalAckOptIn = isRecalAckOptIn; }, + updateIsOemGlassSelected(state, isOemGlassSelected) { + state.order.isOemGlassSelected = isOemGlassSelected; + }, updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { state.order.isMSRFeeApplicable = isMSRFeeApplicable; }, @@ -674,6 +678,7 @@ export const mutations = { state.order.eon = sessionInformation.order.eon; state.order.customerPortalLoginToken = sessionInformation.order.CustomerPortalLoginToken; state.order.isRecalAckOptIn = sessionInformation.order.isRecalAckOptIn; + state.order.isOemGlassSelected = sessionInformation.order.isOemGlassSelected; if (state.order.vehicle.vin !== sessionInformation.order.vehicle?.vin) { state.applicationUser.pageData[routeData.PART_QUESTIONS.name] = null; @@ -1853,8 +1858,7 @@ export const actions = { const serviceType = order.serviceLocation?.appointmentType; const referralSeqNumber = order.referralSequenceNumber; const parentAccountNumber = applicationConfig.CASH_PARENT_ACCOUNT_NUMBER; - const oemEndorsementFlag = false; // Need to update from the store when available - + const oemEndorsementFlag = order.isOemGlassSelected; // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); const resultsArrayForPayload = convertResultsForApi(resultsArray); @@ -2542,6 +2546,7 @@ export const actions = { lockToken: order.lockToken, isRecalAckOptIn: order.isRecalAckOptIn, isMSRFeeApplicable: order.isMSRFeeApplicable, + isOemGlassSelected: order.isOemGlassSelected, cashPriceSubTotal: order.cashPriceSubTotal, }, }, @@ -2959,6 +2964,10 @@ export const actions = { context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn); }, + saveIsOemGlassSelected(context, isOemGlassSelected) { + context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected); + }, + saveIsMSRFeeApplicable(context, isMSRFeeApplicable) { context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable); }, From 9af898604d8f28d12ee50ca2f6f410ecbc640068 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Fri, 9 Jan 2026 10:59:33 -0500 Subject: [PATCH 107/233] CASH-2025: temp commenting during dev --- src/layouts/quote/quote.vue | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index b08e9ed72..9f973d79d 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -2,6 +2,13 @@
+ +
+

+ alternateGlassPartsForOem: {{ alternateGlassPartsForOem }} +

+
+
@@ -743,6 +750,12 @@ export default { this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems); } + // isOemGlassSelected? + // if it IS, then swap out the glass parts in the line items for these + + // if it's NOT selected BUT alternateGlassPartsForOem exist, then swap out + // // the existing glass line items for whatever is in alternateGlassPartsForOem + if (this.lineItems.glassParts?.length > 0) { this.dispatchStoreAction( this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING, From c6e006020c1decd8a57fffd63e371c068f7acf34 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Fri, 9 Jan 2026 11:01:16 -0500 Subject: [PATCH 108/233] CASH-2025: new oem glass question component --- .../oem-glass-question/oem-glass-question.vue | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/layouts/quote/oem-glass-question/oem-glass-question.vue diff --git a/src/layouts/quote/oem-glass-question/oem-glass-question.vue b/src/layouts/quote/oem-glass-question/oem-glass-question.vue new file mode 100644 index 000000000..b1b4b4e48 --- /dev/null +++ b/src/layouts/quote/oem-glass-question/oem-glass-question.vue @@ -0,0 +1,134 @@ + + + + + From e7e499ca461ac96dc4a0a4a42737cde38cb95855 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Fri, 9 Jan 2026 11:05:40 -0500 Subject: [PATCH 109/233] CASH-2025: add oem question and logic --- src/layouts/quote/quote.vue | 58 ++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 9f973d79d..0f273dcb1 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -93,6 +93,11 @@ isInsuranceSelected ? isInsuranceContinueButtonText : '' " /> + + ({ @@ -410,6 +437,8 @@ export default { vm.holdParentAccountNumber = holdParentAccountNumber; vm.holdBillToAccountNumber = holdBillToAccountNumber; vm.skipToInsurance = shouldSkipToInsurance; + vm.offerOem = offerOem; + vm.alternateGlassPartsForOem = alternateGlassPartsForOem; // oemParts if (revalidatePromoResponse) { const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse( @@ -577,6 +606,8 @@ export default { availableLineItems: null, lineItems: [], addableVaps: [], + offerOem: null, + alternateGlassPartsForOem: null, servicePackage: null, holdParentAccountNumber: null, holdBillToAccountNumber: null, @@ -586,6 +617,7 @@ export default { packageNumber: null, skipToInsurance: null, servicePackageSelected: null, + isOemGlassSelected: false, }; }, async mounted() { @@ -788,6 +820,11 @@ export default { }, false ); + this.dispatchStoreAction( + this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, + this.isOemGlassSelected, + false + ); await savePageData( this.pageName, @@ -922,6 +959,7 @@ export default { recalDisclaimer, saveProgressModalQuestion, saveProgressPopupQuestion, + oemGlassQuestion, }, }; From 9fad67319b925da03b42c8b3ede8135b46bf7871 Mon Sep 17 00:00:00 2001 From: Chris Redelinghuys Date: Fri, 9 Jan 2026 15:50:54 -0500 Subject: [PATCH 110/233] CASH-2025: Swap OEM glass order item --- .../oem-glass-question/oem-glass-question.vue | 5 +-- src/layouts/quote/quote.spec.js | 3 ++ src/layouts/quote/quote.vue | 44 +++++++++++++------ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/layouts/quote/oem-glass-question/oem-glass-question.vue b/src/layouts/quote/oem-glass-question/oem-glass-question.vue index b1b4b4e48..e1fdc2508 100644 --- a/src/layouts/quote/oem-glass-question/oem-glass-question.vue +++ b/src/layouts/quote/oem-glass-question/oem-glass-question.vue @@ -1,7 +1,5 @@