From c9e5ccff54ad208fdbdb7cd695448bf8a36f4f25 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Wed, 11 Mar 2026 14:14:46 -0400 Subject: [PATCH 01/25] Reapply "Merge pull request #3075 from Safelite/feature/CASH-1684" This reverts commit 27c7da1eb5891d761ae8267a690ee44dedc28697. --- src/assets/img/icons/alert-circle-yellow.svg | 3 ++ src/helpers/cms-content-helper.js | 11 +++++ src/layouts/schedule/schedule.vue | 42 ++++++++++++++++++-- src/ux-components/alert/alert.vue | 21 ++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/assets/img/icons/alert-circle-yellow.svg diff --git a/src/assets/img/icons/alert-circle-yellow.svg b/src/assets/img/icons/alert-circle-yellow.svg new file mode 100644 index 000000000..904aefb98 --- /dev/null +++ b/src/assets/img/icons/alert-circle-yellow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 727add296..450875d0f 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -343,3 +343,14 @@ export function splitCMSCopyOnParagraphTag(copy) { export function splitCMSCopyOnBR(copy) { return copy.split("
"); } + +// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not. +export function formatToUSDollar(amount) { + const isInteger = amount % 1 === 0; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: isInteger ? 0 : 2, + maximumFractionDigits: 2, + }).format(amount); +} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 4ec065a75..c13026d36 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -81,6 +81,16 @@ cmsWidgetName="AlertNoShopsWidget" v-if="displayNoShopsAlert" alertClass="alert-warning" /> +

Info Icon + Warning Icon {{ alertHeadline }}


@@ -63,7 +69,9 @@ export default { name: "alert", props: { showInfoIcon: Boolean, + showWarningIcon: Boolean, showHorizontalRow: Boolean, + hasBorder: Boolean, isDismissible: Boolean, alertClass: String, /* @@ -96,6 +104,9 @@ export default { infoIcon() { return require(`@/assets/img/icons/info-circle-blue.svg`); }, + warningIcon() { + return require(`@/assets/img/icons/alert-circle-yellow.svg`); + }, pageQueryString() { return applicationConfig.PAGE_QUERYSTRING; }, @@ -211,12 +222,19 @@ export default { background-color: $yellow-100; .alert-heading { color: $yellow-600; + + .warning-icon { + height: 1rem; + } } svg { fill: $yellow-600; width: 1rem; height: 1rem; } + hr { + border-color: $yellow-800; + } } &.alert-success { background-color: $green-100; @@ -229,6 +247,9 @@ export default { height: 1rem; } } + &.bordered { + border: 1px solid; + } & p { font-size: 0.875rem; } From ee47883973cfecf548ebf6b83f28571267b0de94 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 12 Mar 2026 11:37:31 -0400 Subject: [PATCH 02/25] CASH-2462: CJ conversion tracking updates --- src/mixins/analytics-mixin.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 316553548..a3b599331 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -806,14 +806,16 @@ export default { ); coupons = promoString; } - const serviceZipPackage = submittedOrder.lineItems?.supportingItems?.find( - (x) => x.partType == "SERVICE PACKAGE DISCOUNT" + const additionalDiscounts = submittedOrder.lineItems?.supportingItems?.find( + (x) => + x.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT || + x.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT ); - if (isDefined(serviceZipPackage)) { + if (isDefined(additionalDiscounts)) { if (coupons) { coupons += ","; } - coupons += serviceZipPackage.partNumber; + coupons += additionalDiscounts.partNumber; } refSequenceNum = submittedOrder.referralSequenceNumber; @@ -846,17 +848,17 @@ export default { } } } + pushToDataLayerIfDefined({ + event: "commissionJunctionGtmData", + commissionJunctionGtmData: { + cj_commission_junction_event: cjEvent, + cj_referral_sequence_number: refSequenceNum, + cj_amount: amount.toFixed(2), + cj_repair_replace: repairReplace, + cj_coupon: coupons, + }, + }); } - pushToDataLayerIfDefined({ - event: "commissionJunctionGtmData", - commissionJunctionGtmData: { - cj_commission_junction_event: cjEvent, - cj_referral_sequence_number: refSequenceNum, - cj_amount: amount.toFixed(2), - cj_repair_replace: repairReplace, - cj_coupon: coupons, - }, - }); }, pushPageErrorToDataLayer(error) { From 1edd257d06c267f08b593f6ddb0200890a4cd755 Mon Sep 17 00:00:00 2001 From: kpatel8hs4io <31411746+kpatel8hs4io@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:25:30 -0400 Subject: [PATCH 03/25] adding adyen payment flow for playwright tests --- playwright-tests/.env | 1 + playwright-tests/.env.dev | 1 + playwright-tests/framework/TestData.ts | 1 + .../framework/localTypes/IExperiments.ts | 3 +- playwright-tests/pages/BasePage.ts | 8 +- playwright-tests/pages/PaymentAdyenPage.ts | 81 +++++++++++++++++++ playwright-tests/pages/PaymentMethodPage.ts | 55 ++++++++++--- playwright-tests/pages/PaymentPage.ts | 5 +- playwright-tests/pages/PaypalPage.ts | 18 +++-- 9 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 playwright-tests/pages/PaymentAdyenPage.ts diff --git a/playwright-tests/.env b/playwright-tests/.env index 2b8d27d3e..179424e5e 100644 --- a/playwright-tests/.env +++ b/playwright-tests/.env @@ -10,6 +10,7 @@ SKIP_CONTENT_SITE="false" # Experiments Flag IS_MOBILEFIRST="false" IS_ADYENPAYMENTS="false" +IS_MULTILOCATIONPOPUP="false" # Base URLs by environment # qa diff --git a/playwright-tests/.env.dev b/playwright-tests/.env.dev index 8d2786918..de2c3c0f2 100644 --- a/playwright-tests/.env.dev +++ b/playwright-tests/.env.dev @@ -10,6 +10,7 @@ SKIP_CONTENT_SITE="false" # Experiments Flag IS_MOBILEFIRST="false" IS_ADYENPAYMENTS="false" +IS_MULTILOCATIONPOPUP="false" # Base URLs by environment # qa diff --git a/playwright-tests/framework/TestData.ts b/playwright-tests/framework/TestData.ts index f863a74fd..6a87dfd8f 100644 --- a/playwright-tests/framework/TestData.ts +++ b/playwright-tests/framework/TestData.ts @@ -18,5 +18,6 @@ export function getDefaultExperimentsData(): IExperiments { return { isAdyenPayments: !!process.env.IS_ADYENPAYMENTS && process.env.IS_ADYENPAYMENTS !== "" ? process.env.IS_ADYENPAYMENTS === "true" : false, isMobileFirst: !!process.env.IS_MOBILEFIRST && process.env.IS_MOBILEFIRST !== "" ? process.env.IS_MOBILEFIRST === "true" : false, + isMultiLocationPopup: !!process.env.IS_MULTILOCATIONPOPUP && process.env.IS_MULTILOCATIONPOPUP !== "" ? process.env.IS_MULTILOCATIONPOPUP === "true" : false, } } diff --git a/playwright-tests/framework/localTypes/IExperiments.ts b/playwright-tests/framework/localTypes/IExperiments.ts index 78935f480..bfe13c864 100644 --- a/playwright-tests/framework/localTypes/IExperiments.ts +++ b/playwright-tests/framework/localTypes/IExperiments.ts @@ -1,4 +1,5 @@ export interface IExperiments { isMobileFirst: boolean, - isAdyenPayments: boolean + isAdyenPayments: boolean, + isMultiLocationPopup: boolean } \ No newline at end of file diff --git a/playwright-tests/pages/BasePage.ts b/playwright-tests/pages/BasePage.ts index 07d254391..e9fe835dc 100644 --- a/playwright-tests/pages/BasePage.ts +++ b/playwright-tests/pages/BasePage.ts @@ -257,8 +257,12 @@ export class BasePage { : "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true"; experimentsURLExtension += experiments?.isAdyenPayments - ? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)" - : ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)"; + ? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)=true" + : ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)=true"; + + experimentsURLExtension += experiments?.isMultiLocationPopup + ? ",MultiLocationPopup=MultiLocationPopup_V1=MultiLocationPopup_TEST=true" + : ",MultiLocationPopup=MultiLocationPopup_V1=MultiLocationPopup_CONTROL=true"; } else { console.log("Url extension without query string"); } diff --git a/playwright-tests/pages/PaymentAdyenPage.ts b/playwright-tests/pages/PaymentAdyenPage.ts new file mode 100644 index 000000000..82c876af0 --- /dev/null +++ b/playwright-tests/pages/PaymentAdyenPage.ts @@ -0,0 +1,81 @@ +import { type Locator, type Page } from '@playwright/test'; +import { BasePage } from './BasePage'; +import { IPaymentDetails } from 'safelite-playwright-core'; +import { ITestData } from 'framework/TestData'; +import { PaypalPage } from './PaypalPage'; + +export class PaymentAdyenPage extends BasePage { + readonly page: Page; + readonly creditOrDebitCardButton: Locator; + readonly nameOnCardTextField: Locator; + readonly cardNumberTextField: Locator; + readonly expiryDateTextField: Locator; + readonly cvvTextField: Locator; + readonly countryDropDown: Locator; + readonly billingAddressTextField: Locator; + readonly cityTextField: Locator; + readonly stateDropDown: Locator; + readonly billingZipTextField: Locator; + readonly submitPaymentButton: Locator; + readonly payPalButton: Locator; + readonly navigateToPaypalButton: Locator; + readonly afterPayButton: Locator; + readonly navigateToAfterPayButton: Locator; + + constructor(page: Page) { + super(page); + this.page = page; + this.creditOrDebitCardButton = page.locator('.adyen-checkout__payment-method--credit button'); + this.cardNumberTextField = page.frameLocator('iframe[title="Iframe for card number"]').locator('input[id*="adyen-checkout-encryptedCardNumber"]'); + this.expiryDateTextField = page.frameLocator('iframe[title="Iframe for expiry date"]').locator('input[id*="adyen-checkout-encryptedExpiryDate"]'); + this.cvvTextField = page.frameLocator('iframe[title="Iframe for security code"]').locator('input[id*="adyen-checkout-encryptedSecurityCode"]'); + this.nameOnCardTextField = page .locator('input[id*="adyen-checkout-holderName"]'); + this.countryDropDown = page.locator('input[id*="adyen-checkout-country"]'); + this.billingAddressTextField = page.locator('input[id*="adyen-checkout-street"]'); + this.cityTextField = page.locator('input[id*="adyen-checkout-city"]'); + this.stateDropDown = page.locator('input[id*="adyen-checkout-stateOrProvince"]'); + this.billingZipTextField = page.locator('input[id*="adyen-checkout-postalCode"]'); + this.submitPaymentButton = page.locator('button.adyen-checkout__button'); + this.payPalButton = page.locator('button[id*="button-paypal"]'); + this.navigateToPaypalButton = page.frameLocator('iframe[title="PayPal-paypal"]:first-of-type').locator('div[role="link"][class*="paypal-button"]'); + this.afterPayButton = page.locator('button[id*="button-redirect"]'); + this.navigateToAfterPayButton = page.locator('div.adyen-checkout__payment-method--afterpaytouch_US button.adyen-checkout__button'); + + } + + async populateAdyenCreditCardDetails(testData: Partial){ + const { paymentDetails, customerDetails } = testData; + + paymentDetails!.cardNumber = '5100 0600 0000 0002'; + paymentDetails!.expirationMonth = "12 - December"; + paymentDetails!.expirationYear = "2029"; + paymentDetails!.cvv = "737"; + + await this.creditOrDebitCardButton.click(); + await this.cardNumberTextField.fill(paymentDetails!.cardNumber || ''); + + const expirationDate = `${(paymentDetails!.expirationMonth!.split(' ')[0] || '').padStart(2, '0')} + ${paymentDetails!.expirationYear!.toString().slice(-2)}`; + await this.expiryDateTextField.pressSequentially(expirationDate); + + await this.cvvTextField.fill(paymentDetails!.cvv!); + await this.nameOnCardTextField.fill(customerDetails!.firstName! + ' ' + customerDetails!.lastName!); + await this.billingAddressTextField.fill(paymentDetails!.billingAddress!.street); + await this.countryDropDown.pressSequentially("United States"); + await this.countryDropDown.click(); + await this.page.getByRole('option', { name: 'United States', exact: true }).click(); + + await this.cityTextField.fill(paymentDetails!.billingAddress!.city); + await this.stateDropDown.pressSequentially(paymentDetails!.billingAddress!.state); + await this.stateDropDown.click(); + await this.page.locator(".adyen-checkout__field--stateOrProvince li").nth(0).click(); + await this.billingZipTextField.fill(paymentDetails!.billingAddress!.postalCode); + await this.submitPaymentButton.click(); + } + + async navigateToAdyenPaypalCheckout(): Promise { + await this.payPalButton.click(); + const paypalPage = this.page.waitForEvent('popup'); + await this.navigateToPaypalButton.click(); + return new PaypalPage(await paypalPage); + } +} \ No newline at end of file diff --git a/playwright-tests/pages/PaymentMethodPage.ts b/playwright-tests/pages/PaymentMethodPage.ts index 248ce845f..215ccff30 100644 --- a/playwright-tests/pages/PaymentMethodPage.ts +++ b/playwright-tests/pages/PaymentMethodPage.ts @@ -8,6 +8,8 @@ import { AfterpayPage } from './AfterpayPage'; import { PaypalPage } from './PaypalPage'; import { ITestData } from 'framework/TestData'; import { step } from 'framework/localTypes/Step'; +import { IExperiments } from 'framework/localTypes/IExperiments'; +import { PaymentAdyenPage } from './PaymentAdyenPage'; export class PaymentMethodPage extends BasePage { readonly page: Page; @@ -25,7 +27,9 @@ export class PaymentMethodPage extends BasePage { readonly submitButton: Locator; readonly recalibrationCheckbox: Locator; readonly paymentPage: PaymentPage; + readonly paymentAdyenPage: PaymentAdyenPage; readonly paypalPage: PaypalPage; + readonly afterpayPage: AfterpayPage readonly afterPayBreakoutSection: Locator; readonly afterPayToggle: Locator; @@ -59,7 +63,9 @@ export class PaymentMethodPage extends BasePage { this.recalibrationCheckbox = this.page.locator('label:has(>input[name=\'recalAckOptIn\'])'); // this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1); this.paymentPage = new PaymentPage(page); + this.paymentAdyenPage = new PaymentAdyenPage(page); this.paypalPage = new PaypalPage(page); + this.afterpayPage = new AfterpayPage(page); // Payment details validation locators this.reviewTable = this.page.locator('div.review-table'); @@ -191,35 +197,57 @@ export class PaymentMethodPage extends BasePage { return text; } - async executePayment(paymentDetails: IPaymentDetails, isRecalVehicle: boolean) { + async executePayment(testData: Partial) { const browserContext = this.page.context(); + const { paymentDetails, isRecalVehicle, experiments } = testData; - switch (paymentDetails.paymentType) { + switch (paymentDetails!.paymentType) { case PaymentType.Credit: await this.selectCreditCard(); await this.nextPage(); - await this.paymentPage.populateCreditCardDetails(paymentDetails); + experiments!.isAdyenPayments + ? await this.paymentAdyenPage.populateAdyenCreditCardDetails(testData) + : await this.paymentPage.populateCreditCardDetails(paymentDetails!); break; case PaymentType.AfterPay: await this.payInFourButton.click(); await this.continueButton.click(); - // Capture popup - const afterpayPopup = await browserContext.waitForEvent('page'); - const afterpayPage = new AfterpayPage(afterpayPopup); + if (experiments?.isAdyenPayments) { + await this.paymentAdyenPage.afterPayButton.click(); + await this.paymentAdyenPage.navigateToAfterPayButton.click(); + await this.afterpayPage.executeAfterpayPayment(paymentDetails!); + } else { + // Capture popup + const afterpayPopup = await browserContext.waitForEvent('page'); + const afterpayPage = new AfterpayPage(afterpayPopup); + + // Execute payment + await afterpayPage.executeAfterpayPayment(paymentDetails!); + } - // Execute payment - await afterpayPage.executeAfterpayPayment(paymentDetails); break; case PaymentType.Paypal: await this.selectPaypal(); // TODO: Click paypal button await this.nextPage(); - await this.paymentPage.navigateToPaypalCheckout(); - await this.paypalPage.completePaypalPurchase(paymentDetails); + const paypalPage = experiments?.isAdyenPayments + ? await this.paymentAdyenPage.navigateToAdyenPaypalCheckout() + : await this.paymentPage.navigateToPaypalCheckout(); + + /*if (experiments?.isAdyenPayments) { + // Capture popup + const payPalPopup = await this.page.waitForEvent('popup'); + const paypalPage = new PaypalPage(payPalPopup); + paypalPage.completePaypalPurchase(testData); + } else { + await this.paypalPage.completePaypalPurchase(testData); + }*/ + await paypalPage.completePaypalPurchase(testData); + break; case PaymentType.PayAtService: - await this.selectPayAtService(isRecalVehicle); + await this.selectPayAtService(isRecalVehicle!); await this.nextPage(); break; case PaymentType.PayWithInsurance: @@ -527,7 +555,7 @@ export class PaymentMethodPage extends BasePage { @step("PaymentMethodPage >> Select Payment Method: ") async handlePaymentMethodPage(testData: Partial) { - const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData; + const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM, experiments } = testData; await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage); await this.validatePaymentDetailsPage(testData); @@ -542,7 +570,8 @@ export class PaymentMethodPage extends BasePage { await this.verifyVAPS(); } if (paymentDetails?.paymentType) { - await this.executePayment(paymentDetails!, isRecalVehicle!); + // await this.executePayment(paymentDetails!, isRecalVehicle!, experiments!); + await this.executePayment(testData); } else { await this.nextPage(); } diff --git a/playwright-tests/pages/PaymentPage.ts b/playwright-tests/pages/PaymentPage.ts index 9bc95fb66..ca51450df 100644 --- a/playwright-tests/pages/PaymentPage.ts +++ b/playwright-tests/pages/PaymentPage.ts @@ -1,6 +1,7 @@ import { type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; import { IPaymentDetails } from 'safelite-playwright-core'; +import { PaypalPage } from './PaypalPage'; export class PaymentPage extends BasePage { readonly page: Page; @@ -43,8 +44,8 @@ export class PaymentPage extends BasePage { await this.submitPaymentButton.click(); } - async navigateToPaypalCheckout() { + async navigateToPaypalCheckout(): Promise { await this.payPalButton.click(); + return new PaypalPage(this.page); } - } \ No newline at end of file diff --git a/playwright-tests/pages/PaypalPage.ts b/playwright-tests/pages/PaypalPage.ts index d0601ffcb..650acc27a 100644 --- a/playwright-tests/pages/PaypalPage.ts +++ b/playwright-tests/pages/PaypalPage.ts @@ -1,6 +1,6 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { BasePage } from './BasePage'; -import { IPaymentDetails } from 'safelite-playwright-core'; +import { ITestData } from 'framework/TestData'; export class PaypalPage extends BasePage { readonly page: Page; @@ -30,24 +30,28 @@ export class PaypalPage extends BasePage { this.tryAnotherWayButton = page.getByRole('button', { name: 'Try another way' }); } - async completePaypalPurchase(paymentDetails: IPaymentDetails){ + async completePaypalPurchase(testData: Partial){ + + const {paymentDetails, experiments } = testData; // Wait for the PayPal login page to load await this.page.waitForFunction(() => window.location.href.includes('sandbox.paypal.com'), null, { timeout: 10000 }); // handle flow with and without the "Log in with a password instead" link if (await this.loginWithPasswordButton.isVisible()) { await this.loginWithPasswordButton.click(); - await this.passwordTextBox.fill(paymentDetails.password!); + await this.passwordTextBox.fill(paymentDetails!.password!); 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.usernameTextBox.fill(paymentDetails!.username!); await this.nextButton.click(); - await this.tryAnotherWayButton.click(); - await this.usePasswordInsteadButton.click(); - await this.passwordTextBox.fill(paymentDetails.password!); + if (!experiments!.isAdyenPayments){ + await this.tryAnotherWayButton.click(); + await this.usePasswordInsteadButton.click(); + } + await this.passwordTextBox.fill(paymentDetails!.password!); await this.paypalLoginButton.click(); await this.payWithRadioButton.click(); await this.page.waitForTimeout(2000); From 140f24342a950f291596219669ed44ba49bc6de4 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 16 Mar 2026 13:22:14 -0400 Subject: [PATCH 04/25] All refactored pre-req will check all instead of stopping first failr --- src/layouts/payment-adyen/payment-adyen.vue | 16 +++++++++------- src/layouts/payment-method/payment-method.vue | 15 +++++++++------ src/layouts/payment/payment.vue | 16 +++++++++------- src/layouts/quote/quote.vue | 9 +++++---- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 5ae3e25cc..adb0a1210 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -146,13 +146,15 @@ export default { arePagePrerequisitesValid() { const order = store.getters.order; const logQueue = []; - const result = - hasServiceLocationInfo(order, logQueue) && - hasInsuranceInfo(order, logQueue) && - hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue) && - hasPaymentMethodInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue); + const results = [ + hasServiceLocationInfo(order, logQueue), + hasInsuranceInfo(order, logQueue), + hasSchedulingInfo(order, logQueue), + hasCustomerInfo(order, logQueue), + hasPaymentMethodInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + const result = results.every(Boolean); flushPagePrereqsLogs("payment-adyen.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 0ee3ad8ce..48f076f84 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -390,12 +390,15 @@ export default { arePagePrerequisitesValid() { const order = store.getters.order; const logQueue = []; - const result = - hasServiceLocationInfo(order, logQueue) && - hasInsuranceInfo(order, logQueue) && - hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue); + const results = [ + hasServiceLocationInfo(order, logQueue), + hasInsuranceInfo(order, logQueue), + hasSchedulingInfo(order, logQueue), + hasCustomerInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + //false if any check is false but calls all checks for logging purposes + const result = results.every(Boolean); flushPagePrereqsLogs("payment-method.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 0fd452a9d..a8c3653b1 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -445,13 +445,15 @@ export default { arePagePrerequisitesValid() { const order = store.getters.order; const logQueue = []; - const result = - hasServiceLocationInfo(order, logQueue) && - hasInsuranceInfo(order, logQueue) && - hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue) && - hasPaymentMethodInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue); + const results = [ + hasServiceLocationInfo(order, logQueue), + hasInsuranceInfo(order, logQueue), + hasSchedulingInfo(order, logQueue), + hasCustomerInfo(order, logQueue), + hasPaymentMethodInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + const result = results.every(Boolean); flushPagePrereqsLogs("payment.vue", result, logQueue); return result; }, diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index df1a4ab7d..82249e790 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -739,10 +739,11 @@ export default { order.payment?.insuranceCoverage?.isVerified == null || order.payment?.insuranceCoverage?.isVerified === false; - const preReqResult = - hasServiceZipInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue) && - isVerifiedOk; + const results = [ + hasServiceZipInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + const preReqResult = results.every(Boolean) && isVerifiedOk; logQueue.push(() => { debugLog( From 89e4c38a6a334996e87adbb0005dc983418dd22c Mon Sep 17 00:00:00 2001 From: matthew-sykes <32104062+matthew-sykes@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:05:57 -0400 Subject: [PATCH 05/25] Merge pull request #3102 from Safelite/feature/page-prereqs-refactor-CASH-2456 All refactored pre-req will check all instead of stopping first failr --- src/layouts/payment-adyen/payment-adyen.vue | 16 +++++++++------- src/layouts/payment-method/payment-method.vue | 15 +++++++++------ src/layouts/payment/payment.vue | 16 +++++++++------- src/layouts/quote/quote.vue | 9 +++++---- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 5ae3e25cc..adb0a1210 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -146,13 +146,15 @@ export default { arePagePrerequisitesValid() { const order = store.getters.order; const logQueue = []; - const result = - hasServiceLocationInfo(order, logQueue) && - hasInsuranceInfo(order, logQueue) && - hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue) && - hasPaymentMethodInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue); + const results = [ + hasServiceLocationInfo(order, logQueue), + hasInsuranceInfo(order, logQueue), + hasSchedulingInfo(order, logQueue), + hasCustomerInfo(order, logQueue), + hasPaymentMethodInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + const result = results.every(Boolean); flushPagePrereqsLogs("payment-adyen.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 0ee3ad8ce..48f076f84 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -390,12 +390,15 @@ export default { arePagePrerequisitesValid() { const order = store.getters.order; const logQueue = []; - const result = - hasServiceLocationInfo(order, logQueue) && - hasInsuranceInfo(order, logQueue) && - hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue); + const results = [ + hasServiceLocationInfo(order, logQueue), + hasInsuranceInfo(order, logQueue), + hasSchedulingInfo(order, logQueue), + hasCustomerInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + //false if any check is false but calls all checks for logging purposes + const result = results.every(Boolean); flushPagePrereqsLogs("payment-method.vue", result, logQueue); return result; }, diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 0fd452a9d..a8c3653b1 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -445,13 +445,15 @@ export default { arePagePrerequisitesValid() { const order = store.getters.order; const logQueue = []; - const result = - hasServiceLocationInfo(order, logQueue) && - hasInsuranceInfo(order, logQueue) && - hasSchedulingInfo(order, logQueue) && - hasCustomerInfo(order, logQueue) && - hasPaymentMethodInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue); + const results = [ + hasServiceLocationInfo(order, logQueue), + hasInsuranceInfo(order, logQueue), + hasSchedulingInfo(order, logQueue), + hasCustomerInfo(order, logQueue), + hasPaymentMethodInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + const result = results.every(Boolean); flushPagePrereqsLogs("payment.vue", result, logQueue); return result; }, diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index df1a4ab7d..82249e790 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -739,10 +739,11 @@ export default { order.payment?.insuranceCoverage?.isVerified == null || order.payment?.insuranceCoverage?.isVerified === false; - const preReqResult = - hasServiceZipInfo(order, logQueue) && - hasGlassPartsOrRepairInfo(order, logQueue) && - isVerifiedOk; + const results = [ + hasServiceZipInfo(order, logQueue), + hasGlassPartsOrRepairInfo(order, logQueue), + ]; + const preReqResult = results.every(Boolean) && isVerifiedOk; logQueue.push(() => { debugLog( From cc12098c197269a777ba4dc8a31ee41bbf73253d Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 17 Mar 2026 15:41:01 -0400 Subject: [PATCH 06/25] CASH-1951: Service Package Discount decom --- src/constants/experiments.js | 2 - src/constants/part-type-strings.js | 1 - src/layouts/quote/quote.spec.js | 86 ------------------- .../service-package-question.spec.js | 8 +- src/mixins/analytics-mixin.js | 4 +- 5 files changed, 3 insertions(+), 98 deletions(-) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index f8691fc46..b9f38ca36 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -19,8 +19,6 @@ const experimentSettings = { PIA_INSURANCE: "DisplayPIAInsurance", SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA", IS_EMAIL_OPTIONAL: "isEmailOptional", - SERVICE_PACKAGE_DISCOUNT: "OfferServicePackageDiscount", - PROMO_ON_PACKAGE: "Offer_Promo_On_Pkg", RECAL_PRICE_REMOVE: "RecalPriceRemove", INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL: "NextGen_InternalInsuranceTabDisplayThreshold", INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL: "NextGen_ExternalInsuranceTabDisplayThreshold", diff --git a/src/constants/part-type-strings.js b/src/constants/part-type-strings.js index 0118c0b2c..7f62088d5 100644 --- a/src/constants/part-type-strings.js +++ b/src/constants/part-type-strings.js @@ -9,7 +9,6 @@ const partTypeStrings = { MOBILE_FEE: "MOBILE FEE", REPAIR_FEE: "REPAIR FEE", EARLY_BIRD: "EARLY BIRD", - SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT", DONATION: "DONATION", WINDSHIELD: "WINDSHIELD", diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index d9a36f11d..8042b8f24 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -100,9 +100,6 @@ const mockExperimentSettings = experimentSettings; jest.mock("@/mixins/experiment-mixin.js", () => ({ methods: { getSettingValue(settingName) { - if (settingName === mockExperimentSettings.SERVICE_PACKAGE_DISCOUNT) { - return true; - } if ( settingName === mockExperimentSettings.INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL ) { @@ -367,9 +364,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; const { wrapper } = setupMocks({}); @@ -391,59 +385,6 @@ describe("quote.vue", () => { // This should have its own test //expect(vm.isInsuranceSelected !== null).toBe(true); }); - test("Returns true if service package discount setting is true", async () => { - //Arrange - store.getters = { - lineItems: { - glassParts: ["item", "item2"], - }, - pageData: jest.fn((page) => { - if (page === "quote") { - return { saveProgressPopupSkipped: true }; - } - return {}; - }), - applicationUser: { - experiments: [], - }, - order: { - lineItems: { - glassParts: ["item", "item2"], - }, - payment: {}, - customer: { - emailAddress: "test@test.com", - }, - serviceLocation: { - zipCode: "12345", - zipCodeCtu: "value", - }, - }, - vehicle: { - cardId: "123", - }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, - }; - const { wrapper } = setupMocks({}); - - wrapper.vm.$route = { query: { isInsurance: "false" } }; - - const isServicePackageDiscount = experimentMixin.methods.getSettingValue( - experimentSettings.SERVICE_PACKAGE_DISCOUNT - ); - - //Act - await quote.beforeRouteEnter.call( - wrapper.vm, - { query: { fmgPage: "quote" } }, - undefined, - (c) => c(wrapper.vm) - ); - //Assert - expect(isServicePackageDiscount).toBe(true); - }); test("should default to insurance if query param 'isInsurance' is true", async () => { //Arrange store.getters = { @@ -476,9 +417,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; const { wrapper } = setupMocks({}); wrapper.vm.$route = { query: { isInsurance: "true" } }; @@ -526,9 +464,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; mockTierOnePrice = 200; const { wrapper } = setupMocks({}); @@ -577,9 +512,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; const { wrapper } = setupMocks({}); // Ensure that query param isn't overriding selection @@ -628,9 +560,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; const { wrapper } = setupMocks({}); // Ensure that query param isn't overriding selection @@ -680,9 +609,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; mockTierOnePrice = 200; const { wrapper } = setupMocks({}); @@ -734,9 +660,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; mockTierOnePrice = 505; const { wrapper } = setupMocks({}); @@ -786,9 +709,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; mockTierOnePrice = 505; const { wrapper } = setupMocks({}); @@ -839,9 +759,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; const { wrapper } = setupMocks({}); wrapper.vm.$route = { query: null }; @@ -962,9 +879,6 @@ describe("quote.vue", () => { vehicle: { cardId: "123", }, - experimentSettings: { - settingName: "SERVICE_PACKAGE_DISCOUNT", - }, }; // Set up the component 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 b3e4742e7..f87faeec8 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 @@ -31,14 +31,10 @@ const mockExperimentSettings = experimentSettings; jest.mock("@/mixins/experiment-mixin.js", () => ({ methods: { getSettingValue(settingName) { - if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) { - return true; - } + return false; }, hasSetting(settingName) { - if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) { - return false; - } + return false; }, hasSettingEqualTo(settingName, settingValue) { return false; diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index a3b599331..781c0cb34 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -807,9 +807,7 @@ export default { coupons = promoString; } const additionalDiscounts = submittedOrder.lineItems?.supportingItems?.find( - (x) => - x.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT || - x.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT + (x) => x.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT ); if (isDefined(additionalDiscounts)) { if (coupons) { From c3e646b000a139c71c31f2e0bcb572e9f8c8fd7d Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 18 Mar 2026 10:16:17 -0400 Subject: [PATCH 07/25] CASH-2472 - Enabled custom exposure logging for Multi Location Pop up --- src/layouts/schedule/schedule.vue | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c13026d36..ec8051cc4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2054,14 +2054,7 @@ export default { } }, async showMultiLocationModal() { - const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( - experimentSettings.SHOW_MULTI_LOCATION_APPT - ); - - if ( - !this.selectedRouteCodeData?.routeCode && - showMultiLocationAppointment?.toLowerCase() === "true" - ) { + if (!this.selectedRouteCodeData?.routeCode) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION ); From 00afde2c6a146555afe780c5378e589fb04db820 Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 18 Mar 2026 10:50:35 -0400 Subject: [PATCH 08/25] CASH-2472 - Reverted the change as it got merged to develop directly --- src/layouts/schedule/schedule.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ec8051cc4..c13026d36 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2054,7 +2054,14 @@ export default { } }, async showMultiLocationModal() { - if (!this.selectedRouteCodeData?.routeCode) { + const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( + experimentSettings.SHOW_MULTI_LOCATION_APPT + ); + + if ( + !this.selectedRouteCodeData?.routeCode && + showMultiLocationAppointment?.toLowerCase() === "true" + ) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION ); From f80e85e2d89d1953c4965ce794faea988615538a Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 18 Mar 2026 11:05:39 -0400 Subject: [PATCH 09/25] CASH-2472 - Update Custom Exposure Logging for MultiLocationPopup --- src/layouts/schedule/schedule.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c13026d36..e69dab9f4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2054,13 +2054,8 @@ export default { } }, async showMultiLocationModal() { - const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( - experimentSettings.SHOW_MULTI_LOCATION_APPT - ); - if ( - !this.selectedRouteCodeData?.routeCode && - showMultiLocationAppointment?.toLowerCase() === "true" + !this.selectedRouteCodeData?.routeCode ) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION From 335fe7576839629dea8e743e64917868989f1513 Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 18 Mar 2026 11:12:17 -0400 Subject: [PATCH 10/25] CASH-2472 - Fixed formatting --- src/layouts/schedule/schedule.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e69dab9f4..ec8051cc4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2054,9 +2054,7 @@ export default { } }, async showMultiLocationModal() { - if ( - !this.selectedRouteCodeData?.routeCode - ) { + if (!this.selectedRouteCodeData?.routeCode) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION ); From 0a9e7c789c1be0add75ffc4af97b29a7a39d7671 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 18 Mar 2026 14:33:26 -0400 Subject: [PATCH 11/25] Update locators for content homepage --- playwright-tests/pages/HomePage.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/playwright-tests/pages/HomePage.ts b/playwright-tests/pages/HomePage.ts index e2db5ac26..d88c27fe5 100644 --- a/playwright-tests/pages/HomePage.ts +++ b/playwright-tests/pages/HomePage.ts @@ -20,8 +20,7 @@ export class HomePage extends BasePage { readonly paymentOptionDropdown: Locator; readonly viewQuoteButton: Locator; - //Zip Entry - readonly enterServiceZipTextBox: Locator; + //Zip Entry readonly zipEntryLetsGetStartedButton: Locator; readonly getQuoteAndScheduleButton: Locator; @@ -31,7 +30,7 @@ export class HomePage extends BasePage { super(page); this.page = page; - this.letsGetStartedButton = this.page.locator('#zipCodeTextboxButton'); + this.letsGetStartedButton = this.page.getByRole('button', { name: 'Let\'s get started' }); this.cusmodalPopup = this.page.locator('#Cusmodalpopup'); this.closePopupButton = this.page.getByRole('button', { name: '×' }); @@ -47,8 +46,7 @@ export class HomePage extends BasePage { this.viewQuoteButton = this.page.locator('#ctaSubmit'); //Zip Entry - this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox'); - this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextbox'); + this.zipEntryLetsGetStartedButton = this.page.getByRole('textbox', { name: 'Enter service ZIP code' }); this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' }); } From 48ad72d996b2d3a853b058df4411d59257c190e0 Mon Sep 17 00:00:00 2001 From: JennyNou Date: Wed, 18 Mar 2026 14:38:29 -0400 Subject: [PATCH 12/25] Rename locator for readability --- playwright-tests/pages/HomePage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playwright-tests/pages/HomePage.ts b/playwright-tests/pages/HomePage.ts index d88c27fe5..52ceca875 100644 --- a/playwright-tests/pages/HomePage.ts +++ b/playwright-tests/pages/HomePage.ts @@ -21,7 +21,7 @@ export class HomePage extends BasePage { readonly viewQuoteButton: Locator; //Zip Entry - readonly zipEntryLetsGetStartedButton: Locator; + readonly widgetZipEntryField: Locator; readonly getQuoteAndScheduleButton: Locator; url = process.env['BASE_URL']!; @@ -46,7 +46,7 @@ export class HomePage extends BasePage { this.viewQuoteButton = this.page.locator('#ctaSubmit'); //Zip Entry - this.zipEntryLetsGetStartedButton = this.page.getByRole('textbox', { name: 'Enter service ZIP code' }); + this.widgetZipEntryField = this.page.getByRole('textbox', { name: 'Enter service ZIP code' }); this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' }); } @@ -61,7 +61,7 @@ export class HomePage extends BasePage { async letsGetStarted(zip: string, enterFunnelWithZip: boolean) { if (enterFunnelWithZip) { - await this.zipEntryLetsGetStartedButton.fill(zip); + await this.widgetZipEntryField.fill(zip); /* await this.letsGetStartedButton.evaluate((element, zip) => { const currentHref = element.getAttribute('href') || ''; element.setAttribute('href', `${currentHref}?zipCode=${zip}`); From 80e77bc18461015ccc5297bce5d25b7186530892 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 19 Mar 2026 12:01:00 -0400 Subject: [PATCH 13/25] Revert "Reapply "Merge pull request #3075 from Safelite/feature/CASH-1684"" This reverts commit c9e5ccff54ad208fdbdb7cd695448bf8a36f4f25. --- src/assets/img/icons/alert-circle-yellow.svg | 3 -- src/helpers/cms-content-helper.js | 11 ----- src/layouts/schedule/schedule.vue | 42 ++------------------ src/ux-components/alert/alert.vue | 21 ---------- 4 files changed, 4 insertions(+), 73 deletions(-) delete mode 100644 src/assets/img/icons/alert-circle-yellow.svg diff --git a/src/assets/img/icons/alert-circle-yellow.svg b/src/assets/img/icons/alert-circle-yellow.svg deleted file mode 100644 index 904aefb98..000000000 --- a/src/assets/img/icons/alert-circle-yellow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 450875d0f..727add296 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) { export function splitCMSCopyOnBR(copy) { return copy.split("
"); } - -// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not. -export function formatToUSDollar(amount) { - const isInteger = amount % 1 === 0; - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: isInteger ? 0 : 2, - maximumFractionDigits: 2, - }).format(amount); -} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ec8051cc4..d33695833 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -81,16 +81,6 @@ cmsWidgetName="AlertNoShopsWidget" v-if="displayNoShopsAlert" alertClass="alert-warning" /> -

Info Icon - Warning Icon {{ alertHeadline }}


@@ -69,9 +63,7 @@ export default { name: "alert", props: { showInfoIcon: Boolean, - showWarningIcon: Boolean, showHorizontalRow: Boolean, - hasBorder: Boolean, isDismissible: Boolean, alertClass: String, /* @@ -104,9 +96,6 @@ export default { infoIcon() { return require(`@/assets/img/icons/info-circle-blue.svg`); }, - warningIcon() { - return require(`@/assets/img/icons/alert-circle-yellow.svg`); - }, pageQueryString() { return applicationConfig.PAGE_QUERYSTRING; }, @@ -222,19 +211,12 @@ export default { background-color: $yellow-100; .alert-heading { color: $yellow-600; - - .warning-icon { - height: 1rem; - } } svg { fill: $yellow-600; width: 1rem; height: 1rem; } - hr { - border-color: $yellow-800; - } } &.alert-success { background-color: $green-100; @@ -247,9 +229,6 @@ export default { height: 1rem; } } - &.bordered { - border: 1px solid; - } & p { font-size: 0.875rem; } From fe05b61fa635980a8543bbf5b63c3a46a68da35e Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 19 Mar 2026 13:53:25 -0400 Subject: [PATCH 14/25] Change idempotency to be cached with invalidating fields. --- src/constants/store-actions.js | 2 + src/constants/store-mutations.js | 1 + src/layouts/payment-adyen/payment-adyen.vue | 20 ++-- src/store/index.js | 103 +++++++++++++++++++- 4 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 3afd4df24..26bcc25e1 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -123,6 +123,8 @@ const storeActions = { SAVE_LOGGING_OPTION: "saveLoggingOption", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", + GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey", + CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 9b4b3719c..5c6c2e22d 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -98,6 +98,7 @@ const storeMutations = { UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LOGGING_OPTION: "updateLoggingOption", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", + UPDATE_IDEMPOTENCY_KEY: "updateIdempotencyKey", // EXPERIMENT MUTATIONS UPDATE_EXPERIMENTS: "updateExperiments", diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index adb0a1210..4a4936a7f 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -163,7 +163,7 @@ export default { console.log(`Price = ${this.amountDue}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`); - const requestBody = this.getAdyenInitRequestInfo(); + const requestBody = await this.getAdyenInitRequestInfo(); console.log(`Calling with:`); console.log(requestBody); @@ -257,6 +257,7 @@ export default { this.dropinComponent = dropin; dropin.mount("#adyen-container"); + await this.dispatchStoreAction(storeActions.CORRECT_IDEMPOTENCY_KEY_EXPIRY, expiryTime); }, async handleCompletedPayment(result) { @@ -438,19 +439,13 @@ export default { }, // Now a Method so it is always freshly called and not cached. - getIdempotencyKey() { - const currentDateTime = new Date(); - const currentHour = currentDateTime.getUTCHours(); - const currentDate = currentDateTime.getUTCDate(); - const id = this?.$store?.getters?.order?.referralCorrelationId; - const system = this.sourceSystem; - const total = this.adyenPriceTotal; - - return `${id}-${system}-${currentDate}-${currentHour}-${total}`; + async getIdempotencyKey() { + return await this.dispatchStoreAction(storeActions.GET_VALID_IDEMPOTENCY_KEY); }, // Now a Method so it is always freshly called and not cached. - getAdyenInitRequestInfo() { + async getAdyenInitRequestInfo() { + const key = await this.getIdempotencyKey(); return { sourceSystem: this.sourceSystem, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, @@ -464,10 +459,9 @@ export default { stateOrProvince: this.locationInfo.state, 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.getIdempotencyKey(), + idempotencyKey: key, }; }, }, diff --git a/src/store/index.js b/src/store/index.js index 10218892c..e740bf0d3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -59,7 +59,7 @@ import { } from "@/helpers/recal-helper"; import { externalParameterStatus } from "@/constants/external-parameters"; import { experimentSettings } from "@/constants/experiments"; -import { addPricesToLineItems } from "@/helpers/pricing-helper"; +import { addPricesToLineItems, getAmountDue } from "@/helpers/pricing-helper"; // Export State const getDefaultState = () => { @@ -207,6 +207,12 @@ const getDefaultState = () => { loggingOption: false, hasAlreadyTriggeredError: false, }, + idempotencyKeyFields: { + referralCorrelationId: null, + totalInCents: 0, + expiryTime: null, + idempotencyKey: null, + }, }; }; @@ -862,6 +868,15 @@ export const mutations = { updateHasTriggeredError(state, hasTriggeredError) { state.applicationUser.hasAlreadyTriggeredError = hasTriggeredError; }, + updateIdempotencyKey( + state, + { idempotencyKey, referralCorrelationId, totalInCents, expiryTime } + ) { + state.idempotencyKeyFields.idempotencyKey = idempotencyKey; + state.idempotencyKeyFields.referralCorrelationId = referralCorrelationId; + state.idempotencyKeyFields.totalInCents = totalInCents; + state.idempotencyKeyFields.expiryTime = expiryTime; + }, }; // Export Getters @@ -1105,6 +1120,35 @@ export const getters = { state.order.customer.emailAddress ); }, + + isIdempotencyKeyValid: (state) => { + if (!state.idempotencyKeyFields?.idempotencyKey) { + return false; + } + + const referralCorrelationId = state.order?.referralCorrelationId; + if ( + !referralCorrelationId || + referralCorrelationId !== state.idempotencyKeyFields?.referralCorrelationId + ) { + return false; + } + + const total = getAmountDue(state.order?.lineItems); + const totalInCents = Math.round(total * 100); + if (totalInCents !== state.idempotencyKeyFields?.totalInCents) { + return false; + } + + const currentTime = new Date(); + const expiryTime = new Date(state.idempotencyKeyFields.expiryTime); + if (!expiryTime || expiryTime.getTime() < currentTime.getTime()) { + return false; + } + + // If all checks pass, then the key is valid. + return true; + }, }; // Export Actions @@ -3665,6 +3709,63 @@ export const actions = { updateHasTriggeredError(context, hasAlreadyTriggeredError) { context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, hasAlreadyTriggeredError); }, + + getValidIdempotencyKey(context) { + if (context.getters.isIdempotencyKeyValid) { + return context.state.idempotencyKeyFields.idempotencyKey; + } + + // If invalid, need to regenerate. + const referralCorrelationId = context.state.order?.referralCorrelationId; + if (!referralCorrelationId) { + // Can't generate! + return null; + } + + const total = getAmountDue(context.state.order?.lineItems); + const totalInCents = Math.round(total * 100); + if (Number.isNaN(totalInCents)) { + // Can't generate! + return null; + } + + const currentTime = new Date(); + const nextHour = currentTime.getUTCHours() + 1; + currentTime.setUTCHours(nextHour); + + const newId = crypto.randomUUID(); + + const newKeyInfo = { + idempotencyKey: newId, + referralCorrelationId: referralCorrelationId, + totalInCents: totalInCents, + expiryTime: currentTime, + }; + + context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKeyInfo); + + return newId; + }, + + correctIdempotencyKeyExpiry(context, expiryTime) { + if (!context.getters.isIdempotencyKeyValid) { + return; + } + + const currentExpiryTime = new Date(context.state.idempotencyKeyFields.expiryTime); + const expiryTimeAsDate = new Date(expiryTime); + + if (expiryTimeAsDate.getTime() < currentExpiryTime.getTime()) { + const existingKey = context.state.idempotencyKeyFields; + const newKey = { + referralCorrelationId: existingKey.referralCorrelationId, + totalInCents: existingKey.totalInCents, + idempotencyKey: existingKey.idempotencyKey, + expiryTime: expiryTimeAsDate, + }; + context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey); + } + }, }; export default createStore({ From 58dcd307603b55a63d9357c7bdb8484e632a1227 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 24 Mar 2026 11:34:56 -0400 Subject: [PATCH 15/25] CASH-2433: Update event label --- src/constants/analytics.js | 1 + src/layouts/quote/quote.vue | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 51b0c24b6..ff93e935a 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -43,6 +43,7 @@ const GaLabels = { ADDRESS_LOOKUP: "Address_Look_up", YES: "yes", NO: "no", + GLASS_CASH_QUOTE: "GlassCashQuote", }; const ValueToLogTypes = { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 82249e790..b428ab905 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -190,6 +190,7 @@ import { } from "@/helpers/page-prerequisites-helper.js"; import { savePageData } from "@/router/methods/helpers/save-page-data"; import { quotePageDiscountTable } from "../../constants/quote-page-discounts"; +import { GaLabels } from "@/constants/analytics"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); @@ -964,8 +965,7 @@ export default { priceLabel = price.toFixed(2); } } - const subTotalLabel = this.Variables?.CASH_SUBTOTAL; - this.pushVariableToDataLayer?.({ [subTotalLabel]: priceLabel }); + this.pushVariableToDataLayer?.({ [this.GaLabels.GLASS_CASH_QUOTE]: priceLabel }); }); }, async skip(insuranceSelection, packageSelection) { From 1e59a924cc61b4e0716af8984c1cdbe37478512e Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Tue, 24 Mar 2026 13:45:04 -0400 Subject: [PATCH 16/25] Reapply "Reapply "Merge pull request #3075 from Safelite/feature/CASH-1684"" This reverts commit 80e77bc18461015ccc5297bce5d25b7186530892. --- src/assets/img/icons/alert-circle-yellow.svg | 3 ++ src/helpers/cms-content-helper.js | 11 +++++ src/layouts/schedule/schedule.vue | 42 ++++++++++++++++++-- src/ux-components/alert/alert.vue | 21 ++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 src/assets/img/icons/alert-circle-yellow.svg diff --git a/src/assets/img/icons/alert-circle-yellow.svg b/src/assets/img/icons/alert-circle-yellow.svg new file mode 100644 index 000000000..904aefb98 --- /dev/null +++ b/src/assets/img/icons/alert-circle-yellow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 727add296..450875d0f 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -343,3 +343,14 @@ export function splitCMSCopyOnParagraphTag(copy) { export function splitCMSCopyOnBR(copy) { return copy.split("
"); } + +// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not. +export function formatToUSDollar(amount) { + const isInteger = amount % 1 === 0; + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: isInteger ? 0 : 2, + maximumFractionDigits: 2, + }).format(amount); +} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index d33695833..ec8051cc4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -81,6 +81,16 @@ cmsWidgetName="AlertNoShopsWidget" v-if="displayNoShopsAlert" alertClass="alert-warning" /> +

Info Icon + Warning Icon {{ alertHeadline }}


@@ -63,7 +69,9 @@ export default { name: "alert", props: { showInfoIcon: Boolean, + showWarningIcon: Boolean, showHorizontalRow: Boolean, + hasBorder: Boolean, isDismissible: Boolean, alertClass: String, /* @@ -96,6 +104,9 @@ export default { infoIcon() { return require(`@/assets/img/icons/info-circle-blue.svg`); }, + warningIcon() { + return require(`@/assets/img/icons/alert-circle-yellow.svg`); + }, pageQueryString() { return applicationConfig.PAGE_QUERYSTRING; }, @@ -211,12 +222,19 @@ export default { background-color: $yellow-100; .alert-heading { color: $yellow-600; + + .warning-icon { + height: 1rem; + } } svg { fill: $yellow-600; width: 1rem; height: 1rem; } + hr { + border-color: $yellow-800; + } } &.alert-success { background-color: $green-100; @@ -229,6 +247,9 @@ export default { height: 1rem; } } + &.bordered { + border: 1px solid; + } & p { font-size: 0.875rem; } From 22cba2d54b435a9d1f575a82d18da39db201624f Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 25 Mar 2026 10:57:59 -0400 Subject: [PATCH 17/25] CASH-2481 - when navigating forward, skip service-zip page if zip exists and is valid --- src/layouts/estimate/estimate.vue | 3 ++- src/layouts/service-zip/service-zip.vue | 8 ++++++++ src/layouts/vehicle-damage/vehicle-damage.vue | 5 +++-- src/layouts/vehicle/vehicle.vue | 10 ++++++++-- .../methods/helpers/initialize-from-querystrings.js | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/layouts/estimate/estimate.vue b/src/layouts/estimate/estimate.vue index 60d050d73..36e27127a 100644 --- a/src/layouts/estimate/estimate.vue +++ b/src/layouts/estimate/estimate.vue @@ -62,6 +62,7 @@ import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; import { nextTick } from "vue"; import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -169,7 +170,7 @@ export default { baseMixin.methods.ResetExternalParamsAndHideModal(); } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } } else { baseMixin.methods.ResetExternalParamsAndHideModal(); diff --git a/src/layouts/service-zip/service-zip.vue b/src/layouts/service-zip/service-zip.vue index ab99adc6f..c82a0a0e3 100644 --- a/src/layouts/service-zip/service-zip.vue +++ b/src/layouts/service-zip/service-zip.vue @@ -80,6 +80,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import baseMixin from "@/mixins/base-mixin.js"; import { queryStrings } from "@/constants/query-strings"; import { routeData } from "@/router/constants/routes"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; // Define Validation Rules defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -137,6 +138,13 @@ export default { }, false ); + vm.serviceZipCode = store.getters.externalParameterServiceZip.zipCode; + const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); + if (isValid) { + showFmgLoadingModal(true); + vm.forwardButtonAction(); + return; + } } if (store.getters.externalParameterCustomer?.phoneNumber) { diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index 5e128e867..abad0da7d 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -93,6 +93,7 @@ import { errorMessages } from "@/constants/error-messages"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { debugLog } from "@/helpers/debug-log-helper"; import store from "@/store"; @@ -199,7 +200,7 @@ export default { return vm.forwardButtonAction(); } } - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); //clear any validation errors for external parameter flow const form = vm.$refs.theForm; @@ -209,7 +210,7 @@ export default { } } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } }); }, diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index 4521a6e2f..92a7aa810 100644 --- a/src/layouts/vehicle/vehicle.vue +++ b/src/layouts/vehicle/vehicle.vue @@ -109,6 +109,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { applicationConfig } from "../../constants/application-config"; import { queryStrings } from "@/constants/query-strings"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { showFmgLoadingModal } from "@/helpers/loading-modal-helper"; import { debugLog } from "@/helpers/debug-log-helper"; //define validation rules @@ -291,7 +292,12 @@ export default { resultMap.modelQuestionInitialData, resultMap.styleQuestionInitialData ); - if (store.getters.externalParameterState?.isExternalParameter) { + if ( + store.getters.externalParameterState?.isExternalParameter && + store.getters.externalParameterVehicle.year && + store.getters.externalParameterVehicle.make && + store.getters.externalParameterVehicle.model + ) { await vm.getVehicleDetails(); if (!vm.displayNoServiceAlert) { const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); @@ -304,7 +310,7 @@ export default { baseMixin.methods.ResetExternalParamsAndHideModal(); } } else { - baseMixin.methods.ResetExternalParamsAndHideModal(); + showFmgLoadingModal(false); } }); }, diff --git a/src/router/methods/helpers/initialize-from-querystrings.js b/src/router/methods/helpers/initialize-from-querystrings.js index 8dff4423d..6b6cecff2 100644 --- a/src/router/methods/helpers/initialize-from-querystrings.js +++ b/src/router/methods/helpers/initialize-from-querystrings.js @@ -95,6 +95,7 @@ function updateExternalParameterState() { } if (externalParameterZipCode) { store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_ZIP_CODE, externalParameterZipCode); + store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, externalParameterStatus.ACTIVE); } if (externalParameterEmail) { store.commit( From c8c3b406b4faee3457e29a4aacbbfdb63e6c0f6e Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 25 Mar 2026 11:04:39 -0400 Subject: [PATCH 18/25] CASH-2481 - update unit test --- src/layouts/vehicle/vehicle.spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layouts/vehicle/vehicle.spec.js b/src/layouts/vehicle/vehicle.spec.js index d4bc1f726..ba1c4c621 100644 --- a/src/layouts/vehicle/vehicle.spec.js +++ b/src/layouts/vehicle/vehicle.spec.js @@ -93,8 +93,11 @@ describe("vehicle.vue", () => { }); describe("vehicle.vue", () => { - test("should call forwardButtonAction if isExternalParameter is true and form is valid", async () => { + test("should call forwardButtonAction if isExternalParameter is true, has a year, make and model, and form is valid", async () => { store.getters.externalParameterState.isExternalParameter = true; + store.getters.externalParameterVehicle.year = "1886"; + store.getters.externalParameterVehicle.make = "Benz"; + store.getters.externalParameterVehicle.model = "Patent-Motorwagen"; // Create a shallow mount of MyComponent const { wrapper } = setupMocks(); // Set displayNoServiceAlert to false From d1ebb59cbb1132870138e42458df7c31d364fb87 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Thu, 26 Mar 2026 13:55:27 -0400 Subject: [PATCH 19/25] CASH-2497 log full querystring on init session CASH-2497 log full querystring on init session --- src/constants/endpoints.js | 4 -- src/constants/store-actions.js | 1 - src/global-methods.js | 1 - src/mixins/analytics-mixin.js | 73 ++++--------------------- src/router/methods/after-each.js | 3 - src/router/methods/route-logic/error.js | 2 - src/store/index.js | 32 +---------- 7 files changed, 11 insertions(+), 105 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index d25fce6a3..1a4cc4677 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -180,10 +180,6 @@ const endpoints = { url: "/analytics/api/v1/analytics/log-part-questions", method: "POST", }, - LogDigitalConsumer: { - url: "/analytics/api/v1/analytics/digitalconsumer-log", - method: "POST", - }, LogFmgSessionData: { url: "/analytics/api/v1/analytics/digitalconsumer-session-logging", method: "POST", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 26bcc25e1..04d51056a 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -63,7 +63,6 @@ const storeActions = { LOG_CUSTOM_EVENT: "logCustomEvent", INITIALIZE_SESSION: "initializeSession", LOG_PART_QUESTIONS: "logPartQuestions", - LOG_DIGITALCONSUMER: "logDigitalConsumer", LOG_FMG_SESSION_DATA: "logFmgSessionData", // DEPENDENCY MUTATIONS diff --git a/src/global-methods.js b/src/global-methods.js index 4f1dc601b..f408230f8 100644 --- a/src/global-methods.js +++ b/src/global-methods.js @@ -102,7 +102,6 @@ export default { }, (error) => { if ( - endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url) || endpoint.toLowerCase().includes(endpoints.LogFmgSessionData.url) || endpoint.toLowerCase().includes(endpoints.LogPageView.url) || endpoint.toLowerCase().includes(endpoints.LogCustomEvent.url) || diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 781c0cb34..9386846d7 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -132,51 +132,6 @@ export default { } }, - async logDigitalConsumer() { - const currentPageName = getPageNameFromRouter(); - const universes = store.getters.applicationUser.experiments; - - const variationNames = universes - .filter((item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL) - .map((item) => item.variationName) - .filter(Boolean); // removes undefined/null - - const conceptVariation = variationNames.length > 0 ? variationNames[0] : ""; - - const isConceptExposed = universes.find( - (item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL - )?.isExposed; - - const submittedOrder = baseMixin.methods.getSubmittedOrder(); - const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); - const hasSubmittedOrderAtConfirmationPage = - hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name; - - var payload = { - actionName: `Browser page:${currentPageName}`, - referralSequenceNumber: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.referralSequenceNumber - : store.getters.order.referralSequenceNumber, - referralNumber: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.referralNumber - : store.getters.order.referralNumber, - workOrderId: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.workOrderId - : store.getters.order.workOrderId, - workOrderNumber: hasSubmittedOrderAtConfirmationPage - ? submittedOrder.workOrderNumber - : store.getters.order.workOrderNumber, - conceptVariation: conceptVariation, - isConceptExposed: isConceptExposed, - }; - - await baseMixin.methods.dispatchStoreAction( - storeActions.LOG_DIGITALCONSUMER, - payload, - false - ); - }, - async pushEventForChatsToGA(category, action, label, pushToLogApp = false) { const currentPageName = getPageNameFromRouter(); const value = `2.0_${currentPageName}`; @@ -206,6 +161,11 @@ export default { // S3 bucket, safelite-dev-digitalconsumer-session-data-us-east-2/1. // This bucket data is then picked up by snowflake for analytics use. async pushFmgSessionData() { + var currentPageName = getPageNameFromRouter(true); + if (!currentPageName) { + return; + } + const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.getters.order; @@ -247,22 +207,6 @@ export default { } var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`; - var currentPageName = getPageNameFromRouter(); - - // add query strings to the page name for debugging. on the vehicle page, if from an external link, pull it from the stash - if (currentPageName === "vehicle") { - if (!window.location.search) { - if (store.getters.externalParameterState?.qsStash) { - currentPageName += `${store.getters.externalParameterState.qsStash}`; - } - } else { - currentPageName += `${window.location.search}`; - } - } else { - if (window.location.search) { - currentPageName += `${window.location.search}`; - } - } var sessionData = {}; sessionData.currentPage = currentPageName; @@ -911,6 +855,9 @@ export default { setSessionIdIfUnset(response.data.sessionId); } } + + // logging on session init so we capture complete query string data + await this.pushFmgSessionData(); }, noSession() { @@ -977,7 +924,7 @@ function pushToDataLayerIfDefined(data) { } } -function getPageNameFromRouter() { +function getPageNameFromRouter(useDefaultUrl = false) { if ( router && router.currentRoute && @@ -987,7 +934,7 @@ function getPageNameFromRouter() { return router.currentRoute.value.name; } - return window.location.href.replace(/\/$/, "").split("/").pop(); + return useDefaultUrl ? window.location.search : window.location.href.replace(/\/$/, "").split("/").pop(); } function getValueToLog(value, valueToLogType) { diff --git a/src/router/methods/after-each.js b/src/router/methods/after-each.js index 608119399..43ed0d4b5 100644 --- a/src/router/methods/after-each.js +++ b/src/router/methods/after-each.js @@ -5,9 +5,6 @@ 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(); - // digital consumer fmg session logging to snowflake analyticsMixin.methods.pushFmgSessionData(); diff --git a/src/router/methods/route-logic/error.js b/src/router/methods/route-logic/error.js index fb9dd2cbc..162e5b914 100644 --- a/src/router/methods/route-logic/error.js +++ b/src/router/methods/route-logic/error.js @@ -19,7 +19,6 @@ export async function errorBeforeEnter(to, from) { nextPage: to?.name, }; - analyticsMixin.methods.logDigitalConsumer(); await handleHardError(errorPayload); return; } @@ -33,7 +32,6 @@ export async function errorBeforeEnter(to, from) { nextPage: to?.name, }; - analyticsMixin.methods.logDigitalConsumer(); await handleHardError(errorPayload); return; } else { diff --git a/src/store/index.js b/src/store/index.js index e740bf0d3..8a3628164 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1630,37 +1630,6 @@ export const actions = { }); }, - logDigitalConsumer( - context, - { - actionName, - referralSequenceNumber, - referralNumber, - workOrderId, - workOrderNumber, - conceptVariation, - isConceptExposed, - } - ) { - var payload = { - sessionId: getSessionIdValue(), - deviceId: getDeviceIdValue(), - actionName: actionName ?? "", - referralSequenceNumber: referralSequenceNumber ?? "", - referralNumber: referralNumber ?? "", - applicationName: baseMixin.methods.isMobileDevice() ? "2.0 Mobile" : "2.0", - workOrderId: workOrderId ?? "", - workOrderNumber: workOrderNumber ?? "", - conceptVariation: conceptVariation, - isConceptExposed: isConceptExposed, - }; - - return globalMethods.callHttpClient({ - method: endpoints.LogDigitalConsumer.method, - endpoint: endpoints.LogDigitalConsumer.url, - payload: payload, - }); - }, logFmgSessionData( context, @@ -1770,6 +1739,7 @@ export const actions = { method: endpoints.LogFmgSessionData.method, endpoint: endpoints.LogFmgSessionData.url, payload: payload, + logApiCall: false, }); }, From 38d50f6612a90ba926f32dd38ad68a97e7dcc4c5 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 26 Mar 2026 15:39:40 -0400 Subject: [PATCH 20/25] CASH-1689: MSR fee in cart for insurance --- src/constants/store-actions.js | 1 + src/constants/store-mutations.js | 1 + src/fmg-components/cart/cart.vue | 108 ++++++++++++- .../remove-msr-fee-modal.vue | 153 ++++++++++++++++++ .../content-group-modal.vue | 4 + src/helpers/pricing-helper.js | 19 +++ src/layouts/confirmation/confirmation.vue | 7 +- src/layouts/payment-adyen/payment-adyen.vue | 1 + src/layouts/payment-method/payment-method.vue | 16 +- src/layouts/payment/payment.vue | 1 + src/layouts/schedule/schedule.vue | 8 + src/router/constants/navigation-scenarios.js | 1 + src/router/constants/routing-table.js | 4 + src/store/index.js | 12 ++ 14 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 26bcc25e1..4590249f3 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -114,6 +114,7 @@ const storeActions = { SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling", SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable", + SAVE_IS_MSR_FEE_COVERED_BY_INSURANCE: "saveIsMSRFeeCoveredByInsurance", CREATE_SUBMITTED_STATE: "createSubmittedState", RESET_SUBMITTED_STATE: "resetSubmittedState", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 5c6c2e22d..b1344dcb5 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -72,6 +72,7 @@ const storeMutations = { UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling", UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", + UPDATE_IS_MSR_FEE_COVERED_BY_INSURANCE: "updateIsMSRFeeCoveredByInsurance", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", // EVENT BUS MUTATIONS diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index e890e53cc..7de87b678 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -77,7 +77,7 @@ i % 2 == 0 ? 'even' : 'odd', cartItem.isRemovable ? 'removable-cart-item' : '', ]"> - {{ cartItem.name }} + {{ cartItem.name }} + + {{ msrFeeCartItem.name }} + + Info Icon + + +
+
@@ -162,6 +179,7 @@ import textLink from "@/ux-components/text-link/text-link"; import textBlock from "@/digital-components/text-block/text-block"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question"; +import removeMsrFeeModal from "./remove-msr-fee-modal/remove-msr-fee-modal.vue"; // Mixins import baseMixin from "@/mixins/base-mixin.js"; @@ -183,6 +201,7 @@ import { getSalesTax, getAmountDueWithDonation, } from "@/helpers/pricing-helper.js"; +import { formatToUSDollar } from "@/helpers/cms-content-helper"; // Constants import { partTypeStrings } from "@/constants/part-type-strings"; @@ -191,6 +210,7 @@ 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"; +import { partNumberStrings } from "@/constants/part-number-strings"; export default { name: "cart", @@ -203,6 +223,7 @@ export default { pageName: String, allowItemRemoval: Boolean, recyclingModalCmsWidgetName: String, + msrModalCmsWidgetName: String, showAsPaid: Boolean, isInsurance: Boolean, insuranceDeductible: Number, @@ -214,6 +235,7 @@ export default { isExpandedOnLoad: Boolean, isCollapsible: { type: Boolean, default: true }, isMSRFeeApplicable: Boolean, + IsMSRFeeCoveredByInsurance: Boolean, donationCartItem: Object, }, data() { @@ -311,6 +333,10 @@ export default { }, async removeItem(cartItemType, category) { + if (cartItemType == cartItemTypes.MOBILE_FEE && this.isMSRFeeApplicable) { + this.$refs.removeMsrFeeModal.openModal(); + return false; + } this.lineItems[category] = this.lineItems[category].filter( (lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType ); @@ -375,6 +401,12 @@ export default { } return []; }, + shouldShowInfoIcon(cartItem) { + return cartItem == this.recycleFeeCartItem || cartItem == this.msrFeeCartItem; + }, + switchToInshop() { + this.$emit("switchToInshop"); + }, }, computed: { isCartReadyToLoad() { @@ -490,6 +522,10 @@ export default { cartItems.push(this.mobileFeeCartItem); } + if (this.msrFeeCartItem) { + cartItems.push(this.msrFeeCartItem); + } + if (this.quotePageDiscountCartItem) { cartItems.push(this.quotePageDiscountCartItem); } @@ -892,7 +928,9 @@ export default { let cartItem = null; let showMobileFeeCartItem = false; const mobileFeeLineItem = this.supportingItems.find( - (lineItem) => lineItem.partType == partTypeStrings.MOBILE_FEE + (lineItem) => + lineItem.partType == partTypeStrings.MOBILE_FEE && + lineItem.partNumber == partTypeStrings.MOBILE_FEE ); var mobileTotal = @@ -929,6 +967,67 @@ export default { } return cartItem; }, + msrFeeCartItemName() { + return this.getCmsContent("MSRFeeTextWidget", "Text"); + }, + getMsrFeeLineItem() { + return this.supportingItems.find( + (lineItem) => + lineItem.partType == partTypeStrings.MOBILE_FEE && + lineItem.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE + ); + }, + msrFeeCartItem() { + let cartItem = null; + const msrFeeLineItem = this.getMsrFeeLineItem; + const shouldCreateMsrFeeCartItem = + this.isInsurance && + !this.IsMSRFeeCoveredByInsurance && + msrFeeLineItem && + this.msrFeeAmount > 0; + + if (shouldCreateMsrFeeCartItem) { + cartItem = { + name: this.msrFeeCartItemName, + category: cartItemCategories.SUPPORTING_ITEMS, + cartItemType: cartItemTypes.MOBILE_FEE, + isDisplayed: this.isInsurance, + isRemovable: true, + subTotal: 0, + salesTax: 0, + lineItems: [], + isCoveredByInsurance: this.IsMSRFeeCoveredByInsurance, + }; + msrFeeLineItem.cartItemType = cartItem.cartItemType; + cartItem.lineItems.push(msrFeeLineItem); + cartItem.subTotal += this.msrFeeAmount; + cartItem.salesTax += msrFeeLineItem.salesTax ?? 0; + } + return cartItem; + }, + msrFeeAmount() { + const msrFeeLineItem = this.getMsrFeeLineItem; + if (!msrFeeLineItem) { + return 0; + } + return ( + msrFeeLineItem.laborAmount + msrFeeLineItem.sellingPrice + msrFeeLineItem.kitPrice + ); + }, + msrModalTextBlock() { + let cmsContentText = this.getCmsContent("MSRModal", "BodyText"); + if (cmsContentText) { + cmsContentText = cmsContentText.replaceAll( + "{custom:mobileFee}", + formatToUSDollar(this.msrFeeAmount) + ); + cmsContentText = cmsContentText.replaceAll( + "{custom:submittedOrder.policy.insuranceCompanyName}", + this.insuranceCompanyName + ); + } + return cmsContentText; + }, quotePageDiscountCartItemName() { return this.getCmsContent("ServicePackageDiscountTextWidget", "Text"); }, @@ -1189,6 +1288,7 @@ export default { textLink, contentGroupModal, promoModalQuestion, + removeMsrFeeModal, }, }; @@ -1289,6 +1389,10 @@ export default { order: 3; margin-right: 100%; text-decoration: none; + + &#msr-fee-cartItem { + margin: 0 0 0 0.2rem; + } } } .package-type, diff --git a/src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue b/src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue new file mode 100644 index 000000000..6c2cd1fb6 --- /dev/null +++ b/src/fmg-components/cart/remove-msr-fee-modal/remove-msr-fee-modal.vue @@ -0,0 +1,153 @@ + + + + diff --git a/src/fmg-components/content-group-modal/content-group-modal.vue b/src/fmg-components/content-group-modal/content-group-modal.vue index 25c50b2df..e4a5241c4 100644 --- a/src/fmg-components/content-group-modal/content-group-modal.vue +++ b/src/fmg-components/content-group-modal/content-group-modal.vue @@ -26,6 +26,7 @@ export default { name: "content-group-modal", props: { cmsWidgetName: String, + customBodyText: String, footerButtonActionName: { type: String, default: null, @@ -43,6 +44,9 @@ export default { return this.getCmsContent(this.cmsWidgetName, "SubheaderText"); }, ModalBodyText() { + if (this.customBodyText) { + return this.customBodyText; + } return this.getCmsContent(this.cmsWidgetName, "BodyText"); }, ModalSubBodyText() { diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js index 68de04515..9564b5c1f 100644 --- a/src/helpers/pricing-helper.js +++ b/src/helpers/pricing-helper.js @@ -2,6 +2,7 @@ import store from "@/store"; import { storeActions } from "@/constants/store-actions"; import baseMixin from "@/mixins/base-mixin.js"; import { deepClone } from "@/helpers/object-helper"; +import { partNumberStrings } from "@/constants/part-number-strings"; export function getDisplayAmountDue(lineItemsObject, includeTax = true) { return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", { @@ -32,6 +33,10 @@ export function getAmountDue(lineItemsObject, includeTax = true) { if (store.getters.coverageIsVerified && !order.policy.isNoComp && !order.policy.isItac) { amountDue = order.policy.currentDeductible; + + if (order.isMSRFeeApplicable && !order.isMSRFeeCoveredByInsurance) { + amountDue += getMSRFeePartPrice(lineItemsObject?.supportingItems, includeTax); + } } if (lineItemsObject?.vaps) { @@ -100,6 +105,20 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) { return pricingResults[0]; } +export function getMSRFeePartPrice(supportingItems, includeTax) { + let msrFeePrice = 0; + const msrFeeLineItem = supportingItems?.find( + (lineItem) => lineItem.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE + ); + if (msrFeeLineItem) { + msrFeePrice = baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts( + [msrFeeLineItem], + includeTax + ); + } + return msrFeePrice; +} + export function addPricesToLineItems(lineItems, pricingLineItems) { lineItems.forEach((lineItem) => { const lineItemIndex = pricingLineItems.findIndex( diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index fa0e53425..e26c5f9b1 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -62,6 +62,7 @@ :showAsPaid="isPia" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" @@ -70,7 +71,8 @@ :isItac="isItac" :isNoComp="isNoComp" :isExpandedOnLoad="false" - :isMSRFeeApplicable="isMSRFeeApplicable" /> + :isMSRFeeApplicable="isMSRFeeApplicable" + :IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance" />
@@ -419,6 +421,9 @@ export default { isMSRFeeApplicable() { return this.submittedOrder?.isMSRFeeApplicable; }, + isMSRFeeCoveredByInsurance() { + return this.submittedOrder?.isMSRFeeCoveredByInsurance; + }, isInsurance() { return this.submittedOrder?.payment?.isInsurance; }, diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 4a4936a7f..693263ebb 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -29,6 +29,7 @@ v-model="lineItems" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 48f076f84..e5b08bdbc 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -26,6 +26,7 @@ pageName="payment-method" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + msrModalCmsWidgetName="MSRModal" :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" @@ -35,7 +36,9 @@ :isNoComp="isNoComp" :isExpandedOnLoad="false" :isMSRFeeApplicable="isMSRFeeApplicable" - @itemRemoved="evaluatePromosAndTaxItemsOnOrder" /> + :IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance" + @itemRemoved="evaluatePromosAndTaxItemsOnOrder" + @switchToInshop="navigateToSchedulePage" /> { isRecalAckOptIn: false, isRecalAcknowledgedForScheduling: "", isMSRFeeApplicable: false, + isMSRFeeCoveredByInsurance: false, }, applicationUser: { eventBus: [], @@ -359,6 +360,9 @@ export const mutations = { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { state.order.isMSRFeeApplicable = isMSRFeeApplicable; }, + updateIsMSRFeeCoveredByInsurance(state, isMSRFeeCoveredByInsurance) { + state.order.isMSRFeeCoveredByInsurance = isMSRFeeCoveredByInsurance; + }, updateCashPriceSubTotal(state, cashPriceSubTotal) { state.order.cashPriceSubTotal = cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString(); @@ -2611,6 +2615,7 @@ export const actions = { lockToken: order.lockToken, isRecalAckOptIn: order.isRecalAckOptIn, isMSRFeeApplicable: order.isMSRFeeApplicable, + isMSRFeeCoveredByInsurance: order.isMSRFeeCoveredByInsurance, cashPriceSubTotal: order.cashPriceSubTotal, }, }, @@ -3043,6 +3048,13 @@ export const actions = { context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable); }, + saveIsMSRFeeCoveredByInsurance(context, isMSRFeeCoveredByInsurance) { + context.commit( + storeMutations.UPDATE_IS_MSR_FEE_COVERED_BY_INSURANCE, + isMSRFeeCoveredByInsurance + ); + }, + saveParentAccountNumber(context, parentAccountNumber) { context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber); }, From 4c82d807999785e160b5be66b4000387af8547b6 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Fri, 27 Mar 2026 08:12:15 -0400 Subject: [PATCH 21/25] CASH-1689: Prettier --- src/mixins/analytics-mixin.js | 6 ++++-- src/store/index.js | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 9386846d7..3ab575f58 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -165,7 +165,7 @@ export default { if (!currentPageName) { return; } - + const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.getters.order; @@ -934,7 +934,9 @@ function getPageNameFromRouter(useDefaultUrl = false) { return router.currentRoute.value.name; } - return useDefaultUrl ? window.location.search : window.location.href.replace(/\/$/, "").split("/").pop(); + return useDefaultUrl + ? window.location.search + : window.location.href.replace(/\/$/, "").split("/").pop(); } function getValueToLog(value, valueToLogType) { diff --git a/src/store/index.js b/src/store/index.js index d76a0e0da..94128a402 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1634,7 +1634,6 @@ export const actions = { }); }, - logFmgSessionData( context, { From 2f71aa32f2641cc1c4dd36ff627b6aea5b837c87 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Fri, 27 Mar 2026 14:53:57 -0400 Subject: [PATCH 22/25] CASH-1689: Add feature toggle for MSR Split pay --- src/constants/experiments.js | 1 + src/fmg-components/cart/cart.vue | 1 + src/layouts/payment-adyen/payment-adyen.vue | 4 ++++ src/layouts/payment/payment.vue | 4 ++++ src/layouts/schedule/schedule.vue | 14 ++++++++++++-- 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index b9f38ca36..97e049df0 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -23,6 +23,7 @@ const experimentSettings = { INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL: "NextGen_InternalInsuranceTabDisplayThreshold", INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL: "NextGen_ExternalInsuranceTabDisplayThreshold", DISPLAY_MSR: "DisplayMSR", + ENABLE_MSR_SPLIT_PAY: "EnableMSRSplitPay", DISPLAY_WAITLIST: "DisplayWaitlist2.0", WAITLIST_THRESHOLD_DAYS: "Waitlist_Threshold_Days", PRICING_BY_DAY: "DisplayPricingByDay", diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 7de87b678..f4626caa5 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -981,6 +981,7 @@ export default { let cartItem = null; const msrFeeLineItem = this.getMsrFeeLineItem; const shouldCreateMsrFeeCartItem = + this.isMSRFeeApplicable && this.isInsurance && !this.IsMSRFeeCoveredByInsurance && msrFeeLineItem && diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index 693263ebb..bcb827623 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -34,6 +34,7 @@ :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" + :isMSRFeeApplicable="isMSRFeeApplicable" :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> @@ -569,6 +570,9 @@ export default { insuranceCompanyName() { return this.$store.getters.policy.insuranceCompanyName; }, + isMSRFeeApplicable() { + return this.$store.getters.order.isMSRFeeApplicable; + }, lineItems() { return deepClone(this.$store.getters.lineItems); }, diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 68e57ba9a..739376d94 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -52,6 +52,7 @@ :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" + :isMSRFeeApplicable="isMSRFeeApplicable" :isItac="isItac" :isNoComp="isNoComp" :isCollapsible="false" /> @@ -411,6 +412,9 @@ export default { insuranceCompanyName() { return this.$store.getters.policy.insuranceCompanyName; }, + isMSRFeeApplicable() { + return this.$store.getters.order.isMSRFeeApplicable; + }, isPaypal() { if (this.paymentType == "pp") { return true; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 967ea1828..8068ac6c1 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -704,8 +704,11 @@ export default { }, isMobileStaticRecalibrationApplicable() { return ( - this.displayMSR && - this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE + (this.displayMSR && + this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE && + this.enableMSRSplitPay) || + this.isCashItacNoComp || + this.mobileFeePart?.isInsurable ); }, displayMSR() { @@ -715,6 +718,13 @@ export default { ?.toLowerCase() === "true" ); }, + enableMSRSplitPay() { + return ( + experimentMixin.methods + .getSettingValue(experimentSettings.ENABLE_MSR_SPLIT_PAY) + ?.toLowerCase() === "true" + ); + }, isMSRFeeNotCoveredByInsurance() { return ( this.isMobileSelected && From 0b7d0a9083a6f7b28b2dc39976b3b3b8e8f90b9b Mon Sep 17 00:00:00 2001 From: CarlNation Date: Fri, 27 Mar 2026 23:14:16 -0400 Subject: [PATCH 23/25] CASH-2481 add null check to fix error involving external parameter CASH-2481 add null check to fix error involving external parameter --- src/layouts/vehicle/vehicle.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index 92a7aa810..ac4a54f62 100644 --- a/src/layouts/vehicle/vehicle.vue +++ b/src/layouts/vehicle/vehicle.vue @@ -253,17 +253,17 @@ export default { const matchingMake = resultMap.makeQuestionInitialData?.filter( (item) => item.toLowerCase() === - store.getters.externalParameterVehicle.make.toLowerCase() + store.getters.externalParameterVehicle.make?.toLowerCase() ); const matchingModel = resultMap.modelQuestionInitialData?.filter( (item) => item.toLowerCase() === - store.getters.externalParameterVehicle.model.toLowerCase() + store.getters.externalParameterVehicle.model?.toLowerCase() ); const matchingStyle = resultMap.styleQuestionInitialData?.filter( (item) => item.toLowerCase() === - store.getters.externalParameterVehicle.style.toLowerCase() + store.getters.externalParameterVehicle.style?.toLowerCase() ); baseMixin.methods.dispatchStoreAction( storeActions.UPDATE_EXTERNAL_PARAMETER_MMS, From 2391748d6059e5231439e9cf7556c3e7c60d64ee Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 30 Mar 2026 14:39:30 -0400 Subject: [PATCH 24/25] Always set vm.isInsuranceSelected Some cases were not setting it if external parameter state was marked 0 or 1 --- src/layouts/quote/quote.vue | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index b428ab905..339e543df 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -583,6 +583,8 @@ export default { debugLog("--- quote.vue thresholds end ---"); } + let setInsuranceSelected = true; + if (isExternalParameter === externalParameterStatus.NOT_SET) { // prettier-ignore { @@ -590,12 +592,6 @@ export default { } // there are no active or inactive external parameters; use internal threshold if (internalThreshold) thresholdToUse = internalThreshold; - - vm.isInsuranceSelected = getIsInsuranceSelectedValue( - vm.availableLineItems, - thresholdToUse - ); - baseMixin.methods.ResetExternalParamsAndHideModal(); } else { // user came from an external source, however, the externalParms may have been reset on service-zip, property-questions, etc... if (getBoolFromString(store.getters.externalParameterQuote?.isInsurance)) { @@ -614,7 +610,9 @@ export default { await vm.skip(insuranceSelection, servicePackage); } - baseMixin.methods.ResetExternalParamsAndHideModal(); + if (vm.isInsuranceSelected != null) { + setInsuranceSelected = false; + } } else { if (externalThreshold) thresholdToUse = externalThreshold; @@ -622,13 +620,16 @@ export default { { debugLog("quote.vue tab select NOT EXTERNAL"); } + } + //Set isInsuranceSelected if + if (setInsuranceSelected) { vm.isInsuranceSelected = getIsInsuranceSelectedValue( vm.availableLineItems, thresholdToUse ); - baseMixin.methods.ResetExternalParamsAndHideModal(); } + baseMixin.methods.ResetExternalParamsAndHideModal(); } if (vm.skipToInsurance && !vm.showSaveProgressPopup) { From 849443fa1c849b946720a1b7809aaac4e9ae08b6 Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Mon, 30 Mar 2026 15:42:33 -0400 Subject: [PATCH 25/25] simplify logic -> set isInsuranceSelected if its null/undefined --- src/layouts/quote/quote.vue | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 339e543df..106c04bfa 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -583,8 +583,6 @@ export default { debugLog("--- quote.vue thresholds end ---"); } - let setInsuranceSelected = true; - if (isExternalParameter === externalParameterStatus.NOT_SET) { // prettier-ignore { @@ -609,10 +607,6 @@ export default { ) { await vm.skip(insuranceSelection, servicePackage); } - - if (vm.isInsuranceSelected != null) { - setInsuranceSelected = false; - } } else { if (externalThreshold) thresholdToUse = externalThreshold; @@ -621,17 +615,16 @@ export default { debugLog("quote.vue tab select NOT EXTERNAL"); } } - - //Set isInsuranceSelected if - if (setInsuranceSelected) { - vm.isInsuranceSelected = getIsInsuranceSelectedValue( - vm.availableLineItems, - thresholdToUse - ); - } - baseMixin.methods.ResetExternalParamsAndHideModal(); } + if (vm.isInsuranceSelected == null) { + vm.isInsuranceSelected = getIsInsuranceSelectedValue( + vm.availableLineItems, + thresholdToUse + ); + } + baseMixin.methods.ResetExternalParamsAndHideModal(); + if (vm.skipToInsurance && !vm.showSaveProgressPopup) { // skip ahead only if no email popup. await vm.skip(true, externalParamPackageLabels.GLASS_ONLY);